From d29e50b2844d14e6f0117a74076d3074a2919c26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81d=C3=A1m=20S=C3=A1ndor?= <10675791+adam-sandor@users.noreply.github.com> Date: Fri, 12 Nov 2021 14:08:18 +0100 Subject: [PATCH] feat: Add tomcat operator sample (#659) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixed informer bug and related IT as part of this commit Co-authored-by: Chris Laprun Co-authored-by: Attila Mészáros Co-authored-by: Adam Sándor --- .github/workflows/end-to-end-tests.yml | 77 +++++++ .../internal/CustomResourceEventSource.java | 9 +- .../event/internal/InformerEventSource.java | 8 +- .../operator/InformerEventSourceIT.java | 8 +- ...formerEventSourceTestCustomReconciler.java | 4 +- pom.xml | 29 ++- sample-operators/pom.xml | 78 +++++++ sample-operators/tomcat-operator/README.md | 76 +++++++ .../tomcat-operator/k8s/operator.yaml | 91 ++++++++ .../tomcat-operator/k8s/tomcat-sample1.yaml | 7 + .../tomcat-operator/k8s/tomcat-sample2.yaml | 7 + .../tomcat-operator/k8s/webapp-sample1.yaml | 8 + .../tomcat-operator/k8s/webapp-sample2.yaml | 8 + sample-operators/tomcat-operator/pom.xml | 82 +++++++ .../operator/sample/Tomcat.java | 20 ++ .../operator/sample/TomcatOperator.java | 34 +++ .../operator/sample/TomcatReconciler.java | 201 +++++++++++++++++ .../operator/sample/TomcatSpec.java | 23 ++ .../operator/sample/TomcatStatus.java | 14 ++ .../operator/sample/Webapp.java | 21 ++ .../operator/sample/WebappReconciler.java | 208 ++++++++++++++++++ .../operator/sample/WebappSpec.java | 34 +++ .../operator/sample/WebappStatus.java | 24 ++ .../operator/sample/deployment.yaml | 39 ++++ .../operator/sample/service.yaml | 12 + .../src/main/resources/log4j2.xml | 13 ++ .../operator/sample/TomcatOperatorE2E.java | 136 ++++++++++++ .../src/test/resources/log4j2.xml | 13 ++ samples/README.md | 4 - smoke-test-samples/README.md | 4 + .../common/crd/test_object.yaml | 0 .../common/pom.xml | 6 +- .../operator/sample/CustomService.java | 0 .../sample/CustomServiceReconciler.java | 0 .../operator/sample/ServiceSpec.java | 0 .../common/src/main/resources/log4j2.xml | 0 {samples => smoke-test-samples}/pom.xml | 6 +- .../pure-java/pom.xml | 8 +- .../sample/PureJavaApplicationRunner.java | 0 .../spring-boot-plain/pom.xml | 8 +- .../operator/sample/Config.java | 0 .../SpringBootStarterSampleApplication.java | 0 .../src/main/resources/application.yaml | 0 43 files changed, 1288 insertions(+), 32 deletions(-) create mode 100644 .github/workflows/end-to-end-tests.yml create mode 100644 sample-operators/pom.xml create mode 100644 sample-operators/tomcat-operator/README.md create mode 100644 sample-operators/tomcat-operator/k8s/operator.yaml create mode 100644 sample-operators/tomcat-operator/k8s/tomcat-sample1.yaml create mode 100644 sample-operators/tomcat-operator/k8s/tomcat-sample2.yaml create mode 100644 sample-operators/tomcat-operator/k8s/webapp-sample1.yaml create mode 100644 sample-operators/tomcat-operator/k8s/webapp-sample2.yaml create mode 100644 sample-operators/tomcat-operator/pom.xml create mode 100644 sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/Tomcat.java create mode 100644 sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java create mode 100644 sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java create mode 100644 sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatSpec.java create mode 100644 sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatStatus.java create mode 100644 sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/Webapp.java create mode 100644 sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java create mode 100644 sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappSpec.java create mode 100644 sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappStatus.java create mode 100644 sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/deployment.yaml create mode 100644 sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/service.yaml create mode 100644 sample-operators/tomcat-operator/src/main/resources/log4j2.xml create mode 100644 sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java create mode 100644 sample-operators/tomcat-operator/src/test/resources/log4j2.xml delete mode 100644 samples/README.md create mode 100644 smoke-test-samples/README.md rename {samples => smoke-test-samples}/common/crd/test_object.yaml (100%) rename {samples => smoke-test-samples}/common/pom.xml (85%) rename {samples => smoke-test-samples}/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomService.java (100%) rename {samples => smoke-test-samples}/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java (100%) rename {samples => smoke-test-samples}/common/src/main/java/io/javaoperatorsdk/operator/sample/ServiceSpec.java (100%) rename {samples => smoke-test-samples}/common/src/main/resources/log4j2.xml (100%) rename {samples => smoke-test-samples}/pom.xml (85%) rename {samples => smoke-test-samples}/pure-java/pom.xml (70%) rename {samples => smoke-test-samples}/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java (100%) rename {samples => smoke-test-samples}/spring-boot-plain/pom.xml (88%) rename {samples => smoke-test-samples}/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/Config.java (100%) rename {samples => smoke-test-samples}/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/SpringBootStarterSampleApplication.java (100%) rename {samples => smoke-test-samples}/spring-boot-plain/src/main/resources/application.yaml (100%) diff --git a/.github/workflows/end-to-end-tests.yml b/.github/workflows/end-to-end-tests.yml new file mode 100644 index 0000000000..404be8c102 --- /dev/null +++ b/.github/workflows/end-to-end-tests.yml @@ -0,0 +1,77 @@ +# End to end integration test which deploys the Tomcat operator to a Kubernetes +# (Kind) cluster and creates custom resources to verify the operator's functionality +name: TomcatOperator End to End test +on: + push: + branches: + - "*" +jobs: + tomcat_e2e_test: + runs-on: ubuntu-latest + env: + KIND_CL_NAME: e2e-test + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: clean resident local docker + if: ${{ env.ACT }} + continue-on-error: true + run: | + for DIMG in "$KIND_CL_NAME-control-plane "; do + docker stop $DIMG ; docker rm $DIMG ; + done ; + sleep 1 + + - name: Create Kubernetes KinD Cluster + uses: container-tools/kind-action@v1.5.0 + with: + cluster_name: e2e-test + registry: false + + - name: Set up Java and Maven + uses: actions/setup-java@v2 + with: + java-version: 11 + distribution: adopt-hotspot + cache: 'maven' + + - name: Build SDK + run: mvn install -DskipTests + + - name: build jib + working-directory: sample-operators/tomcat-operator + run: | + mvn --version + mvn -B package jib:dockerBuild jib:buildTar -Djib-maven-image=tomcat-operator -DskipTests + kind load image-archive target/jib-image.tar --name=${{ env.KIND_CL_NAME }} + + - name: Apply CRDs + working-directory: sample-operators/tomcat-operator + run: | + kubectl apply -f target/classes/META-INF/fabric8/tomcats.tomcatoperator.io-v1.yml + kubectl apply -f target/classes/META-INF/fabric8/webapps.tomcatoperator.io-v1.yml + + - name: Deploy Tomcat Operator + working-directory: sample-operators/tomcat-operator + run: | + kubectl apply -f k8s/operator.yaml + + - name: Run E2E Tests + working-directory: sample-operators/tomcat-operator + run: mvn -B test -P end-to-end-tests + + - name: Dump state + if: ${{ failure() }} + run: | + set +e + echo "All namespaces" + kubectl get ns + echo "All objects in tomcat-operator" + kubectl get all -n tomcat-operator -o yaml + echo "Output of tomcat-operator pod" + kubectl logs -l app=tomcat-operator -n tomcat-operator + echo "All objects in tomcat-test" + kubectl get deployment,pod,tomcat,webapp -n tomcat-test -o yaml + echo "Output of curl command" + kubectl logs curl -n tomcat-test diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java index 578c6894cb..67998c7584 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java @@ -161,8 +161,11 @@ public void onDelete(T resource, boolean b) { @Override public Optional getCustomResource(CustomResourceID resourceID) { - var sharedIndexInformer = - sharedIndexInformers.get(resourceID.getNamespace().orElse(ANY_NAMESPACE_MAP_KEY)); + var sharedIndexInformer = sharedIndexInformers.get(ANY_NAMESPACE_MAP_KEY); + if (sharedIndexInformer == null) { + sharedIndexInformer = + sharedIndexInformers.get(resourceID.getNamespace().orElse(ANY_NAMESPACE_MAP_KEY)); + } var resource = sharedIndexInformer.getStore() .getByKey(Cache.namespaceKeyFunc(resourceID.getNamespace().orElse(null), resourceID.getName())); @@ -173,6 +176,8 @@ public Optional getCustomResource(CustomResourceID resourceID) { } } + + /** * @return shared informers by namespace. If custom resource is not namespace scoped use * CustomResourceEventSource.ANY_NAMESPACE_MAP_KEY diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java index 64b4a8d753..9b61b18ade 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java @@ -86,12 +86,12 @@ public void onDelete(T t, boolean b) { } private void propagateEvent(T object) { - var uids = resourceToCustomResourceIDSet.apply(object); - if (uids.isEmpty()) { + var customResourceIDSet = resourceToCustomResourceIDSet.apply(object); + if (customResourceIDSet.isEmpty()) { return; } - uids.forEach(uid -> { - Event event = new Event(CustomResourceID.fromResource(object)); + customResourceIDSet.forEach(customResourceId -> { + Event event = new Event(customResourceId); /* * In fabric8 client for certain cases informers can be created on in a way that they are * automatically started, what would cause a NullPointerException here, since an event might diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java index d290d06ecd..06a1238bb5 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java @@ -13,7 +13,7 @@ import io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomReconciler; import io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomResource; -import static io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomReconciler.RELATED_RESOURCE_UID; +import static io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomReconciler.RELATED_RESOURCE_NAME; import static io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomReconciler.TARGET_CONFIG_MAP_KEY; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; @@ -36,11 +36,11 @@ public void testUsingInformerToWatchChangesOfConfigMap() { var customResource = initialCustomResource(); customResource = operator.create(InformerEventSourceTestCustomResource.class, customResource); ConfigMap configMap = - operator.create(ConfigMap.class, relatedConfigMap(customResource.getMetadata().getUid())); + operator.create(ConfigMap.class, relatedConfigMap(customResource.getMetadata().getName())); waitForCRStatusValue(INITIAL_STATUS_MESSAGE); configMap.getData().put(TARGET_CONFIG_MAP_KEY, UPDATE_STATUS_MESSAGE); - operator.replace(ConfigMap.class, configMap); + configMap = operator.replace(ConfigMap.class, configMap); waitForCRStatusValue(UPDATE_STATUS_MESSAGE); } @@ -51,7 +51,7 @@ private ConfigMap relatedConfigMap(String relatedResourceAnnotation) { ObjectMeta objectMeta = new ObjectMeta(); objectMeta.setName(RESOURCE_NAME); objectMeta.setAnnotations(new HashMap<>()); - objectMeta.getAnnotations().put(RELATED_RESOURCE_UID, relatedResourceAnnotation); + objectMeta.getAnnotations().put(RELATED_RESOURCE_NAME, relatedResourceAnnotation); configMap.setMetadata(objectMeta); configMap.setData(new HashMap<>()); 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 a0b7805ccf..322ff6194e 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 @@ -29,7 +29,7 @@ public class InformerEventSourceTestCustomReconciler implements private static final Logger LOGGER = LoggerFactory.getLogger(InformerEventSourceTestCustomReconciler.class); - public static final String RELATED_RESOURCE_UID = "relatedResourceName"; + public static final String RELATED_RESOURCE_NAME = "relatedResourceName"; public static final String TARGET_CONFIG_MAP_KEY = "targetStatus"; private KubernetesClient kubernetesClient; @@ -38,7 +38,7 @@ public class InformerEventSourceTestCustomReconciler implements @Override public void prepareEventSources(EventSourceRegistry eventSourceRegistry) { eventSource = new InformerEventSource<>(kubernetesClient, ConfigMap.class, - Mappers.fromAnnotation(RELATED_RESOURCE_UID)); + Mappers.fromAnnotation(RELATED_RESOURCE_NAME)); eventSourceRegistry.registerEventSource(eventSource); } diff --git a/pom.xml b/pom.xml index b788e40919..c11942d447 100644 --- a/pom.xml +++ b/pom.xml @@ -76,8 +76,9 @@ operator-framework-core operator-framework-junit5 operator-framework - samples - micrometer-support + smoke-test-samples + micrometer-support + sample-operators @@ -271,6 +272,7 @@ **/*IT.java + **/*E2E.java @@ -316,6 +318,7 @@ **/*Test.java **/*IT.java + **/*E2E.java @@ -335,6 +338,27 @@ **/*Test.java + **/*E2E.java + + + + + + + + end-to-end-tests + + + + org.apache.maven.plugins + maven-surefire-plugin + + + **/*E2E.java + + + **/*Test.java + **/*IT.java @@ -352,6 +376,7 @@ **/*IT.java + **/*E2E.java diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml new file mode 100644 index 0000000000..289a2889c3 --- /dev/null +++ b/sample-operators/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + + + io.javaoperatorsdk + java-operator-sdk + 2.0.0-SNAPSHOT + + + sample-operators + Operator SDK - Samples + pom + + + 3.1.4 + + + + tomcat-operator + + + + + 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/README.md b/sample-operators/tomcat-operator/README.md new file mode 100644 index 0000000000..c16335ae80 --- /dev/null +++ b/sample-operators/tomcat-operator/README.md @@ -0,0 +1,76 @@ +# Tomcat Operator + +Creates a Tomcat deployment from a Custom Resource, while keeping the WAR separated with another Custom Resource. + +This sample demonstrates the following capabilities of the Java Operator SDK: +* Multiple Controllers in a single Operator. The Tomcat resource is managed by the TomcatController while the Webapp +resource is managed by the WebappController. +* Reacting to events about resources created by the controller. The TomcatController will receive events about the +Deployment resources it created. See EventSource section below for more detail. + +## Example input for creating a Tomcat instance +``` +apiVersion: "tomcatoperator.io/v1" +kind: Tomcat +metadata: + name: test-tomcat1 +spec: + version: 9.0 + replicas: 2 +``` + +## Example input for the Webapp +``` +apiVersion: "tomcatoperator.io/v1" +kind: Webapp +metadata: + name: sample-webapp1 +spec: + tomcat: test-tomcat1 + url: http://tomcat.apache.org/tomcat-7.0-doc/appdev/sample/sample.war + contextPath: mysample +``` + +## Getting started / Testing + +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 CRDs on your cluster by running: +- `kubectl apply -f target/classes/META-INF/fabric8/tomcats.tomcatoperator.io-v1.yml` +- `kubectl apply -f target/classes/META-INF/fabric8/webapps.tomcatoperator.io-v1.yml` + +The CRDs are generated automatically from your code by simply adding the `crd-generator-apt` +dependency to your `pom.xml` file. + +When the Operator is running you can create some Tomcat Custom Resources. You can find a sample +custom resources in the k8s folder. + +If you want the Operator to be running as a deployment in your cluster, follow the below steps. + +## Build + +You can build the sample using `mvn install 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. + +## Install Operator into cluster + +Install the CRDs as shown above if you haven't already, then +run `kubectl apply -f k8s/operator.yaml`. Now you can create Tomcat instances with CRs (see examples +above). + +## EventSources +The TomcatController is listening to events about Deployments created by the TomcatOperator by registering a +InformerEventSource with the EventSourceManager. The InformerEventSource will in turn register a watch on +all Deployments managed by the Controller (identified by the `app.kubernetes.io/managed-by` label). +When an event from a Deployment is received we have to identify which Tomcat object does the Deployment +belong to. This is done when the InformerEventSource creates the event. + +The TomcatController has to take care of setting the `app.kubernetes.io/managed-by` label on the Deployment so the +InformerEventSource can watch the right Deployments. +The TomcatController also has to set `ownerReference` on the Deployment so later the InformerEventSource can +identify which Tomcat does the Deployment belong to. This is necessary so the frameowork can call the Controller +`createOrUpdate` method correctly. + diff --git a/sample-operators/tomcat-operator/k8s/operator.yaml b/sample-operators/tomcat-operator/k8s/operator.yaml new file mode 100644 index 0000000000..a88b6514e0 --- /dev/null +++ b/sample-operators/tomcat-operator/k8s/operator.yaml @@ -0,0 +1,91 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: tomcat-operator + +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: tomcat-operator + namespace: tomcat-operator + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: tomcat-operator + namespace: tomcat-operator +spec: + selector: + matchLabels: + app: tomcat-operator + template: + metadata: + labels: + app: tomcat-operator + spec: + serviceAccountName: tomcat-operator + containers: + - name: operator + image: tomcat-operator + imagePullPolicy: IfNotPresent + ports: + - containerPort: 80 + readinessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 1 + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 30 + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: tomcat-operator-admin +subjects: +- kind: ServiceAccount + name: tomcat-operator + namespace: tomcat-operator +roleRef: + kind: ClusterRole + name: tomcat-operator + apiGroup: "" + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: tomcat-operator +rules: +- apiGroups: + - "" + - "extensions" + - "apps" + resources: + - deployments + - services + - pods + - pods/exec + verbs: + - '*' +- apiGroups: + - "apiextensions.k8s.io" + resources: + - customresourcedefinitions + verbs: + - '*' +- apiGroups: + - "tomcatoperator.io" + resources: + - tomcats + - tomcats/status + - webapps + - webapps/status + verbs: + - '*' \ No newline at end of file diff --git a/sample-operators/tomcat-operator/k8s/tomcat-sample1.yaml b/sample-operators/tomcat-operator/k8s/tomcat-sample1.yaml new file mode 100644 index 0000000000..ddd30663cd --- /dev/null +++ b/sample-operators/tomcat-operator/k8s/tomcat-sample1.yaml @@ -0,0 +1,7 @@ +apiVersion: "tomcatoperator.io/v1" +kind: Tomcat +metadata: + name: test-tomcat1 +spec: + version: 9.0 + replicas: 2 diff --git a/sample-operators/tomcat-operator/k8s/tomcat-sample2.yaml b/sample-operators/tomcat-operator/k8s/tomcat-sample2.yaml new file mode 100644 index 0000000000..2dec431734 --- /dev/null +++ b/sample-operators/tomcat-operator/k8s/tomcat-sample2.yaml @@ -0,0 +1,7 @@ +apiVersion: "tomcatoperator.io/v1" +kind: Tomcat +metadata: + name: test-tomcat2 +spec: + version: 8.0 + replicas: 4 diff --git a/sample-operators/tomcat-operator/k8s/webapp-sample1.yaml b/sample-operators/tomcat-operator/k8s/webapp-sample1.yaml new file mode 100644 index 0000000000..4913eb2444 --- /dev/null +++ b/sample-operators/tomcat-operator/k8s/webapp-sample1.yaml @@ -0,0 +1,8 @@ +apiVersion: "tomcatoperator.io/v1" +kind: Webapp +metadata: + name: sample-webapp1 +spec: + tomcat: test-tomcat1 + url: http://tomcat.apache.org/tomcat-7.0-doc/appdev/sample/sample.war + contextPath: mysample diff --git a/sample-operators/tomcat-operator/k8s/webapp-sample2.yaml b/sample-operators/tomcat-operator/k8s/webapp-sample2.yaml new file mode 100644 index 0000000000..e0415f9ce5 --- /dev/null +++ b/sample-operators/tomcat-operator/k8s/webapp-sample2.yaml @@ -0,0 +1,8 @@ +apiVersion: "tomcatoperator.io/v1" +kind: Webapp +metadata: + name: sample-webapp2 +spec: + tomcat: test-tomcat2 + url: charlottemach.com/assets/jax.war + contextPath: othercontext diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml new file mode 100644 index 0000000000..77f87b8158 --- /dev/null +++ b/sample-operators/tomcat-operator/pom.xml @@ -0,0 +1,82 @@ + + + 4.0.0 + + + io.javaoperatorsdk + sample-operators + 2.0.0-SNAPSHOT + + + sample-tomcat-operator + Operator SDK - Samples - Tomcat + Provisions Tomcat Pods and deploys Webapplications in them + jar + + + 11 + 11 + 3.1.4 + + + + + io.javaoperatorsdk + operator-framework + ${project.version} + + + io.fabric8 + crd-generator-apt + provided + + + 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/Tomcat.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/Tomcat.java new file mode 100644 index 0000000000..7f60bd00d5 --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/Tomcat.java @@ -0,0 +1,20 @@ +package io.javaoperatorsdk.operator.sample; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +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.ShortNames; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("tomcatoperator.io") +@Version("v1") +@ShortNames("tc") +public class Tomcat extends CustomResource implements Namespaced { + + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE); + } +} diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java new file mode 100644 index 0000000000..a7a7ca40ff --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.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 TomcatOperator { + + private static final Logger log = LoggerFactory.getLogger(TomcatOperator.class); + + public static void main(String[] args) throws IOException { + + Config config = new ConfigBuilder().withNamespace(null).build(); + KubernetesClient client = new DefaultKubernetesClient(config); + Operator operator = new Operator(client, DefaultConfigurationService.instance()); + operator.register(new TomcatReconciler(client)); + operator.register(new WebappReconciler(client)); + operator.start(); + + new FtBasic(new TkFork(new FkRegex("/health", "ALL GOOD.")), 8080).start(Exit.NEVER); + } +} 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 new file mode 100644 index 0000000000..1aa7f1cfd0 --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java @@ -0,0 +1,201 @@ +package io.javaoperatorsdk.operator.sample; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.OwnerReference; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.api.model.apps.DeploymentStatus; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.dsl.RollableScalableResource; +import io.fabric8.kubernetes.client.dsl.ServiceResource; +import io.fabric8.kubernetes.client.informers.SharedIndexInformer; +import io.fabric8.kubernetes.client.utils.Serialization; +import io.javaoperatorsdk.operator.api.*; +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.processing.event.CustomResourceID; +import io.javaoperatorsdk.operator.processing.event.EventSourceRegistry; +import io.javaoperatorsdk.operator.processing.event.internal.InformerEventSource; + +import static java.util.Collections.EMPTY_SET; + +/** + * Runs a specified number of Tomcat app server Pods. It uses a Deployment to create the Pods. Also + * creates a Service over which the Pods can be accessed. + */ +@ControllerConfiguration +public class TomcatReconciler implements Reconciler, EventSourceInitializer { + + private final Logger log = LoggerFactory.getLogger(getClass()); + + private final KubernetesClient kubernetesClient; + + private volatile InformerEventSource informerEventSource; + + public TomcatReconciler(KubernetesClient client) { + this.kubernetesClient = client; + } + + @Override + public void prepareEventSources(EventSourceRegistry eventSourceRegistry) { + SharedIndexInformer deploymentInformer = + kubernetesClient.apps().deployments().inAnyNamespace() + .withLabel("app.kubernetes.io/managed-by", "tomcat-operator") + .runnableInformer(0); + + this.informerEventSource = new InformerEventSource<>(deploymentInformer, d -> { + var ownerReferences = d.getMetadata().getOwnerReferences(); + if (!ownerReferences.isEmpty()) { + return Set.of(new CustomResourceID(ownerReferences.get(0).getName(), + d.getMetadata().getNamespace())); + } else { + return EMPTY_SET; + } + }); + eventSourceRegistry.registerEventSource(this.informerEventSource); + } + + @Override + public UpdateControl reconcile(Tomcat tomcat, Context context) { + createOrUpdateDeployment(tomcat); + createOrUpdateService(tomcat); + + Deployment deployment = informerEventSource.getAssociated(tomcat); + + if (deployment != null) { + Tomcat updatedTomcat = + updateTomcatStatus(tomcat, deployment); + log.info( + "Updating status of Tomcat {} in namespace {} to {} ready replicas", + tomcat.getMetadata().getName(), + tomcat.getMetadata().getNamespace(), + tomcat.getStatus().getReadyReplicas()); + return UpdateControl.updateStatusSubResource(updatedTomcat); + } + return UpdateControl.noUpdate(); + } + + @Override + public DeleteControl cleanup(Tomcat tomcat, Context context) { + deleteDeployment(tomcat); + deleteService(tomcat); + return DeleteControl.defaultDelete(); + } + + private Tomcat updateTomcatStatus(Tomcat tomcat, Deployment deployment) { + DeploymentStatus deploymentStatus = + Objects.requireNonNullElse(deployment.getStatus(), new DeploymentStatus()); + int readyReplicas = Objects.requireNonNullElse(deploymentStatus.getReadyReplicas(), 0); + TomcatStatus status = new TomcatStatus(); + status.setReadyReplicas(readyReplicas); + tomcat.setStatus(status); + return tomcat; + } + + private void createOrUpdateDeployment(Tomcat tomcat) { + String ns = tomcat.getMetadata().getNamespace(); + Deployment existingDeployment = + kubernetesClient + .apps() + .deployments() + .inNamespace(ns) + .withName(tomcat.getMetadata().getName()) + .get(); + if (existingDeployment == null) { + Deployment deployment = loadYaml(Deployment.class, "deployment.yaml"); + deployment.getMetadata().setName(tomcat.getMetadata().getName()); + deployment.getMetadata().setNamespace(ns); + deployment.getMetadata().getLabels().put("app.kubernetes.io/part-of", + tomcat.getMetadata().getName()); + deployment.getMetadata().getLabels().put("app.kubernetes.io/managed-by", "tomcat-operator"); + // set tomcat version + deployment + .getSpec() + .getTemplate() + .getSpec() + .getContainers() + .get(0) + .setImage("tomcat:" + tomcat.getSpec().getVersion()); + deployment.getSpec().setReplicas(tomcat.getSpec().getReplicas()); + + // make sure label selector matches label (which has to be matched by service selector too) + deployment + .getSpec() + .getTemplate() + .getMetadata() + .getLabels() + .put("app", tomcat.getMetadata().getName()); + deployment + .getSpec() + .getSelector() + .getMatchLabels() + .put("app", tomcat.getMetadata().getName()); + + OwnerReference ownerReference = deployment.getMetadata().getOwnerReferences().get(0); + ownerReference.setName(tomcat.getMetadata().getName()); + ownerReference.setUid(tomcat.getMetadata().getUid()); + + log.info("Creating or updating Deployment {} in {}", deployment.getMetadata().getName(), ns); + kubernetesClient.apps().deployments().inNamespace(ns).create(deployment); + } else { + existingDeployment + .getSpec() + .getTemplate() + .getSpec() + .getContainers() + .get(0) + .setImage("tomcat:" + tomcat.getSpec().getVersion()); + existingDeployment.getSpec().setReplicas(tomcat.getSpec().getReplicas()); + kubernetesClient.apps().deployments().inNamespace(ns).createOrReplace(existingDeployment); + } + } + + private void deleteDeployment(Tomcat tomcat) { + log.info("Deleting Deployment {}", tomcat.getMetadata().getName()); + RollableScalableResource deployment = + kubernetesClient + .apps() + .deployments() + .inNamespace(tomcat.getMetadata().getNamespace()) + .withName(tomcat.getMetadata().getName()); + if (deployment.get() != null) { + deployment.delete(); + } + } + + private void createOrUpdateService(Tomcat tomcat) { + Service service = loadYaml(Service.class, "service.yaml"); + service.getMetadata().setName(tomcat.getMetadata().getName()); + String ns = tomcat.getMetadata().getNamespace(); + service.getMetadata().setNamespace(ns); + service.getSpec().getSelector().put("app", tomcat.getMetadata().getName()); + log.info("Creating or updating Service {} in {}", service.getMetadata().getName(), ns); + kubernetesClient.services().inNamespace(ns).createOrReplace(service); + } + + private void deleteService(Tomcat tomcat) { + log.info("Deleting Service {}", tomcat.getMetadata().getName()); + ServiceResource service = + kubernetesClient + .services() + .inNamespace(tomcat.getMetadata().getNamespace()) + .withName(tomcat.getMetadata().getName()); + if (service.get() != null) { + service.delete(); + } + } + + 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); + } + } +} diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatSpec.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatSpec.java new file mode 100644 index 0000000000..fbd22f30f9 --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatSpec.java @@ -0,0 +1,23 @@ +package io.javaoperatorsdk.operator.sample; + +public class TomcatSpec { + + private Integer version; + private Integer replicas; + + public Integer getVersion() { + return version; + } + + public void setVersion(Integer version) { + this.version = version; + } + + public Integer getReplicas() { + return replicas; + } + + public void setReplicas(Integer replicas) { + this.replicas = replicas; + } +} diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatStatus.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatStatus.java new file mode 100644 index 0000000000..3bf3d2ab4b --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatStatus.java @@ -0,0 +1,14 @@ +package io.javaoperatorsdk.operator.sample; + +public class TomcatStatus { + + private Integer readyReplicas = 0; + + public Integer getReadyReplicas() { + return readyReplicas; + } + + public void setReadyReplicas(Integer readyReplicas) { + this.readyReplicas = readyReplicas; + } +} diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/Webapp.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/Webapp.java new file mode 100644 index 0000000000..d61f8791f7 --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/Webapp.java @@ -0,0 +1,21 @@ +package io.javaoperatorsdk.operator.sample; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +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; + +/** + * Represents a web application deployed in a Tomcat deployment + */ +@Group("tomcatoperator.io") +@Version("v1") +public class Webapp extends CustomResource implements Namespaced { + + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE); + } +} 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 new file mode 100644 index 0000000000..64f5db8e77 --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java @@ -0,0 +1,208 @@ +package io.javaoperatorsdk.operator.sample; + +import java.io.ByteArrayOutputStream; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.dsl.ExecListener; +import io.fabric8.kubernetes.client.dsl.ExecWatch; +import io.javaoperatorsdk.operator.api.*; +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.processing.event.CustomResourceID; +import io.javaoperatorsdk.operator.processing.event.EventSourceRegistry; +import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventSource; +import io.javaoperatorsdk.operator.processing.event.internal.InformerEventSource; + +import okhttp3.Response; + +@ControllerConfiguration +public class WebappReconciler implements Reconciler, EventSourceInitializer { + + private KubernetesClient kubernetesClient; + + private final Logger log = LoggerFactory.getLogger(getClass()); + + public WebappReconciler(KubernetesClient kubernetesClient) { + this.kubernetesClient = kubernetesClient; + } + + @Override + public void prepareEventSources(EventSourceRegistry eventSourceRegistry) { + InformerEventSource tomcatEventSource = + new InformerEventSource<>(kubernetesClient, Tomcat.class, t -> { + // To create an event to a related WebApp resource and trigger the reconciliation + // we need to find which WebApp this Tomcat custom resource is related to. + // To find the related customResourceId of the WebApp resource we traverse the cache to + // and identify it based on naming convention. + var webAppInformer = + eventSourceRegistry.getCustomResourceEventSource() + .getInformer(CustomResourceEventSource.ANY_NAMESPACE_MAP_KEY); + + var ids = webAppInformer.getStore().list().stream() + .filter( + (Webapp webApp) -> webApp.getSpec().getTomcat().equals(t.getMetadata().getName())) + .map(webapp -> new CustomResourceID(webapp.getMetadata().getName(), + webapp.getMetadata().getNamespace())) + .collect(Collectors.toSet()); + return ids; + }); + eventSourceRegistry.registerEventSource(tomcatEventSource); + } + + /** + * This method will be called not only on changes to Webapp objects but also when Tomcat objects + * change. + */ + @Override + public UpdateControl reconcile(Webapp webapp, Context context) { + if (webapp.getStatus() != null + && Objects.equals(webapp.getSpec().getUrl(), webapp.getStatus().getDeployedArtifact())) { + return UpdateControl.noUpdate(); + } + + var tomcatClient = kubernetesClient.customResources(Tomcat.class); + Tomcat tomcat = tomcatClient.inNamespace(webapp.getMetadata().getNamespace()) + .withName(webapp.getSpec().getTomcat()).get(); + if (tomcat == null) { + throw new IllegalStateException("Cannot find Tomcat " + webapp.getSpec().getTomcat() + + " for Webapp " + webapp.getMetadata().getName() + " in namespace " + + webapp.getMetadata().getNamespace()); + } + + if (tomcat.getStatus() != null + && Objects.equals(tomcat.getSpec().getReplicas(), tomcat.getStatus().getReadyReplicas())) { + log.info( + "Tomcat is ready and webapps not yet deployed. Commencing deployment of {} in Tomcat {}", + webapp.getMetadata().getName(), tomcat.getMetadata().getName()); + String[] command = new String[] {"wget", "-O", + "/data/" + webapp.getSpec().getContextPath() + ".war", webapp.getSpec().getUrl()}; + if (log.isInfoEnabled()) { + command = new String[] {"time", "wget", "-O", + "/data/" + webapp.getSpec().getContextPath() + ".war", webapp.getSpec().getUrl()}; + } + + String[] commandStatusInAllPods = executeCommandInAllPods(kubernetesClient, webapp, command); + + if (webapp.getStatus() == null) { + webapp.setStatus(new WebappStatus()); + } + webapp.getStatus().setDeployedArtifact(webapp.getSpec().getUrl()); + webapp.getStatus().setDeploymentStatus(commandStatusInAllPods); + return UpdateControl.updateStatusSubResource(webapp); + } else { + log.info("WebappController invoked but Tomcat not ready yet ({}/{})", + tomcat.getStatus() != null ? tomcat.getStatus().getReadyReplicas() : 0, + tomcat.getSpec().getReplicas()); + return UpdateControl.noUpdate(); + } + } + + @Override + public DeleteControl cleanup(Webapp webapp, Context context) { + + String[] command = new String[] {"rm", "/data/" + webapp.getSpec().getContextPath() + ".war"}; + String[] commandStatusInAllPods = executeCommandInAllPods(kubernetesClient, webapp, command); + if (webapp.getStatus() != null) { + webapp.getStatus().setDeployedArtifact(null); + webapp.getStatus().setDeploymentStatus(commandStatusInAllPods); + } + return DeleteControl.defaultDelete(); + } + + private String[] executeCommandInAllPods( + KubernetesClient kubernetesClient, Webapp webapp, String[] command) { + String[] status = new String[0]; + + Deployment deployment = + kubernetesClient + .apps() + .deployments() + .inNamespace(webapp.getMetadata().getNamespace()) + .withName(webapp.getSpec().getTomcat()) + .get(); + + if (deployment != null) { + List pods = + kubernetesClient + .pods() + .inNamespace(webapp.getMetadata().getNamespace()) + .withLabels(deployment.getSpec().getSelector().getMatchLabels()) + .list() + .getItems(); + status = new String[pods.size()]; + for (int i = 0; i < pods.size(); i++) { + Pod pod = pods.get(i); + log.info( + "Executing command {} in Pod {}", + String.join(" ", command), + pod.getMetadata().getName()); + + CompletableFuture data = new CompletableFuture<>(); + try (ExecWatch execWatch = execCmd(pod, data, command)) { + status[i] = "" + pod.getMetadata().getName() + ":" + data.get(30, TimeUnit.SECONDS);; + } catch (ExecutionException e) { + status[i] = "" + pod.getMetadata().getName() + ": ExecutionException - " + e.getMessage(); + } catch (InterruptedException e) { + status[i] = + "" + pod.getMetadata().getName() + ": InterruptedException - " + e.getMessage(); + } catch (TimeoutException e) { + status[i] = "" + pod.getMetadata().getName() + ": TimeoutException - " + e.getMessage(); + } + } + } + return status; + } + + private ExecWatch execCmd(Pod pod, CompletableFuture data, String... command) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + return kubernetesClient.pods() + .inNamespace(pod.getMetadata().getNamespace()) + .withName(pod.getMetadata().getName()) + .inContainer("war-downloader") + .writingOutput(baos) + .writingError(baos) + .usingListener(new SimpleListener(data, baos)) + .exec(command); + } + + static class SimpleListener implements ExecListener { + + private CompletableFuture data; + private ByteArrayOutputStream baos; + private final Logger log = LoggerFactory.getLogger(getClass()); + + public SimpleListener(CompletableFuture data, ByteArrayOutputStream baos) { + this.data = data; + this.baos = baos; + } + + @Override + public void onOpen(Response response) { + log.debug("Reading data... " + response.message()); + } + + @Override + public void onFailure(Throwable t, Response response) { + log.debug(t.getMessage() + " " + response.message()); + data.completeExceptionally(t); + } + + @Override + public void onClose(int code, String reason) { + log.debug("Exit with: " + code + " and with reason: " + reason); + data.complete(baos.toString()); + } + } + +} diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappSpec.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappSpec.java new file mode 100644 index 0000000000..a34621c35b --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappSpec.java @@ -0,0 +1,34 @@ +package io.javaoperatorsdk.operator.sample; + +public class WebappSpec { + + private String url; + + private String contextPath; + + private String tomcat; + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getContextPath() { + return contextPath; + } + + public void setContextPath(String contextPath) { + this.contextPath = contextPath; + } + + public String getTomcat() { + return tomcat; + } + + public void setTomcat(String tomcat) { + this.tomcat = tomcat; + } +} diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappStatus.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappStatus.java new file mode 100644 index 0000000000..8267abe24c --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappStatus.java @@ -0,0 +1,24 @@ +package io.javaoperatorsdk.operator.sample; + +public class WebappStatus { + + private String deployedArtifact; + + public String getDeployedArtifact() { + return deployedArtifact; + } + + public void setDeployedArtifact(String deployedArtifact) { + this.deployedArtifact = deployedArtifact; + } + + private String[] deploymentStatus; + + public String[] getDeploymentStatus() { + return deploymentStatus; + } + + public void setDeploymentStatus(String[] deploymentStatus) { + this.deploymentStatus = deploymentStatus; + } +} diff --git a/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/deployment.yaml b/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/deployment.yaml new file mode 100644 index 0000000000..2f6f373c6c --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/deployment.yaml @@ -0,0 +1,39 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: "" + labels: + app.kubernetes.io/part-of: "" + app.kubernetes.io/managed-by: "" # used for filtering of Deployments created by the controller + ownerReferences: # used for finding which Tomcat does this Deployment belong to + - apiVersion: apps/v1 + kind: Tomcat + name: "" + uid: "" +spec: + selector: + matchLabels: + app: "" + replicas: 1 + template: + metadata: + labels: + app: "" + spec: + containers: + - name: tomcat + image: tomcat:8.0 + ports: + - containerPort: 8080 + volumeMounts: + - mountPath: /usr/local/tomcat/webapps + name: webapps-volume + - name: war-downloader + image: busybox:1.28 + command: ['tail', '-f', '/dev/null'] + volumeMounts: + - name: webapps-volume + mountPath: /data + volumes: + - name: webapps-volume + emptydir: {} diff --git a/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/service.yaml b/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/service.yaml new file mode 100644 index 0000000000..ab198643ed --- /dev/null +++ b/sample-operators/tomcat-operator/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: 8080 + type: NodePort diff --git a/sample-operators/tomcat-operator/src/main/resources/log4j2.xml b/sample-operators/tomcat-operator/src/main/resources/log4j2.xml new file mode 100644 index 0000000000..a99aaf31b6 --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/resources/log4j2.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + 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 new file mode 100644 index 0000000000..f3cd473294 --- /dev/null +++ b/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java @@ -0,0 +1,136 @@ +package io.javaoperatorsdk.operator.sample; + +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.*; +import io.fabric8.kubernetes.client.*; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.ConfigBuilder; +import io.fabric8.kubernetes.client.extended.run.RunConfigBuilder; +import io.javaoperatorsdk.operator.Operator; +import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; + +import static java.util.concurrent.TimeUnit.*; +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; + +public class TomcatOperatorE2E { + + final static String TEST_NS = "tomcat-test"; + + final static Logger log = LoggerFactory.getLogger(TomcatOperatorE2E.class); + + @Test + public void test() { + 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()); + operator.register(new TomcatReconciler(client)); + operator.register(new WebappReconciler(client)); + operator.start(); + } + + Tomcat tomcat = new Tomcat(); + tomcat.setMetadata(new ObjectMetaBuilder() + .withName("test-tomcat1") + .withNamespace(TEST_NS) + .build()); + tomcat.setSpec(new TomcatSpec()); + tomcat.getSpec().setReplicas(3); + tomcat.getSpec().setVersion(9); + + Webapp webapp1 = new Webapp(); + webapp1.setMetadata(new ObjectMetaBuilder() + .withName("test-webapp1") + .withNamespace(TEST_NS) + .build()); + webapp1.setSpec(new WebappSpec()); + webapp1.getSpec().setContextPath("webapp1"); + webapp1.getSpec().setTomcat(tomcat.getMetadata().getName()); + webapp1.getSpec().setUrl("http://tomcat.apache.org/tomcat-7.0-doc/appdev/sample/sample.war"); + + var tomcatClient = client.customResources(Tomcat.class); + var webappClient = client.customResources(Webapp.class); + + 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("tomcat-test").get() == null); + } + + log.info("Creating test namespace {}", TEST_NS); + client.namespaces().create(testNs); + + log.info("Creating test Tomcat object: {}", tomcat); + tomcatClient.inNamespace(TEST_NS).create(tomcat); + log.info("Creating test Webapp object: {}", webapp1); + webappClient.inNamespace(TEST_NS).create(webapp1); + + log.info("Waiting 5 minutes for Tomcat and Webapp CR statuses to be updated"); + await().atMost(5, MINUTES).untilAsserted(() -> { + Tomcat updatedTomcat = + tomcatClient.inNamespace(TEST_NS).withName(tomcat.getMetadata().getName()).get(); + Webapp updatedWebapp = + webappClient.inNamespace(TEST_NS).withName(webapp1.getMetadata().getName()).get(); + assertThat(updatedTomcat.getStatus(), is(notNullValue())); + assertThat(updatedTomcat.getStatus().getReadyReplicas(), equalTo(3)); + assertThat(updatedWebapp.getStatus(), is(notNullValue())); + assertThat(updatedWebapp.getStatus().getDeployedArtifact(), is(notNullValue())); + }); + + String url = + "http://" + tomcat.getMetadata().getName() + "/" + webapp1.getSpec().getContextPath() + "/"; + log.info("Starting curl Pod and waiting 5 minutes for GET of {} to return 200", url); + + await("wait-for-webapp").atMost(6, MINUTES).untilAsserted(() -> { + try { + + log.info("Starting curl Pod to test if webapp was deployed correctly"); + Pod curlPod = client.run().inNamespace(TEST_NS) + .withRunConfig(new RunConfigBuilder() + .withArgs("-s", "-o", "/dev/null", "-w", "%{http_code}", url) + .withName("curl") + .withImage("curlimages/curl:7.78.0") + .withRestartPolicy("Never") + .build()) + .done(); + log.info("Waiting for curl Pod to finish running"); + await("wait-for-curl-pod-run").atMost(2, MINUTES) + .until(() -> { + String phase = + client.pods().inNamespace(TEST_NS).withName("curl").get().getStatus().getPhase(); + return phase.equals("Succeeded") || phase.equals("Failed"); + }); + + String curlOutput = + client.pods().inNamespace(TEST_NS).withName(curlPod.getMetadata().getName()).getLog(); + log.info("Output from curl: '{}'", curlOutput); + assertThat(curlOutput, equalTo("200")); + } catch (KubernetesClientException ex) { + throw new AssertionError(ex); + } finally { + log.info("Deleting curl Pod"); + client.pods().inNamespace(TEST_NS).withName("curl").delete(); + await("wait-for-curl-pod-stop").atMost(1, MINUTES) + .until(() -> client.pods().inNamespace(TEST_NS).withName("curl").get() == null); + } + }); + } + +} diff --git a/sample-operators/tomcat-operator/src/test/resources/log4j2.xml b/sample-operators/tomcat-operator/src/test/resources/log4j2.xml new file mode 100644 index 0000000000..a99aaf31b6 --- /dev/null +++ b/sample-operators/tomcat-operator/src/test/resources/log4j2.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/samples/README.md b/samples/README.md deleted file mode 100644 index 3af900da4f..0000000000 --- a/samples/README.md +++ /dev/null @@ -1,4 +0,0 @@ -This samples folder contains simple artificial samples used for testing the framework rather -than showing off its real-world usage. - -More realistic samples can be found here: https://github.com/java-operator-sdk/samples diff --git a/smoke-test-samples/README.md b/smoke-test-samples/README.md new file mode 100644 index 0000000000..12daaa70f6 --- /dev/null +++ b/smoke-test-samples/README.md @@ -0,0 +1,4 @@ +This samples folder contains simple artificial samples used for testing the framework rather than +showing off its real-world usage. + +More realistic samples can be found in the `sample-operators` directory. diff --git a/samples/common/crd/test_object.yaml b/smoke-test-samples/common/crd/test_object.yaml similarity index 100% rename from samples/common/crd/test_object.yaml rename to smoke-test-samples/common/crd/test_object.yaml diff --git a/samples/common/pom.xml b/smoke-test-samples/common/pom.xml similarity index 85% rename from samples/common/pom.xml rename to smoke-test-samples/common/pom.xml index 5e873dfe23..ed1884aecd 100644 --- a/samples/common/pom.xml +++ b/smoke-test-samples/common/pom.xml @@ -5,12 +5,12 @@ io.javaoperatorsdk - java-operator-sdk-samples + java-operator-sdk-smoke-test-samples 2.0.0-SNAPSHOT - operator-framework-samples-common - Operator SDK - Samples - Common Files + operator-framework-smoke-test-samples-common + Operator SDK - Smoke Test Samples - Common Files Files shared between some of the samples jar diff --git a/samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomService.java b/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomService.java similarity index 100% rename from samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomService.java rename to smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomService.java diff --git a/samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java b/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java similarity index 100% rename from samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java rename to smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java diff --git a/samples/common/src/main/java/io/javaoperatorsdk/operator/sample/ServiceSpec.java b/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/ServiceSpec.java similarity index 100% rename from samples/common/src/main/java/io/javaoperatorsdk/operator/sample/ServiceSpec.java rename to smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/ServiceSpec.java diff --git a/samples/common/src/main/resources/log4j2.xml b/smoke-test-samples/common/src/main/resources/log4j2.xml similarity index 100% rename from samples/common/src/main/resources/log4j2.xml rename to smoke-test-samples/common/src/main/resources/log4j2.xml diff --git a/samples/pom.xml b/smoke-test-samples/pom.xml similarity index 85% rename from samples/pom.xml rename to smoke-test-samples/pom.xml index 386cc0c160..566fbd98be 100644 --- a/samples/pom.xml +++ b/smoke-test-samples/pom.xml @@ -9,9 +9,9 @@ 2.0.0-SNAPSHOT - java-operator-sdk-samples - Operator SDK - Samples - Sample usage of the operator sdk + java-operator-sdk-smoke-test-samples + Operator SDK - Smoke Test Samples + Samples to manually smoke the sdk pom diff --git a/samples/pure-java/pom.xml b/smoke-test-samples/pure-java/pom.xml similarity index 70% rename from samples/pure-java/pom.xml rename to smoke-test-samples/pure-java/pom.xml index 74a9cebd56..d1a2de4c2b 100644 --- a/samples/pure-java/pom.xml +++ b/smoke-test-samples/pure-java/pom.xml @@ -5,19 +5,19 @@ io.javaoperatorsdk - java-operator-sdk-samples + java-operator-sdk-smoke-test-samples 2.0.0-SNAPSHOT - operator-framework-samples-pure-java - Operator SDK - Samples - Pure Java + operator-framework-smoke-test-samples-pure-java + Operator SDK - Smoke Test Samples - Pure Java Sample usage with pure java app jar io.javaoperatorsdk - operator-framework-samples-common + operator-framework-smoke-test-samples-common ${project.version} diff --git a/samples/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java b/smoke-test-samples/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java similarity index 100% rename from samples/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java rename to smoke-test-samples/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java diff --git a/samples/spring-boot-plain/pom.xml b/smoke-test-samples/spring-boot-plain/pom.xml similarity index 88% rename from samples/spring-boot-plain/pom.xml rename to smoke-test-samples/spring-boot-plain/pom.xml index 82fd73013b..de64a684e5 100644 --- a/samples/spring-boot-plain/pom.xml +++ b/smoke-test-samples/spring-boot-plain/pom.xml @@ -5,19 +5,19 @@ io.javaoperatorsdk - java-operator-sdk-samples + java-operator-sdk-smoke-test-samples 2.0.0-SNAPSHOT - operator-framework-samples-spring-boot-plain - Operator SDK - Samples - Spring Boot - Plain + operator-framework-smoke-test-samples-spring-boot + Operator SDK - Smoke Test Samples - Spring Boot Sample usage with Spring Boot jar io.javaoperatorsdk - operator-framework-samples-common + operator-framework-smoke-test-samples-common ${project.version} diff --git a/samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/Config.java b/smoke-test-samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/Config.java similarity index 100% rename from samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/Config.java rename to smoke-test-samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/Config.java diff --git a/samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/SpringBootStarterSampleApplication.java b/smoke-test-samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/SpringBootStarterSampleApplication.java similarity index 100% rename from samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/SpringBootStarterSampleApplication.java rename to smoke-test-samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/SpringBootStarterSampleApplication.java diff --git a/samples/spring-boot-plain/src/main/resources/application.yaml b/smoke-test-samples/spring-boot-plain/src/main/resources/application.yaml similarity index 100% rename from samples/spring-boot-plain/src/main/resources/application.yaml rename to smoke-test-samples/spring-boot-plain/src/main/resources/application.yaml