diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/api/UpdateControl.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/api/UpdateControl.java index fa02e845a2..09bde6856a 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/api/UpdateControl.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/api/UpdateControl.java @@ -27,6 +27,15 @@ public static UpdateControl updateStatusSubResourc return new UpdateControl<>(customResource, true, false); } + /** + * As a results of this there will be two call to K8S API. First the custom resource will be + * updates then the status sub-resource. + */ + public static UpdateControl updateCustomResourceAndStatus( + T customResource) { + return new UpdateControl<>(customResource, true, true); + } + public static UpdateControl noUpdate() { return new UpdateControl<>(null, false, false); } @@ -42,4 +51,8 @@ public boolean isUpdateStatusSubResource() { public boolean isUpdateCustomResource() { return updateCustomResource; } + + public boolean isUpdateCustomResourceAndStatusSubResource() { + return updateCustomResource && updateStatusSubResource; + } } diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java index b3fc86228b..1193005769 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java @@ -103,12 +103,21 @@ private PostExecutionControl handleCreateOrUpdate( UpdateControl updateControl = controller.createOrUpdateResource(resource, context); CustomResource updatedCustomResource = null; - if (updateControl.isUpdateStatusSubResource()) { + if (updateControl.isUpdateCustomResourceAndStatusSubResource()) { + updatedCustomResource = updateCustomResource(updateControl.getCustomResource()); + updateControl + .getCustomResource() + .getMetadata() + .setResourceVersion(updatedCustomResource.getMetadata().getResourceVersion()); + updatedCustomResource = + customResourceFacade.updateStatus(updateControl.getCustomResource()); + } else if (updateControl.isUpdateStatusSubResource()) { updatedCustomResource = customResourceFacade.updateStatus(updateControl.getCustomResource()); } else if (updateControl.isUpdateCustomResource()) { updatedCustomResource = updateCustomResource(updateControl.getCustomResource()); } + if (updatedCustomResource != null) { return PostExecutionControl.customResourceUpdated(updatedCustomResource); } else { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventDispatcherTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventDispatcherTest.java index 4e37bc573d..f33887c154 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventDispatcherTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventDispatcherTest.java @@ -72,6 +72,19 @@ void updatesOnlyStatusSubResource() { verify(customResourceFacade, never()).replaceWithLock(any()); } + @Test + void updatesBothResourceAndStatus() { + when(controller.createOrUpdateResource(eq(testCustomResource), any())) + .thenReturn(UpdateControl.updateCustomResourceAndStatus(testCustomResource)); + when(customResourceFacade.replaceWithLock(testCustomResource)).thenReturn(testCustomResource); + + eventDispatcher.handleExecution( + executionScopeWithCREvent(Watcher.Action.MODIFIED, testCustomResource)); + + verify(customResourceFacade, times(1)).replaceWithLock(testCustomResource); + verify(customResourceFacade, times(1)).updateStatus(testCustomResource); + } + @Test void callCreateOrUpdateOnModifiedResource() { eventDispatcher.handleExecution( diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/IntegrationTestSupport.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/IntegrationTestSupport.java index 78512f6fb0..7005bd100c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/IntegrationTestSupport.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/IntegrationTestSupport.java @@ -183,6 +183,10 @@ public KubernetesClient getK8sClient() { return crOperations; } + public CustomResource getCustomResource(String name) { + return getCrOperations().inNamespace(TEST_NAMESPACE).withName(name).get(); + } + public Operator getOperator() { return operator; } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/TestUtils.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/TestUtils.java index 8189b9c574..910f6907e4 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/TestUtils.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/TestUtils.java @@ -31,4 +31,12 @@ public static TestCustomResource testCustomResource(String uid) { resource.getSpec().setValue("test-value"); return resource; } + + public static void waitXms(int x) { + try { + Thread.sleep(x); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java new file mode 100644 index 0000000000..8c903fb8d3 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java @@ -0,0 +1,88 @@ +package io.javaoperatorsdk.operator; + +import static io.javaoperatorsdk.operator.IntegrationTestSupport.TEST_NAMESPACE; +import static io.javaoperatorsdk.operator.TestUtils.waitXms; +import static io.javaoperatorsdk.operator.doubleupdate.DoubleUpdateTestCustomResourceController.TEST_ANNOTATION; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.client.DefaultKubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.doubleupdate.DoubleUpdateTestCustomResource; +import io.javaoperatorsdk.operator.doubleupdate.DoubleUpdateTestCustomResourceController; +import io.javaoperatorsdk.operator.doubleupdate.DoubleUpdateTestCustomResourceSpec; +import io.javaoperatorsdk.operator.doubleupdate.DoubleUpdateTestCustomResourceStatus; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class UpdatingResAndSubResIT { + + private IntegrationTestSupport integrationTestSupport = new IntegrationTestSupport(); + + @BeforeEach + public void initAndCleanup() { + KubernetesClient k8sClient = new DefaultKubernetesClient(); + integrationTestSupport.initialize( + k8sClient, new DoubleUpdateTestCustomResourceController(), "doubleupdate-test-crd.yaml"); + integrationTestSupport.cleanup(); + } + + @Test + public void updatesSubResourceStatus() { + integrationTestSupport.teardownIfSuccess( + () -> { + DoubleUpdateTestCustomResource resource = createTestCustomResource("1"); + integrationTestSupport.getCrOperations().inNamespace(TEST_NAMESPACE).create(resource); + + awaitStatusUpdated(resource.getMetadata().getName()); + // wait for sure, there are no more events + waitXms(300); + + DoubleUpdateTestCustomResource customResource = + (DoubleUpdateTestCustomResource) + integrationTestSupport.getCustomResource(resource.getMetadata().getName()); + assertThat(integrationTestSupport.numberOfControllerExecutions()).isEqualTo(1); + assertThat(customResource.getStatus().getState()) + .isEqualTo(DoubleUpdateTestCustomResourceStatus.State.SUCCESS); + assertThat(customResource.getMetadata().getAnnotations().get(TEST_ANNOTATION)) + .isNotNull(); + }); + } + + void awaitStatusUpdated(String name) { + await("cr status updated") + .atMost(5, TimeUnit.SECONDS) + .untilAsserted( + () -> { + DoubleUpdateTestCustomResource cr = + (DoubleUpdateTestCustomResource) + integrationTestSupport + .getCrOperations() + .inNamespace(TEST_NAMESPACE) + .withName(name) + .get(); + assertThat(cr.getMetadata().getFinalizers()).hasSize(1); + assertThat(cr).isNotNull(); + assertThat(cr.getStatus()).isNotNull(); + assertThat(cr.getStatus().getState()) + .isEqualTo(DoubleUpdateTestCustomResourceStatus.State.SUCCESS); + }); + } + + public DoubleUpdateTestCustomResource createTestCustomResource(String id) { + DoubleUpdateTestCustomResource resource = new DoubleUpdateTestCustomResource(); + resource.setMetadata( + new ObjectMetaBuilder() + .withName("doubleupdateresource-" + id) + .withNamespace(TEST_NAMESPACE) + .build()); + resource.setKind("DoubleUpdateSample"); + resource.setSpec(new DoubleUpdateTestCustomResourceSpec()); + resource.getSpec().setValue(id); + return resource; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/doubleupdate/DoubleUpdateTestCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/doubleupdate/DoubleUpdateTestCustomResource.java new file mode 100644 index 0000000000..b5e21452d5 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/doubleupdate/DoubleUpdateTestCustomResource.java @@ -0,0 +1,38 @@ +package io.javaoperatorsdk.operator.doubleupdate; + +import io.fabric8.kubernetes.client.CustomResource; + +public class DoubleUpdateTestCustomResource extends CustomResource { + + private DoubleUpdateTestCustomResourceSpec spec; + + private DoubleUpdateTestCustomResourceStatus status; + + public DoubleUpdateTestCustomResourceSpec getSpec() { + return spec; + } + + public void setSpec(DoubleUpdateTestCustomResourceSpec spec) { + this.spec = spec; + } + + public DoubleUpdateTestCustomResourceStatus getStatus() { + return status; + } + + public void setStatus(DoubleUpdateTestCustomResourceStatus status) { + this.status = status; + } + + @Override + public String toString() { + return "DoubleUpdateTestCustomResource{" + + "spec=" + + spec + + ", status=" + + status + + ", extendedFrom=" + + super.toString() + + '}'; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/doubleupdate/DoubleUpdateTestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/doubleupdate/DoubleUpdateTestCustomResourceController.java new file mode 100644 index 0000000000..62f3a53e7f --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/doubleupdate/DoubleUpdateTestCustomResourceController.java @@ -0,0 +1,53 @@ +package io.javaoperatorsdk.operator.doubleupdate; + +import io.javaoperatorsdk.operator.TestExecutionInfoProvider; +import io.javaoperatorsdk.operator.api.*; +import java.util.HashMap; +import java.util.concurrent.atomic.AtomicInteger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Controller(crdName = DoubleUpdateTestCustomResourceController.CRD_NAME) +public class DoubleUpdateTestCustomResourceController + implements ResourceController, TestExecutionInfoProvider { + + public static final String CRD_NAME = "doubleupdatesamples.sample.javaoperatorsdk"; + private static final Logger log = + LoggerFactory.getLogger(DoubleUpdateTestCustomResourceController.class); + public static final String TEST_ANNOTATION = "TestAnnotation"; + public static final String TEST_ANNOTATION_VALUE = "TestAnnotationValue"; + private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + + @Override + public DeleteControl deleteResource( + DoubleUpdateTestCustomResource resource, Context context) { + return DeleteControl.DEFAULT_DELETE; + } + + @Override + public UpdateControl createOrUpdateResource( + DoubleUpdateTestCustomResource resource, Context context) { + numberOfExecutions.addAndGet(1); + + log.info("Value: " + resource.getSpec().getValue()); + + resource.getMetadata().setAnnotations(new HashMap<>()); + resource.getMetadata().getAnnotations().put(TEST_ANNOTATION, TEST_ANNOTATION_VALUE); + ensureStatusExists(resource); + resource.getStatus().setState(DoubleUpdateTestCustomResourceStatus.State.SUCCESS); + + return UpdateControl.updateCustomResourceAndStatus(resource); + } + + private void ensureStatusExists(DoubleUpdateTestCustomResource resource) { + DoubleUpdateTestCustomResourceStatus status = resource.getStatus(); + if (status == null) { + status = new DoubleUpdateTestCustomResourceStatus(); + resource.setStatus(status); + } + } + + public int getNumberOfExecutions() { + return numberOfExecutions.get(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/doubleupdate/DoubleUpdateTestCustomResourceSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/doubleupdate/DoubleUpdateTestCustomResourceSpec.java new file mode 100644 index 0000000000..ea1428bf51 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/doubleupdate/DoubleUpdateTestCustomResourceSpec.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.doubleupdate; + +public class DoubleUpdateTestCustomResourceSpec { + + private String value; + + public String getValue() { + return value; + } + + public DoubleUpdateTestCustomResourceSpec setValue(String value) { + this.value = value; + return this; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/doubleupdate/DoubleUpdateTestCustomResourceStatus.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/doubleupdate/DoubleUpdateTestCustomResourceStatus.java new file mode 100644 index 0000000000..78245b4058 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/doubleupdate/DoubleUpdateTestCustomResourceStatus.java @@ -0,0 +1,20 @@ +package io.javaoperatorsdk.operator.doubleupdate; + +public class DoubleUpdateTestCustomResourceStatus { + + private State state; + + public State getState() { + return state; + } + + public DoubleUpdateTestCustomResourceStatus setState(State state) { + this.state = state; + return this; + } + + public enum State { + SUCCESS, + ERROR + } +} diff --git a/operator-framework/src/test/resources/io/javaoperatorsdk/operator/doubleupdate-test-crd.yaml b/operator-framework/src/test/resources/io/javaoperatorsdk/operator/doubleupdate-test-crd.yaml new file mode 100644 index 0000000000..afaa0ecab7 --- /dev/null +++ b/operator-framework/src/test/resources/io/javaoperatorsdk/operator/doubleupdate-test-crd.yaml @@ -0,0 +1,16 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: doubleupdatesamples.sample.javaoperatorsdk +spec: + group: sample.javaoperatorsdk + version: v1 + subresources: + status: {} + scope: Namespaced + names: + plural: doubleupdatesamples + singular: doubleupdatesample + kind: DoubleUpdateSample + shortNames: + - du