diff --git a/sample-operators/mysql-schema/README.md b/sample-operators/mysql-schema/README.md
new file mode 100644
index 0000000000..366dcf3e42
--- /dev/null
+++ b/sample-operators/mysql-schema/README.md
@@ -0,0 +1,76 @@
+# MySQL Schema Operator
+
+This example shows how an operator can control resources outside of the Kubernetes cluster. In this case it will be
+managing MySQL schemas in an existing database server. This is a common scenario in many organizations where developers
+need to create schemas for different applications and environments, but the database server itself is managed by a
+different team. Using this operator a dev team can create a CR in their namespace and have a schema provisioned automatically.
+Access to the MySQL server is configured in the configuration of the operator, so admin access is restricted.
+
+This is an example input:
+```yaml
+apiVersion: "mysql.sample.javaoperatorsdk/v1"
+kind: MySQLSchema
+metadata:
+ name: mydb
+spec:
+ encoding: utf8
+```
+
+Creating this custom resource will prompt the operator to create a schema named `mydb` in the MySQL server and update
+the resource status with its URL. Once the resource is deleted, the operator will delete the schema. Obviously don't
+use it as is with real databases.
+
+### Try
+
+To try how the operator works you will need the following:
+* JDK installed (minimum version 11, tested with 11 and 15)
+* Maven installed (tested with 3.6.3)
+* A working Kubernetes cluster (tested with v1.15.9-gke.24)
+* kubectl installed (tested with v1.15.5)
+* Docker installed (tested with 19.03.8)
+* Container image registry
+
+How to configure all the above depends heavily on where your Kubernetes cluster is hosted.
+If you use [minikube](https://minikube.sigs.k8s.io/docs/) you will need to configure kubectl and docker differently
+than if you'd use [GKE](https://cloud.google.com/kubernetes-engine/). You will have to read the documentation of your
+Kubernetes provider to figure this out.
+
+Once you have the basics you can build and deploy the operator.
+
+### Build & Deploy
+
+1. We will be building the Docker image from the source code using Maven, so we have to configure the Docker registry
+where the image should be pushed. Do this in mysql-schema/pom.xml. In the example below I'm setting it to
+the [Container Registry](https://cloud.google.com/container-registry/) in Google Cloud Europe.
+
+```xml
+
+ eu.gcr.io/my-gcp-project/mysql-operator
+
+```
+
+1. The following Maven command will build the JAR file, package it as a Docker image and push it to the registry.
+
+ `mvn jib:dockerBuild`
+
+1. Deploy the test MySQL on your cluster if you want to use it. Note that if you have an already running MySQL server
+you want to use, you can skip this step, but you will have to configure the operator to use that server.
+
+ `kubectl apply -f k8s/mysql-db.yaml`
+1. Deploy the CRD:
+
+ `kubectl apply -f k8s/crd.yaml`
+
+1. Make a copy of `k8s/operator.yaml` and replace ${DOCKER_REGISTRY} and ${OPERATOR_VERSION} to the
+right values. You will want to set `OPERATOR_VERSION` to the one used for building the Docker image. `DOCKER_REGISTRY` should
+be the same as you set the docker-registry property in your `pom.xml`.
+If you look at the environment variables you will notice this is where the access to the MySQL server is configured.
+The default values assume the server is running in another Kubernetes namespace (called `mysql`), uses the `root` user
+with a not very secure password. In case you want to use a different MySQL server, this is where you configure it.
+
+1. Run `kubectl apply -f copy-of-operator.yaml` to deploy the operator. You can wait for the deployment to succeed using
+this command: `kubectl rollout status deployment mysql-schema-operator -w`. `-w` will cause kubectl to continuously monitor
+the deployment until you stop it.
+
+1. Now you are ready to create some databases! To create a database schema called `mydb` just apply the `k8s/schema.yaml`
+file with kubectl: `kubectl apply -f k8s/schema.yaml`. You can modify the database name in the file to create more schemas.
diff --git a/sample-operators/mysql-schema/k8s/mysql-deployment.yaml b/sample-operators/mysql-schema/k8s/mysql-deployment.yaml
new file mode 100644
index 0000000000..9dfe210212
--- /dev/null
+++ b/sample-operators/mysql-schema/k8s/mysql-deployment.yaml
@@ -0,0 +1,25 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: mysql
+spec:
+ selector:
+ matchLabels:
+ app: mysql
+ strategy:
+ type: Recreate
+ template:
+ metadata:
+ labels:
+ app: mysql
+ spec:
+ containers:
+ - image: mysql:5.6
+ name: mysql
+ env:
+ # Use secret in real usage
+ - name: MYSQL_ROOT_PASSWORD
+ value: password
+ ports:
+ - containerPort: 3306
+ name: mysql
\ No newline at end of file
diff --git a/sample-operators/mysql-schema/k8s/mysql-service.yaml b/sample-operators/mysql-schema/k8s/mysql-service.yaml
new file mode 100644
index 0000000000..3b4188373d
--- /dev/null
+++ b/sample-operators/mysql-schema/k8s/mysql-service.yaml
@@ -0,0 +1,10 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: mysql
+spec:
+ ports:
+ - port: 3306
+ selector:
+ app: mysql
+ type: LoadBalancer
\ No newline at end of file
diff --git a/sample-operators/mysql-schema/k8s/operator.yaml b/sample-operators/mysql-schema/k8s/operator.yaml
new file mode 100644
index 0000000000..f3e667f3ee
--- /dev/null
+++ b/sample-operators/mysql-schema/k8s/operator.yaml
@@ -0,0 +1,101 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: mysql-schema-operator
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: mysql-schema-operator
+ namespace: mysql-schema-operator
+spec:
+ selector:
+ matchLabels:
+ app: mysql-schema-operator
+ replicas: 1 # we always run a single replica of the operator to avoid duplicate handling of events
+ strategy:
+ type: Recreate # during an upgrade the operator will shut down before the new version comes up to prevent two instances running at the same time
+ template:
+ metadata:
+ labels:
+ app: mysql-schema-operator
+ spec:
+ serviceAccountName: mysql-schema-operator # specify the ServiceAccount under which's RBAC persmissions the operator will be executed under
+ containers:
+ - name: operator
+ image: ${DOCKER_REGISTRY}/mysql-schema-operator:${OPERATOR_VERSION}
+ imagePullPolicy: Always
+ ports:
+ - containerPort: 80
+ env:
+ - name: MYSQL_HOST
+ value: mysql.mysql # assuming the MySQL server runs in a namespace called "mysql" on Kubernetes
+ - name: MYSQL_USER
+ value: root
+ - name: MYSQL_PASSWORD
+ value: password # sample-level security
+ readinessProbe:
+ httpGet:
+ path: /health # when this returns 200 the operator is considered up and running
+ port: 8080
+ initialDelaySeconds: 1
+ timeoutSeconds: 1
+ livenessProbe:
+ httpGet:
+ path: /health # when this endpoint doesn't return 200 the operator is considered broken and get's restarted
+ port: 8080
+ initialDelaySeconds: 30
+ timeoutSeconds: 1
+
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: mysql-schema-operator
+ namespace: mysql-schema-operator
+
+---
+apiVersion: rbac.authorization.k8s.io/v1beta1
+kind: ClusterRole
+metadata:
+ name: mysql-schema-operator
+rules:
+- apiGroups:
+ - mysql.sample.javaoperatorsdk
+ resources:
+ - schemas
+ verbs:
+ - "*"
+- apiGroups:
+ - mysql.sample.javaoperatorsdk
+ resources:
+ - schemas/status
+ verbs:
+ - "*"
+- apiGroups:
+ - apiextensions.k8s.io
+ resources:
+ - customresourcedefinitions
+ verbs:
+ - "get"
+ - "list"
+- apiGroups:
+ - ""
+ resources:
+ - secrets
+ verbs:
+ - "*"
+
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: mysql-schema-operator
+subjects:
+- kind: ServiceAccount
+ name: mysql-schema-operator
+ namespace: mysql-schema-operator
+roleRef:
+ kind: ClusterRole
+ name: mysql-schema-operator
+ apiGroup: ""
diff --git a/sample-operators/mysql-schema/k8s/schema.yaml b/sample-operators/mysql-schema/k8s/schema.yaml
new file mode 100644
index 0000000000..054c9a1695
--- /dev/null
+++ b/sample-operators/mysql-schema/k8s/schema.yaml
@@ -0,0 +1,6 @@
+apiVersion: "mysql.sample.javaoperatorsdk/v1"
+kind: MySQLSchema
+metadata:
+ name: mydb
+spec:
+ encoding: utf8
\ No newline at end of file
diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml
new file mode 100644
index 0000000000..393b8dab36
--- /dev/null
+++ b/sample-operators/mysql-schema/pom.xml
@@ -0,0 +1,96 @@
+
+
+ 4.0.0
+
+
+ io.javaoperatorsdk
+ sample-operators
+ 2.0.0-SNAPSHOT
+
+
+ sample-mysql-schema-operator
+ Operator SDK - Samples - MySQL Schema
+ Provisions Schemas in a MySQL database
+ jar
+
+
+ 11
+ 11
+ 3.1.4
+
+
+
+
+ io.javaoperatorsdk
+ operator-framework
+ ${project.version}
+
+
+ org.takes
+ takes
+ 1.19
+
+
+ mysql
+ mysql-connector-java
+ 8.0.26
+
+
+ io.fabric8
+ crd-generator-apt
+ provided
+
+
+ org.apache.logging.log4j
+ log4j-slf4j-impl
+ 2.13.3
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ org.awaitility
+ awaitility
+ 4.1.0
+ test
+
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-yaml
+ 2.13.0
+
+
+
+
+
+
+ com.google.cloud.tools
+ jib-maven-plugin
+ ${jib-maven-plugin.version}
+
+
+ gcr.io/distroless/java:11
+
+
+ mysql-schema-operator
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.8.1
+
+
+
+
+
\ No newline at end of file
diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLDbConfig.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLDbConfig.java
new file mode 100644
index 0000000000..7cc06dd373
--- /dev/null
+++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLDbConfig.java
@@ -0,0 +1,46 @@
+package io.javaoperatorsdk.operator.sample;
+
+import org.apache.commons.lang3.ObjectUtils;
+
+public class MySQLDbConfig {
+
+ private final String host;
+ private final String port;
+ private final String user;
+ private final String password;
+
+ public MySQLDbConfig(String host, String port, String user, String password) {
+ this.host = host;
+ this.port = port != null ? port : "3306";
+ this.user = user;
+ this.password = password;
+ }
+
+ public static MySQLDbConfig loadFromEnvironmentVars() {
+ if (ObjectUtils.anyNull(System.getenv("MYSQL_HOST"),
+ System.getenv("MYSQL_USER"), System.getenv("MYSQL_PASSWORD"))) {
+ throw new IllegalStateException("Mysql server parameters not defined");
+ }
+ return new MySQLDbConfig(System.getenv("MYSQL_HOST"),
+ System.getenv("MYSQL_PORT"),
+ System.getenv("MYSQL_USER"),
+ System.getenv("MYSQL_PASSWORD"));
+ }
+
+ public String getHost() {
+ return host;
+ }
+
+ public String getPort() {
+ return port;
+ }
+
+ public String getUser() {
+ return user;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+}
+
diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchema.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchema.java
new file mode 100644
index 0000000000..80eb25f8c7
--- /dev/null
+++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchema.java
@@ -0,0 +1,11 @@
+package io.javaoperatorsdk.operator.sample;
+
+import io.fabric8.kubernetes.api.model.Namespaced;
+import io.fabric8.kubernetes.client.CustomResource;
+import io.fabric8.kubernetes.model.annotation.Group;
+import io.fabric8.kubernetes.model.annotation.Version;
+
+@Group("mysql.sample.javaoperatorsdk")
+@Version("v1")
+public class MySQLSchema extends CustomResource implements Namespaced {
+}
diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java
new file mode 100644
index 0000000000..e8f25c84bd
--- /dev/null
+++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java
@@ -0,0 +1,34 @@
+package io.javaoperatorsdk.operator.sample;
+
+import java.io.IOException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.takes.facets.fork.FkRegex;
+import org.takes.facets.fork.TkFork;
+import org.takes.http.Exit;
+import org.takes.http.FtBasic;
+
+import io.fabric8.kubernetes.client.Config;
+import io.fabric8.kubernetes.client.ConfigBuilder;
+import io.fabric8.kubernetes.client.DefaultKubernetesClient;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import io.javaoperatorsdk.operator.Operator;
+import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService;
+
+public class MySQLSchemaOperator {
+
+ private static final Logger log = LoggerFactory.getLogger(MySQLSchemaOperator.class);
+
+ public static void main(String[] args) throws IOException {
+ log.info("MySQL Schema Operator starting");
+
+ Config config = new ConfigBuilder().withNamespace(null).build();
+ KubernetesClient client = new DefaultKubernetesClient(config);
+ Operator operator = new Operator(client, DefaultConfigurationService.instance());
+ operator.register(new MySQLSchemaReconciler(client, MySQLDbConfig.loadFromEnvironmentVars()));
+ operator.start();
+
+ new FtBasic(new TkFork(new FkRegex("/health", "ALL GOOD!")), 8080).start(Exit.NEVER);
+ }
+}
diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java
new file mode 100644
index 0000000000..47d44246e3
--- /dev/null
+++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java
@@ -0,0 +1,168 @@
+package io.javaoperatorsdk.operator.sample;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Base64;
+
+import org.apache.commons.lang3.RandomStringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.fabric8.kubernetes.api.model.Secret;
+import io.fabric8.kubernetes.api.model.SecretBuilder;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import io.javaoperatorsdk.operator.api.reconciler.*;
+
+import static java.lang.String.format;
+
+@ControllerConfiguration
+public class MySQLSchemaReconciler implements Reconciler {
+ static final String USERNAME_FORMAT = "%s-user";
+ static final String SECRET_FORMAT = "%s-secret";
+
+ private final Logger log = LoggerFactory.getLogger(getClass());
+
+ private final KubernetesClient kubernetesClient;
+ private final MySQLDbConfig mysqlDbConfig;
+
+ public MySQLSchemaReconciler(KubernetesClient kubernetesClient, MySQLDbConfig mysqlDbConfig) {
+ this.kubernetesClient = kubernetesClient;
+ this.mysqlDbConfig = mysqlDbConfig;
+ }
+
+ @Override
+ public UpdateControl reconcile(MySQLSchema schema,
+ Context context) {
+ try (Connection connection = getConnection()) {
+ if (!schemaExists(connection, schema.getMetadata().getName())) {
+ try (Statement statement = connection.createStatement()) {
+ statement.execute(
+ format(
+ "CREATE SCHEMA `%1$s` DEFAULT CHARACTER SET %2$s",
+ schema.getMetadata().getName(), schema.getSpec().getEncoding()));
+ }
+
+ String password = RandomStringUtils.randomAlphanumeric(16);
+ String userName = String.format(USERNAME_FORMAT, schema.getMetadata().getName());
+ String secretName = String.format(SECRET_FORMAT, schema.getMetadata().getName());
+ try (Statement statement = connection.createStatement()) {
+ statement.execute(format("CREATE USER '%1$s' IDENTIFIED BY '%2$s'", userName, password));
+ }
+ try (Statement statement = connection.createStatement()) {
+ statement.execute(
+ format("GRANT ALL ON `%1$s`.* TO '%2$s'", schema.getMetadata().getName(), userName));
+ }
+ Secret credentialsSecret =
+ new SecretBuilder()
+ .withNewMetadata()
+ .withName(secretName)
+ .endMetadata()
+ .addToData(
+ "MYSQL_USERNAME", Base64.getEncoder().encodeToString(userName.getBytes()))
+ .addToData(
+ "MYSQL_PASSWORD", Base64.getEncoder().encodeToString(password.getBytes()))
+ .build();
+ this.kubernetesClient
+ .secrets()
+ .inNamespace(schema.getMetadata().getNamespace())
+ .create(credentialsSecret);
+
+ SchemaStatus status = new SchemaStatus();
+ status.setUrl(
+ format(
+ "jdbc:mysql://%1$s/%2$s",
+ System.getenv("MYSQL_HOST"), schema.getMetadata().getName()));
+ status.setUserName(userName);
+ status.setSecretName(secretName);
+ status.setStatus("CREATED");
+ schema.setStatus(status);
+ log.info("Schema {} created - updating CR status", schema.getMetadata().getName());
+
+ return UpdateControl.updateStatus(schema);
+ }
+ return UpdateControl.noUpdate();
+ } catch (SQLException e) {
+ log.error("Error while creating Schema", e);
+
+ SchemaStatus status = new SchemaStatus();
+ status.setUrl(null);
+ status.setUserName(null);
+ status.setSecretName(null);
+ status.setStatus("ERROR: " + e.getMessage());
+ schema.setStatus(status);
+
+ return UpdateControl.updateStatus(schema);
+ }
+ }
+
+ @Override
+ public DeleteControl cleanup(MySQLSchema schema, Context context) {
+ log.info("Execution deleteResource for: {}", schema.getMetadata().getName());
+
+ try (Connection connection = getConnection()) {
+ if (schemaExists(connection, schema.getMetadata().getName())) {
+ try (Statement statement = connection.createStatement()) {
+ statement.execute(format("DROP DATABASE `%1$s`", schema.getMetadata().getName()));
+ }
+ log.info("Deleted Schema '{}'", schema.getMetadata().getName());
+
+ if (schema.getStatus() != null) {
+ if (userExists(connection, schema.getStatus().getUserName())) {
+ try (Statement statement = connection.createStatement()) {
+ statement.execute(format("DROP USER '%1$s'", schema.getStatus().getUserName()));
+ }
+ log.info("Deleted User '{}'", schema.getStatus().getUserName());
+ }
+ }
+
+ this.kubernetesClient
+ .secrets()
+ .inNamespace(schema.getMetadata().getNamespace())
+ .withName(schema.getStatus().getSecretName())
+ .delete();
+ } else {
+ log.info(
+ "Delete event ignored for schema '{}', real schema doesn't exist",
+ schema.getMetadata().getName());
+ }
+ return DeleteControl.defaultDelete();
+ } catch (SQLException e) {
+ log.error("Error while trying to delete Schema", e);
+ return DeleteControl.noFinalizerRemoval();
+ }
+ }
+
+ private Connection getConnection() throws SQLException {
+ String connectionString =
+ format("jdbc:mysql://%1$s:%2$s", mysqlDbConfig.getHost(), mysqlDbConfig.getPort());
+
+ log.info("Connecting to '{}' with user '{}'", connectionString, mysqlDbConfig.getUser());
+ return DriverManager.getConnection(connectionString, mysqlDbConfig.getUser(),
+ mysqlDbConfig.getPassword());
+ }
+
+ private boolean schemaExists(Connection connection, String schemaName) throws SQLException {
+ try (PreparedStatement ps =
+ connection.prepareStatement(
+ "SELECT schema_name FROM information_schema.schemata WHERE schema_name = ?")) {
+ ps.setString(1, schemaName);
+ try (ResultSet resultSet = ps.executeQuery()) {
+ return resultSet.next();
+ }
+ }
+ }
+
+ private boolean userExists(Connection connection, String userName) throws SQLException {
+ try (PreparedStatement ps =
+ connection.prepareStatement("SELECT User FROM mysql.user WHERE User = ?")) {
+ ps.setString(1, userName);
+ try (ResultSet resultSet = ps.executeQuery()) {
+ return resultSet.first();
+ }
+ }
+ }
+}
diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaSpec.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaSpec.java
new file mode 100644
index 0000000000..19101c328a
--- /dev/null
+++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaSpec.java
@@ -0,0 +1,14 @@
+package io.javaoperatorsdk.operator.sample;
+
+public class SchemaSpec {
+
+ private String encoding;
+
+ public String getEncoding() {
+ return encoding;
+ }
+
+ public void setEncoding(String encoding) {
+ this.encoding = encoding;
+ }
+}
diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaStatus.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaStatus.java
new file mode 100644
index 0000000000..168cd8db15
--- /dev/null
+++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaStatus.java
@@ -0,0 +1,44 @@
+package io.javaoperatorsdk.operator.sample;
+
+public class SchemaStatus {
+
+ private String url;
+
+ private String status;
+
+ private String userName;
+
+ private String secretName;
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ public String getUserName() {
+ return userName;
+ }
+
+ public void setUserName(String userName) {
+ this.userName = userName;
+ }
+
+ public String getSecretName() {
+ return secretName;
+ }
+
+ public void setSecretName(String secretName) {
+ this.secretName = secretName;
+ }
+}
diff --git a/sample-operators/mysql-schema/src/main/resources/log4j2.xml b/sample-operators/mysql-schema/src/main/resources/log4j2.xml
new file mode 100644
index 0000000000..5ab4735126
--- /dev/null
+++ b/sample-operators/mysql-schema/src/main/resources/log4j2.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java b/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java
new file mode 100644
index 0000000000..75c5412e9c
--- /dev/null
+++ b/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java
@@ -0,0 +1,112 @@
+package io.javaoperatorsdk.operator.sample;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.fabric8.kubernetes.api.model.*;
+import io.fabric8.kubernetes.api.model.apps.*;
+import io.fabric8.kubernetes.client.*;
+import io.fabric8.kubernetes.client.Config;
+import io.fabric8.kubernetes.client.ConfigBuilder;
+import io.javaoperatorsdk.operator.Operator;
+import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.notNullValue;
+
+@Disabled
+public class MySQLSchemaOperatorE2E {
+
+ final static String TEST_NS = "mysql-schema-test";
+
+ final static Logger log = LoggerFactory.getLogger(MySQLSchemaOperatorE2E.class);
+
+ @Test
+ public void test() throws IOException {
+ Config config = new ConfigBuilder().withNamespace(null).build();
+ KubernetesClient client = new DefaultKubernetesClient(config);
+
+ // Use this if you want to run the test without deploying the Operator to Kubernetes
+ if ("true".equals(System.getenv("RUN_OPERATOR_IN_TEST"))) {
+ Operator operator = new Operator(client, DefaultConfigurationService.instance());
+ MySQLDbConfig dbConfig = new MySQLDbConfig("mysql", null, "root", "password");
+ operator.register(new MySQLSchemaReconciler(client, dbConfig));
+ operator.start();
+ }
+
+ MySQLSchema testSchema = new MySQLSchema();
+ testSchema.setMetadata(new ObjectMetaBuilder()
+ .withName("mydb1")
+ .withNamespace(TEST_NS)
+ .build());
+ testSchema.setSpec(new SchemaSpec());
+ testSchema.getSpec().setEncoding("utf8");
+
+ Namespace testNs = new NamespaceBuilder().withMetadata(
+ new ObjectMetaBuilder().withName(TEST_NS).build()).build();
+
+ if (testNs != null) {
+ // We perform a pre-run cleanup instead of a post-run cleanup. This is to help with debugging
+ // test results when running against a persistent cluster. The test namespace would stay
+ // after the test run so we can check what's there, but it would be cleaned up during the next
+ // test run.
+ log.info("Cleanup: deleting test namespace {}", TEST_NS);
+ client.namespaces().delete(testNs);
+ await().atMost(5, MINUTES)
+ .until(() -> client.namespaces().withName(TEST_NS).get() == null);
+ }
+
+ log.info("Creating test namespace {}", TEST_NS);
+ client.namespaces().create(testNs);
+
+ log.info("Deploying MySQL server");
+ deployMySQLServer(client);
+
+ log.info("Creating test MySQLSchema object: {}", testSchema);
+ // var mysqlSchemaClient = client.customResources(MySQLSchema.class);
+ // mysqlSchemaClient.inNamespace(TEST_NS).createOrReplace(testSchema);
+ client.resource(testSchema).createOrReplace();
+
+ log.info("Waiting 5 minutes for expected resources to be created and updated");
+ await().atMost(5, MINUTES).untilAsserted(() -> {
+ MySQLSchema updatedSchema = client.resources(MySQLSchema.class).inNamespace(TEST_NS)
+ .withName(testSchema.getMetadata().getName()).get();
+ assertThat(updatedSchema.getStatus(), is(notNullValue()));
+ assertThat(updatedSchema.getStatus().getStatus(), equalTo("CREATED"));
+ assertThat(updatedSchema.getStatus().getSecretName(), is(notNullValue()));
+ assertThat(updatedSchema.getStatus().getUserName(), is(notNullValue()));
+ });
+ }
+
+ private void deployMySQLServer(KubernetesClient client) throws IOException {
+ ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
+ Deployment deployment =
+ mapper.readValue(new File("k8s/mysql-deployment.yaml"), Deployment.class);
+ deployment.getMetadata().setNamespace(TEST_NS);
+ Service service = mapper.readValue(new File("k8s/mysql-service.yaml"), Service.class);
+ service.getMetadata().setNamespace(TEST_NS);
+ client.resource(deployment).createOrReplace();
+ client.resource(service).createOrReplace();
+
+ log.info("Waiting for MySQL server to start");
+ await().atMost(5, MINUTES).until(() -> {
+ Deployment mysqlDeployment = client.apps().deployments().inNamespace(TEST_NS)
+ .withName(deployment.getMetadata().getName()).get();
+ return mysqlDeployment.getStatus().getReadyReplicas() != null
+ && mysqlDeployment.getStatus().getReadyReplicas() == 1;
+ });
+ }
+
+}
diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml
index 5d9a497cec..cb47a55049 100644
--- a/sample-operators/pom.xml
+++ b/sample-operators/pom.xml
@@ -21,5 +21,6 @@
tomcat-operator
webpage
+ mysql-schema
\ No newline at end of file
diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml
index ae8605f305..52fdc81cc6 100644
--- a/sample-operators/tomcat-operator/pom.xml
+++ b/sample-operators/tomcat-operator/pom.xml
@@ -43,9 +43,13 @@
1.19
- junit
- junit
- 4.13.2
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
test
diff --git a/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java b/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java
index f3cd473294..e803b70aba 100644
--- a/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java
+++ b/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java
@@ -1,6 +1,6 @@
package io.javaoperatorsdk.operator.sample;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;