diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java index da9843ec40..491ac7fb01 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java @@ -194,8 +194,9 @@ public DeleteControl execute() throws Exception { WorkflowCleanupResult workflowCleanupResult = null; // The cleanup is called also when explicit invocation is true, but the cleaner is not - // implemented - if (managedWorkflow.hasCleaner() || !explicitWorkflowInvocation) { + // implemented, also in case when explicit invocation is false, but there is cleaner + // implemented. + if (managedWorkflow.hasCleaner() && (!explicitWorkflowInvocation || !isCleaner)) { workflowCleanupResult = managedWorkflow.cleanup(resource, context); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java index 36fdc2dee8..a81f524326 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java @@ -1,19 +1,33 @@ package io.javaoperatorsdk.operator.processing; +import java.util.Optional; + import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import io.fabric8.kubernetes.api.model.Secret; import io.javaoperatorsdk.operator.MockKubernetesClient; import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.MockControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.workflow.WorkflowSpec; import io.javaoperatorsdk.operator.api.reconciler.Cleaner; +import io.javaoperatorsdk.operator.api.reconciler.DefaultContext; +import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.processing.dependent.workflow.ManagedWorkflow; +import io.javaoperatorsdk.operator.processing.dependent.workflow.ManagedWorkflowFactory; +import io.javaoperatorsdk.operator.processing.dependent.workflow.Workflow; +import io.javaoperatorsdk.operator.processing.dependent.workflow.WorkflowCleanupResult; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; +import static io.javaoperatorsdk.operator.api.monitoring.Metrics.NOOP; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.Mockito.withSettings; @@ -61,4 +75,59 @@ void usesFinalizerIfThereIfReconcilerImplementsCleaner() { assertThat(controller.useFinalizer()).isTrue(); } + + @ParameterizedTest + @CsvSource({ + "true, true, true, false", + "true, true, false, true", + "false, true, true, true", + "false, true, false, true", + "true, false, true, false", + }) + void callsCleanupOnWorkflowWhenHasCleanerAndReconcilerIsNotCleaner(boolean reconcilerIsCleaner, + boolean workflowIsCleaner, + boolean isExplicitWorkflowInvocation, + boolean workflowCleanerExecuted) throws Exception { + + Reconciler reconciler; + if (reconcilerIsCleaner) { + reconciler = mock(Reconciler.class, withSettings().extraInterfaces(Cleaner.class)); + } else { + reconciler = mock(Reconciler.class); + } + + final var configuration = MockControllerConfiguration.forResource(Secret.class); + + if (reconciler instanceof Cleaner cleaner) { + when(cleaner.cleanup(any(), any())).thenReturn(DeleteControl.noFinalizerRemoval()); + } + + var configurationService = mock(ConfigurationService.class); + var mockWorkflowFactory = mock(ManagedWorkflowFactory.class); + var mockManagedWorkflow = mock(ManagedWorkflow.class); + + when(configuration.getConfigurationService()).thenReturn(configurationService); + var workflowSpec = mock(WorkflowSpec.class); + when(workflowSpec.isExplicitInvocation()).thenReturn(isExplicitWorkflowInvocation); + when(configuration.getWorkflowSpec()).thenReturn(Optional.of(workflowSpec)); + when(configurationService.getMetrics()).thenReturn(NOOP); + when(configurationService.getWorkflowFactory()).thenReturn(mockWorkflowFactory); + when(mockWorkflowFactory.workflowFor(any())).thenReturn(mockManagedWorkflow); + var managedWorkflowMock = workflow(workflowIsCleaner); + when(mockManagedWorkflow.resolve(any(), any())).thenReturn(managedWorkflowMock); + + final var controller = new Controller(reconciler, configuration, + MockKubernetesClient.client(Secret.class)); + + controller.cleanup(new Secret(), new DefaultContext<>(null, controller, new Secret())); + + verify(managedWorkflowMock, times(workflowCleanerExecuted ? 1 : 0)).cleanup(any(), any()); + } + + private Workflow workflow(boolean hasCleaner) { + var workflow = mock(Workflow.class); + when(workflow.cleanup(any(), any())).thenReturn(mock(WorkflowCleanupResult.class)); + when(workflow.hasCleaner()).thenReturn(hasCleaner); + return workflow; + } } diff --git a/sample-operators/mysql-schema/README.md b/sample-operators/mysql-schema/README.md index 366dcf3e42..a0f7999090 100644 --- a/sample-operators/mysql-schema/README.md +++ b/sample-operators/mysql-schema/README.md @@ -23,9 +23,9 @@ 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) +* JDK installed (minimum version 11, tested with 11, 15, and 23) * Maven installed (tested with 3.6.3) -* A working Kubernetes cluster (tested with v1.15.9-gke.24) +* A working Kubernetes cluster (tested with v1.15.9-gke.24 and minikube v1.35.0) * kubectl installed (tested with v1.15.5) * Docker installed (tested with 19.03.8) * Container image registry @@ -59,18 +59,45 @@ you want to use, you can skip this step, but you will have to configure the oper `kubectl apply -f k8s/mysql-db.yaml` 1. Deploy the CRD: - `kubectl apply -f k8s/crd.yaml` + `kubectl apply -f target/classes/META-INF/fabric8/mysqlschemas.mysql.sample.javaoperatorsdk-v1.yml` -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`. +1. Make a copy of `k8s/operator.yaml` and replace `spec.template.spec.containers[0].image` (`$ yq 'select(di == 1).spec.template.spec.containers[0].image' k8s/operator.yaml`) with the operator image that you pushed to your registry. This 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. +this command: `kubectl rollout status deployment -n mysql-schema-operator 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. +file with kubectl: `kubectl apply -f k8s/schema.yaml`. You can modify the database name in the file to create more schemas. To verify, that the schema is installed you need to expose your `LoadBalancer` `mysql` service and you can use the `mysql` + CLI to run `show schemas;` command. For instance, with minikube, this can be done like this: + +``` +$ minikube service mysql -n mysql --url +http://192.168.49.2:30317 + +$ mysql -h 192.168.49.2 -P 30317 --protocol=tcp -u root -ppassword +... + +MariaDB [(none)]> show schemas; ++--------------------+ +| Database | ++--------------------+ +| information_schema | +| mydb | +| mysql | +| performance_schema | +| sys | ++--------------------+ +5 rows in set (0.000 sec) +``` + +Or you can verify it directly with `kubectl` like this: + +``` +$ kubectl get mysqlschemas +NAME AGE +mydb 102s +``` diff --git a/sample-operators/mysql-schema/k8s/mysql-deployment.yaml b/sample-operators/mysql-schema/k8s/mysql-db.yaml similarity index 64% rename from sample-operators/mysql-schema/k8s/mysql-deployment.yaml rename to sample-operators/mysql-schema/k8s/mysql-db.yaml index e25ed60b9d..d80238b32e 100644 --- a/sample-operators/mysql-schema/k8s/mysql-deployment.yaml +++ b/sample-operators/mysql-schema/k8s/mysql-db.yaml @@ -1,3 +1,10 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: mysql + labels: + name: mysql +--- apiVersion: apps/v1 kind: Deployment metadata: @@ -23,4 +30,16 @@ spec: value: password ports: - containerPort: 3306 - name: mysql \ No newline at end of file + name: mysql +--- +apiVersion: v1 +kind: Service +metadata: + name: mysql + namespace: mysql +spec: + ports: + - port: 3306 + selector: + app: mysql + type: LoadBalancer \ 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 deleted file mode 100644 index 4c67148be3..0000000000 --- a/sample-operators/mysql-schema/k8s/mysql-service.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: mysql - namespace: 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 index f4b3296e09..48ddea6a35 100644 --- a/sample-operators/mysql-schema/k8s/operator.yaml +++ b/sample-operators/mysql-schema/k8s/operator.yaml @@ -23,7 +23,7 @@ spec: serviceAccountName: mysql-schema-operator # specify the ServiceAccount under which's RBAC persmissions the operator will be executed under containers: - name: operator - image: mysql-schema-operator + image: mysql-schema-operator # TODO Change this to point to your pushed mysql-schema-operator image imagePullPolicy: IfNotPresent ports: - containerPort: 80 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 index d65ab98647..346ebcb9ef 100644 --- 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 @@ -43,8 +43,7 @@ class MySQLSchemaOperatorE2E { infrastructure.add( new NamespaceBuilder().withNewMetadata().withName(MY_SQL_NS).endMetadata().build()); try { - infrastructure.addAll(client.load(new FileInputStream("k8s/mysql-deployment.yaml")).items()); - infrastructure.addAll(client.load(new FileInputStream("k8s/mysql-service.yaml")).items()); + infrastructure.addAll(client.load(new FileInputStream("k8s/mysql-db.yaml")).items()); } catch (FileNotFoundException e) { e.printStackTrace(); }