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;