diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java index e0a05a6b6c..79c149e366 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java @@ -1,7 +1,5 @@ package io.javaoperatorsdk.operator.api; -import java.util.Optional; - import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; @@ -24,6 +22,6 @@ public interface ObservedGenerationAware { void setObservedGeneration(Long generation); - Optional getObservedGeneration(); + Long getObservedGeneration(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAwareStatus.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAwareStatus.java index 843736242b..d2048c9513 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAwareStatus.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAwareStatus.java @@ -1,7 +1,5 @@ package io.javaoperatorsdk.operator.api; -import java.util.Optional; - /** * A helper base class for status sub-resources classes to extend to support generate awareness. */ @@ -15,7 +13,7 @@ public void setObservedGeneration(Long generation) { } @Override - public Optional getObservedGeneration() { - return Optional.ofNullable(observedGeneration); + public Long getObservedGeneration() { + return observedGeneration; } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java index 3646a6df0f..eff9b68055 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java @@ -23,7 +23,7 @@ public static UpdateControl updateResource(T customRe return new UpdateControl<>(customResource, false, true); } - public static UpdateControl updateStatusSubResource( + public static UpdateControl updateStatus( T customResource) { return new UpdateControl<>(customResource, true, false); } @@ -35,7 +35,7 @@ public static UpdateControl updateStatusSubResource( * @param customResource - custom resource to use in both API calls * @return UpdateControl instance */ - public static UpdateControl updateCustomResourceAndStatus( + public static UpdateControl updateResourceAndStatus( T customResource) { return new UpdateControl<>(customResource, true, true); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcher.java index c198252678..5f5882a7b5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcher.java @@ -75,9 +75,9 @@ private PostExecutionControl handleDispatch(ExecutionScope executionScope) Context context = new DefaultContext(executionScope.getRetryInfo()); if (markedForDeletion) { - return handleDelete(resource, context); + return handleCleanup(resource, context); } else { - return handleCreateOrUpdate(executionScope, resource, context); + return handleReconcile(executionScope, resource, context); } } @@ -99,7 +99,7 @@ private boolean shouldNotDispatchToDelete(R resource) { return configuration().useFinalizer() && !resource.hasFinalizer(configuration().getFinalizer()); } - private PostExecutionControl handleCreateOrUpdate( + private PostExecutionControl handleReconcile( ExecutionScope executionScope, R resource, Context context) { if (configuration().useFinalizer() && !resource.hasFinalizer(configuration().getFinalizer())) { /* @@ -114,7 +114,7 @@ private PostExecutionControl handleCreateOrUpdate( try { var resourceForExecution = cloneResourceForErrorStatusHandlerIfNeeded(resource, context); - return createOrUpdateExecution(executionScope, resourceForExecution, context); + return reconcileExecution(executionScope, resourceForExecution, context); } catch (RuntimeException e) { handleLastAttemptErrorStatusHandler(resource, context, e); throw e; @@ -137,7 +137,7 @@ private R cloneResourceForErrorStatusHandlerIfNeeded(R resource, Context context } } - private PostExecutionControl createOrUpdateExecution(ExecutionScope executionScope, + private PostExecutionControl reconcileExecution(ExecutionScope executionScope, R resource, Context context) { log.debug( "Executing createOrUpdate for resource {} with version: {} with execution scope: {}", @@ -222,7 +222,7 @@ private void updatePostExecutionControlWithReschedule( baseControl.getScheduleDelay().ifPresent(postExecutionControl::withReSchedule); } - private PostExecutionControl handleDelete(R resource, Context context) { + private PostExecutionControl handleCleanup(R resource, Context context) { log.debug( "Executing delete for resource: {} with version: {}", getName(resource), diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceEventFilters.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceEventFilters.java index a46a9175e6..b7d69408ec 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceEventFilters.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceEventFilters.java @@ -34,7 +34,7 @@ public final class ResourceEventFilters { var actualGeneration = newResource.getMetadata().getGeneration(); var observedGeneration = ((ObservedGenerationAware) status) .getObservedGeneration(); - return observedGeneration.map(aLong -> actualGeneration > aLong).orElse(true); + return observedGeneration == null || actualGeneration > observedGeneration; } } return oldResource == null || !generationAware || diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcherTest.java index 052ac91a0e..522f6091fb 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcherTest.java @@ -92,7 +92,7 @@ void updatesOnlyStatusSubResourceIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); when(reconciler.reconcile(eq(testCustomResource), any())) - .thenReturn(UpdateControl.updateStatusSubResource(testCustomResource)); + .thenReturn(UpdateControl.updateStatus(testCustomResource)); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); @@ -105,7 +105,7 @@ void updatesBothResourceAndStatusIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); when(reconciler.reconcile(eq(testCustomResource), any())) - .thenReturn(UpdateControl.updateCustomResourceAndStatus(testCustomResource)); + .thenReturn(UpdateControl.updateResourceAndStatus(testCustomResource)); when(customResourceFacade.replaceWithLock(testCustomResource)).thenReturn(testCustomResource); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); @@ -281,7 +281,7 @@ void setReScheduleToPostExecutionControlFromUpdateControl() { when(reconciler.reconcile(eq(testCustomResource), any())) .thenReturn( - UpdateControl.updateStatusSubResource(testCustomResource).rescheduleAfter(1000L)); + UpdateControl.updateStatus(testCustomResource).rescheduleAfter(1000L)); PostExecutionControl control = reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); @@ -315,12 +315,12 @@ void setObservedGenerationForStatusIfNeeded() { when(lConfiguration.isGenerationAware()).thenReturn(true); when(lController.reconcile(eq(observedGenResource), any())) - .thenReturn(UpdateControl.updateStatusSubResource(observedGenResource)); + .thenReturn(UpdateControl.updateStatus(observedGenResource)); when(lFacade.updateStatus(observedGenResource)).thenReturn(observedGenResource); PostExecutionControl control = lDispatcher.handleExecution( executionScopeWithCREvent(observedGenResource)); - assertThat(control.getUpdatedCustomResource().get().getStatus().getObservedGeneration().get()) + assertThat(control.getUpdatedCustomResource().get().getStatus().getObservedGeneration()) .isEqualTo(1L); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java index ad741aa85d..14218b9398 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java @@ -173,7 +173,7 @@ public UpdateControl reconcile( consumer.accept(resource); - return UpdateControl.updateStatusSubResource(resource); + return UpdateControl.updateStatus(resource); } } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ObservedGenerationHandlingIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ObservedGenerationHandlingIT.java new file mode 100644 index 0000000000..9eac198f35 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ObservedGenerationHandlingIT.java @@ -0,0 +1,40 @@ +package io.javaoperatorsdk.operator; + +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; +import io.javaoperatorsdk.operator.junit.OperatorExtension; +import io.javaoperatorsdk.operator.sample.observedgeneration.ObservedGenerationTestCustomResource; +import io.javaoperatorsdk.operator.sample.observedgeneration.ObservedGenerationTestReconciler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +public class ObservedGenerationHandlingIT { + @RegisterExtension + OperatorExtension operator = + OperatorExtension.builder() + .withConfigurationService(DefaultConfigurationService.instance()) + .withReconciler(new ObservedGenerationTestReconciler()) + .build(); + + @Test + public void testReconciliationOfNonCustomResourceAndStatusUpdate() { + var resource = new ObservedGenerationTestCustomResource(); + resource.setMetadata(new ObjectMeta()); + resource.getMetadata().setName("observed-gen1"); + + var createdResource = operator.create(ObservedGenerationTestCustomResource.class, resource); + + await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { + var d = operator.get(ObservedGenerationTestCustomResource.class, + createdResource.getMetadata().getName()); + assertThat(d.getStatus().getObservedGeneration()).isNotNull(); + assertThat(d.getStatus().getObservedGeneration()).isEqualTo(1); + }); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/deployment/DeploymentReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/deployment/DeploymentReconciler.java index a7340465ef..6a9bd352ce 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/deployment/DeploymentReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/deployment/DeploymentReconciler.java @@ -42,7 +42,7 @@ public UpdateControl reconcile( if (condition.isEmpty()) { conditions.add(new DeploymentCondition(null, null, STATUS_MESSAGE, null, "unknown", "DeploymentReconciler")); - return UpdateControl.updateStatusSubResource(resource); + return UpdateControl.updateStatus(resource); } else { return UpdateControl.noUpdate(); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomReconciler.java index 2f65be15a6..bd398d0a1c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomReconciler.java @@ -34,7 +34,7 @@ public UpdateControl reconcile( ensureStatusExists(resource); resource.getStatus().setState(DoubleUpdateTestCustomResourceStatus.State.SUCCESS); - return UpdateControl.updateCustomResourceAndStatus(resource); + return UpdateControl.updateResourceAndStatus(resource); } private void ensureStatusExists(DoubleUpdateTestCustomResource resource) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomReconciler.java index c8a5c673c0..7ae7e75fe3 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomReconciler.java @@ -27,7 +27,7 @@ public UpdateControl reconcile( ensureStatusExists(resource); resource.getStatus().setState(EventSourceTestCustomResourceStatus.State.SUCCESS); - return UpdateControl.updateStatusSubResource(resource).rescheduleAfter(TIMER_PERIOD); + return UpdateControl.updateStatus(resource).rescheduleAfter(TIMER_PERIOD); } private void ensureStatusExists(EventSourceTestCustomResource resource) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java index 322ff6194e..242498575a 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java @@ -55,7 +55,7 @@ public UpdateControl reconcile( LOGGER.debug("Setting target status for CR: {}", targetStatus); resource.setStatus(new InformerEventSourceTestCustomResourceStatus()); resource.getStatus().setConfigMapValue(targetStatus); - return UpdateControl.updateStatusSubResource(resource); + return UpdateControl.updateStatus(resource); } @Override diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestCustomResource.java new file mode 100644 index 0000000000..51e4a1113a --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestCustomResource.java @@ -0,0 +1,23 @@ +package io.javaoperatorsdk.operator.sample.observedgeneration; + +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.Kind; +import io.fabric8.kubernetes.model.annotation.ShortNames; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("sample.javaoperatorsdk") +@Version("v1") +@Kind("ObservedGenerationTestCustomResource") +@ShortNames("og") +public class ObservedGenerationTestCustomResource + extends CustomResource + implements Namespaced { + + @Override + protected ObservedGenerationTestCustomResourceStatus initStatus() { + return new ObservedGenerationTestCustomResourceStatus(); + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestCustomResourceStatus.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestCustomResourceStatus.java new file mode 100644 index 0000000000..14071775f3 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestCustomResourceStatus.java @@ -0,0 +1,7 @@ +package io.javaoperatorsdk.operator.sample.observedgeneration; + +import io.javaoperatorsdk.operator.api.ObservedGenerationAwareStatus; + +public class ObservedGenerationTestCustomResourceStatus extends ObservedGenerationAwareStatus { + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestReconciler.java new file mode 100644 index 0000000000..01a1705fe4 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestReconciler.java @@ -0,0 +1,23 @@ +package io.javaoperatorsdk.operator.sample.observedgeneration; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.javaoperatorsdk.operator.api.reconciler.*; + +import static io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration.NO_FINALIZER; + +@ControllerConfiguration(finalizerName = NO_FINALIZER) +public class ObservedGenerationTestReconciler + implements Reconciler { + + private static final Logger log = LoggerFactory.getLogger(ObservedGenerationTestReconciler.class); + + @Override + public UpdateControl reconcile( + ObservedGenerationTestCustomResource resource, Context context) { + log.info("Reconcile ObservedGenerationTestCustomResource: {}", + resource.getMetadata().getName()); + return UpdateControl.updateStatus(resource); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java index 5ae059342e..b2a7ca0e17 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java @@ -46,7 +46,7 @@ public UpdateControl reconcile( ensureStatusExists(resource); resource.getStatus().setState(RetryTestCustomResourceStatus.State.SUCCESS); - return UpdateControl.updateStatusSubResource(resource); + return UpdateControl.updateStatus(resource); } private void ensureStatusExists(RetryTestCustomResource resource) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java index 30f57fd8c6..06efcf8cdd 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java @@ -128,7 +128,7 @@ public UpdateControl reconcile( } resource.getStatus().setConfigMapStatus("ConfigMap Ready"); } - return UpdateControl.updateStatusSubResource(resource); + return UpdateControl.updateStatus(resource); } private Map configMapData(TestCustomResource resource) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java index 9eefc7568d..c9e68e0d69 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java @@ -36,7 +36,7 @@ public UpdateControl reconcile( ensureStatusExists(resource); resource.getStatus().setState(SubResourceTestCustomResourceStatus.State.SUCCESS); - return UpdateControl.updateStatusSubResource(resource); + return UpdateControl.updateStatus(resource); } private void ensureStatusExists(SubResourceTestCustomResource resource) { diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml index 289a2889c3..5d9a497cec 100644 --- a/sample-operators/pom.xml +++ b/sample-operators/pom.xml @@ -20,59 +20,6 @@ tomcat-operator + webpage - - - - io.javaoperatorsdk - operator-framework - 1.9.2 - - - org.apache.logging.log4j - log4j-slf4j-impl - 2.13.3 - - - org.takes - takes - 1.19 - - - junit - junit - 4.13.1 - test - - - org.awaitility - awaitility - 4.1.0 - test - - - - - - - com.google.cloud.tools - jib-maven-plugin - ${jib-maven-plugin.version} - - - gcr.io/distroless/java:11 - - - tomcat-operator - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.8.1 - - - - \ No newline at end of file diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java index 62afafb365..65e4030118 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java @@ -75,7 +75,7 @@ public UpdateControl reconcile(Tomcat tomcat, Context context) { tomcat.getMetadata().getName(), tomcat.getMetadata().getNamespace(), tomcat.getStatus().getReadyReplicas()); - return UpdateControl.updateStatusSubResource(updatedTomcat); + return UpdateControl.updateStatus(updatedTomcat); } return UpdateControl.noUpdate(); } diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java index 017a4a79b1..9b9b566061 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java @@ -98,7 +98,7 @@ public UpdateControl reconcile(Webapp webapp, Context context) { } webapp.getStatus().setDeployedArtifact(webapp.getSpec().getUrl()); webapp.getStatus().setDeploymentStatus(commandStatusInAllPods); - return UpdateControl.updateStatusSubResource(webapp); + return UpdateControl.updateStatus(webapp); } else { log.info("WebappController invoked but Tomcat not ready yet ({}/{})", tomcat.getStatus() != null ? tomcat.getStatus().getReadyReplicas() : 0, diff --git a/sample-operators/webpage/README.md b/sample-operators/webpage/README.md new file mode 100644 index 0000000000..ba17b7d962 --- /dev/null +++ b/sample-operators/webpage/README.md @@ -0,0 +1,53 @@ +# WebServer Operator + +This is a simple example of how a Custom Resource backed by an Operator can serve as +an abstraction layer. This Operator will use a webserver resource, which mainly contains a +static webpage definition and creates an NGINX Deployment backed by a ConfigMap which holds +the HTML. + +This is an example input: +```yaml +apiVersion: "sample.javaoperatorsdk/v1" +kind: WebPage +metadata: + name: mynginx-hello +spec: + html: | + + + Webserver Operator + + + Hello World! + + +``` + +### Try + +The quickest way to try the operator is to run it on your local machine, while it connects to a local or remote +Kubernetes cluster. When you start it, it will use the current kubectl context on your machine to connect to the cluster. + +Before you run it you have to install the CRD on your cluster by running `kubectl apply -f k8s/crd.yaml` + +When the Operator is running you can create some Webserver Custom Resources. You can find a sample custom resource in +`k8s/webpage.yaml`. You can create it by running `kubectl apply -f k8s/webpage.yaml` + +After the Operator has picked up the new webserver resource (see the logs) it should create the NGINX server in the +same namespace where the webserver resource is created. To connect to the server using your browser you can +run `kubectl get service` and view the service created by the Operator. It should have a NodePort configured. If you are +running a single-node cluster (e.g. Docker for Mac or Minikube) you can connect to the VM on this port to access the +page. Otherwise you can change the service to a LoadBalancer (e.g on a public cloud). + +You can also try to change the HTML code in `k8s/webpage.yaml` and do another `kubectl apply -f k8s/webpage.yaml`. +This should update the actual NGINX deployment with the new configuration. + +### Build + +You can build the sample using `mvn jib:dockerBuild` this will produce a Docker image you can push to the registry +of your choice. The JAR file is built using your local Maven and JDK and then copied into the Docker image. + +### Deployment + +1. Deploy the CRD: `kubectl apply -f k8s/crd.yaml` +2. Deploy the operator: `kubectl apply -f k8s/operator.yaml` diff --git a/sample-operators/webpage/k8s/operator.yaml b/sample-operators/webpage/k8s/operator.yaml new file mode 100644 index 0000000000..926b2c31e2 --- /dev/null +++ b/sample-operators/webpage/k8s/operator.yaml @@ -0,0 +1,98 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: webserver-operator + +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: webserver-operator + namespace: webserver-operator + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: webserver-operator + namespace: webserver-operator +spec: + selector: + matchLabels: + app: webserver-operator + replicas: 1 + template: + metadata: + labels: + app: webserver-operator + spec: + serviceAccountName: webserver-operator + containers: + - name: operator + image: webserver-operator + imagePullPolicy: Never + ports: + - containerPort: 80 + readinessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 1 + timeoutSeconds: 1 + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 30 + timeoutSeconds: 1 + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: operator-admin +subjects: +- kind: ServiceAccount + name: webserver-operator + namespace: webserver-operator +roleRef: + kind: ClusterRole + name: webserver-operator + apiGroup: "" + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: webserver-operator +rules: +- apiGroups: + - "" + resources: + - deployments + - services + - configmaps + - pods + verbs: + - '*' +- apiGroups: + - "apps" + resources: + - deployments + - services + - configmaps + verbs: + - '*' +- apiGroups: + - "apiextensions.k8s.io" + resources: + - customresourcedefinitions + verbs: + - '*' +- apiGroups: + - "sample.javaoperatorsdk" + resources: + - webservers + - webservers/status + verbs: + - '*' diff --git a/sample-operators/webpage/k8s/webpage.yaml b/sample-operators/webpage/k8s/webpage.yaml new file mode 100644 index 0000000000..382da972cb --- /dev/null +++ b/sample-operators/webpage/k8s/webpage.yaml @@ -0,0 +1,14 @@ +apiVersion: "sample.javaoperatorsdk/v1" +kind: WebPage +metadata: + name: hellows +spec: + html: | + + + Hello Operator World + + + Hello World! + + diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml new file mode 100644 index 0000000000..ed4bcafc0a --- /dev/null +++ b/sample-operators/webpage/pom.xml @@ -0,0 +1,68 @@ + + + 4.0.0 + + + io.javaoperatorsdk + sample-operators + 2.0.0-SNAPSHOT + + + webserver + Operator SDK - Samples - Webserver + Provisions an nginx Webserver based on a CRD + jar + + + 11 + 11 + 2.7.1 + + + + + io.javaoperatorsdk + operator-framework + 2.0.0-SNAPSHOT + + + org.apache.logging.log4j + log4j-slf4j-impl + + + org.takes + takes + 1.19 + + + io.fabric8 + crd-generator-apt + provided + + + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + gcr.io/distroless/java:11 + + + webserver-operator + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + + + + \ No newline at end of file diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ErrorSimulationException.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ErrorSimulationException.java new file mode 100644 index 0000000000..e2d3f3c1dd --- /dev/null +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ErrorSimulationException.java @@ -0,0 +1,8 @@ +package io.javaoperatorsdk.operator.sample; + +public class ErrorSimulationException extends RuntimeException { + + public ErrorSimulationException(String message) { + super(message); + } +} diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPage.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPage.java new file mode 100644 index 0000000000..6c10ffe3af --- /dev/null +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPage.java @@ -0,0 +1,17 @@ +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("sample.javaoperatorsdk") +@Version("v1") +public class WebPage extends CustomResource + implements Namespaced { + + @Override + protected WebPageStatus initStatus() { + return new WebPageStatus(); + } +} diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java new file mode 100644 index 0000000000..4940119ba9 --- /dev/null +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.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 WebPageOperator { + + private static final Logger log = LoggerFactory.getLogger(WebPageOperator.class); + + public static void main(String[] args) throws IOException { + log.info("WebServer Operator starting!"); + + Config config = new ConfigBuilder().withNamespace(null).build(); + KubernetesClient client = new DefaultKubernetesClient(config); + Operator operator = new Operator(client, DefaultConfigurationService.instance()); + operator.register(new WebPageReconciler(client)); + operator.start(); + + new FtBasic(new TkFork(new FkRegex("/health", "ALL GOOD!")), 8080).start(Exit.NEVER); + } +} diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java new file mode 100644 index 0000000000..37caf1503c --- /dev/null +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -0,0 +1,180 @@ +package io.javaoperatorsdk.operator.sample; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.*; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.kubernetes.client.dsl.RollableScalableResource; +import io.fabric8.kubernetes.client.dsl.ServiceResource; +import io.fabric8.kubernetes.client.utils.Serialization; +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.Context; + +@ControllerConfiguration +public class WebPageReconciler implements Reconciler, ErrorStatusHandler { + + private final Logger log = LoggerFactory.getLogger(getClass()); + + private final KubernetesClient kubernetesClient; + + public WebPageReconciler(KubernetesClient kubernetesClient) { + this.kubernetesClient = kubernetesClient; + } + + @Override + public UpdateControl reconcile(WebPage webPage, Context context) { + if (webPage.getSpec().getHtml().contains("error")) { + throw new ErrorSimulationException("Simulating error"); + } + + String ns = webPage.getMetadata().getNamespace(); + + Map data = new HashMap<>(); + data.put("index.html", webPage.getSpec().getHtml()); + + ConfigMap htmlConfigMap = + new ConfigMapBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName(configMapName(webPage)) + .withNamespace(ns) + .build()) + .withData(data) + .build(); + + Deployment deployment = loadYaml(Deployment.class, "deployment.yaml"); + deployment.getMetadata().setName(deploymentName(webPage)); + deployment.getMetadata().setNamespace(ns); + deployment.getSpec().getSelector().getMatchLabels().put("app", deploymentName(webPage)); + deployment + .getSpec() + .getTemplate() + .getMetadata() + .getLabels() + .put("app", deploymentName(webPage)); + deployment + .getSpec() + .getTemplate() + .getSpec() + .getVolumes() + .get(0) + .setConfigMap( + new ConfigMapVolumeSourceBuilder().withName(configMapName(webPage)).build()); + + Service service = loadYaml(Service.class, "service.yaml"); + service.getMetadata().setName(serviceName(webPage)); + service.getMetadata().setNamespace(ns); + service.getSpec().setSelector(deployment.getSpec().getTemplate().getMetadata().getLabels()); + + ConfigMap existingConfigMap = + kubernetesClient + .configMaps() + .inNamespace(htmlConfigMap.getMetadata().getNamespace()) + .withName(htmlConfigMap.getMetadata().getName()) + .get(); + + log.info("Creating or updating ConfigMap {} in {}", htmlConfigMap.getMetadata().getName(), ns); + kubernetesClient.configMaps().inNamespace(ns).createOrReplace(htmlConfigMap); + log.info("Creating or updating Deployment {} in {}", deployment.getMetadata().getName(), ns); + kubernetesClient.apps().deployments().inNamespace(ns).createOrReplace(deployment); + + if (kubernetesClient.services().inNamespace(ns).withName(service.getMetadata().getName()) + .get() == null) { + log.info("Creating Service {} in {}", service.getMetadata().getName(), ns); + kubernetesClient.services().inNamespace(ns).createOrReplace(service); + } + + if (existingConfigMap != null) { + if (!StringUtils.equals( + existingConfigMap.getData().get("index.html"), + htmlConfigMap.getData().get("index.html"))) { + log.info("Restarting pods because HTML has changed in {}", ns); + kubernetesClient + .pods() + .inNamespace(ns) + .withLabel("app", deploymentName(webPage)) + .delete(); + } + } + + WebPageStatus status = new WebPageStatus(); + status.setHtmlConfigMap(htmlConfigMap.getMetadata().getName()); + status.setAreWeGood("Yes!"); + status.setErrorMessage(null); + webPage.setStatus(status); + + return UpdateControl.updateStatus(webPage); + } + + @Override + public DeleteControl cleanup(WebPage nginx, Context context) { + log.info("Execution deleteResource for: {}", nginx.getMetadata().getName()); + + log.info("Deleting ConfigMap {}", configMapName(nginx)); + Resource configMap = + kubernetesClient + .configMaps() + .inNamespace(nginx.getMetadata().getNamespace()) + .withName(configMapName(nginx)); + if (configMap.get() != null) { + configMap.delete(); + } + + log.info("Deleting Deployment {}", deploymentName(nginx)); + RollableScalableResource deployment = + kubernetesClient + .apps() + .deployments() + .inNamespace(nginx.getMetadata().getNamespace()) + .withName(deploymentName(nginx)); + if (deployment.get() != null) { + deployment.cascading(true).delete(); + } + + log.info("Deleting Service {}", serviceName(nginx)); + ServiceResource service = + kubernetesClient + .services() + .inNamespace(nginx.getMetadata().getNamespace()) + .withName(serviceName(nginx)); + if (service.get() != null) { + service.delete(); + } + return DeleteControl.defaultDelete(); + } + + private static String configMapName(WebPage nginx) { + return nginx.getMetadata().getName() + "-html"; + } + + private static String deploymentName(WebPage nginx) { + return nginx.getMetadata().getName(); + } + + private static String serviceName(WebPage nginx) { + return nginx.getMetadata().getName(); + } + + private T loadYaml(Class clazz, String yaml) { + try (InputStream is = getClass().getResourceAsStream(yaml)) { + return Serialization.unmarshal(is, clazz); + } catch (IOException ex) { + throw new IllegalStateException("Cannot find yaml on classpath: " + yaml); + } + } + + @Override + public WebPage updateErrorStatus(WebPage resource, RuntimeException e) { + resource.getStatus().setErrorMessage("Error: " + e.getMessage()); + return resource; + } +} diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageSpec.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageSpec.java new file mode 100644 index 0000000000..db50be6c2d --- /dev/null +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageSpec.java @@ -0,0 +1,14 @@ +package io.javaoperatorsdk.operator.sample; + +public class WebPageSpec { + + private String html; + + public String getHtml() { + return html; + } + + public void setHtml(String html) { + this.html = html; + } +} diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java new file mode 100644 index 0000000000..2b2e2a23c8 --- /dev/null +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java @@ -0,0 +1,37 @@ +package io.javaoperatorsdk.operator.sample; + +import io.javaoperatorsdk.operator.api.ObservedGenerationAwareStatus; + +public class WebPageStatus extends ObservedGenerationAwareStatus { + + private String htmlConfigMap; + + private String areWeGood; + + private String errorMessage; + + public String getHtmlConfigMap() { + return htmlConfigMap; + } + + public void setHtmlConfigMap(String htmlConfigMap) { + this.htmlConfigMap = htmlConfigMap; + } + + public String getAreWeGood() { + return areWeGood; + } + + public void setAreWeGood(String areWeGood) { + this.areWeGood = areWeGood; + } + + public String getErrorMessage() { + return errorMessage; + } + + public WebPageStatus setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + return this; + } +} diff --git a/sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/deployment.yaml b/sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/deployment.yaml new file mode 100644 index 0000000000..f2d10d4325 --- /dev/null +++ b/sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/deployment.yaml @@ -0,0 +1,26 @@ +apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2 +kind: Deployment +metadata: + name: "" +spec: + selector: + matchLabels: + app: "" + replicas: 1 + template: + metadata: + labels: + app: "" + spec: + containers: + - name: nginx + image: nginx:1.17.0 + ports: + - containerPort: 80 + volumeMounts: + - name: html-volume + mountPath: /usr/share/nginx/html + volumes: + - name: html-volume + configMap: + name: "" \ No newline at end of file diff --git a/sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/html-configmap.yaml b/sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/html-configmap.yaml new file mode 100644 index 0000000000..8314c5b927 --- /dev/null +++ b/sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/html-configmap.yaml @@ -0,0 +1,6 @@ +kind: ConfigMap +apiVersion: v1 +metadata: + name: "" +data: + html: "" \ No newline at end of file diff --git a/sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/ingress.yaml b/sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/ingress.yaml new file mode 100644 index 0000000000..6fd1ed93e9 --- /dev/null +++ b/sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/ingress.yaml @@ -0,0 +1,19 @@ +apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2 +kind: Deployment +metadata: + name: +spec: + selector: + matchLabels: + app: + replicas: 1 + template: + metadata: + labels: + app: + spec: + containers: + - name: nginx + image: nginx:1.7.9 + ports: + - containerPort: 80 \ No newline at end of file diff --git a/sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/service.yaml b/sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/service.yaml new file mode 100644 index 0000000000..578fcd6d1e --- /dev/null +++ b/sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: "" +spec: + selector: + app: "" + ports: + - protocol: TCP + port: 80 + targetPort: 80 + type: NodePort \ No newline at end of file diff --git a/sample-operators/webpage/src/main/resources/log4j2.xml b/sample-operators/webpage/src/main/resources/log4j2.xml new file mode 100644 index 0000000000..5b794e7de3 --- /dev/null +++ b/sample-operators/webpage/src/main/resources/log4j2.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file