diff --git a/.github/workflows/TomcatIntegrationTest.yml b/.github/workflows/TomcatIntegrationTest.yml
index 4221783..0d1acd9 100644
--- a/.github/workflows/TomcatIntegrationTest.yml
+++ b/.github/workflows/TomcatIntegrationTest.yml
@@ -6,35 +6,174 @@ on:
jobs:
tomcat_integration_test:
runs-on: ubuntu-latest
+ env:
+ KIND_CL_NAME: tomcat-integration
steps:
- name: Checkout
uses: actions/checkout@v2
-
- - name: Set up Helm
- uses: azure/setup-helm@v1
- with:
- version: v3.4.0
-
- - uses: actions/setup-python@v2
- with:
- python-version: 3.7
+
+ - 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 kind cluster
uses: helm/kind-action@v1.2.0
+ with:
+ cluster_name: ${{ env.KIND_CL_NAME }}
- name: Apply CRDs
run: kubectl apply -f tomcat/k8s/crd.yaml
- name: Set up Java and Maven
- uses: actions/setup-java@v1
+ uses: actions/setup-java@v2
+ with:
+ java-version: 15
+ distribution: adopt-hotspot
+
+ - name: cache
+ uses: actions/cache@v2
+ if: ${{ !env.ACT }}
+ with:
+ path: ~/.m2/repository
+ key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
+ restore-keys: |
+ ${{ runner.os }}-maven-m2
+
+ - name: Set up Maven
+ uses: stCarolas/setup-maven@v4
+ if: ${{ env.ACT }}
+ with:
+ maven-version: 3.8.1
+
+ - name: Run unit tests
+ if: ${{ env.ACT }}
+ run: mvn --version
+
+ - name: Run unit tests
+ run: mvn -B failsafe:integration-test --file tomcat/pom.xml
+
+ tomcat_local_apply_setup_test:
+ runs-on: ubuntu-latest
+ env:
+ KIND_CL_NAME: tomcat-local-apply
+ 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: ${{ env.KIND_CL_NAME }}
+ registry: false
+
+ # for DIMG in "tomcat-local-apply-control-plane kind-registry tomcat_local_apply_setup_test "; do docker stop $DIMG ; docker rm $DIMG ; done ; sleep 1
+
+ - name: Set up Java and Maven
+ uses: actions/setup-java@v2
with:
# java-version: ${{ matrix.java }}
java-version: 15
- - uses: actions/cache@v2
+ distribution: adopt-hotspot
+
+ - name: cache
+ uses: actions/cache@v2
+ if: ${{ !env.ACT }}
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- - name: Run unit tests
- run: mvn -B test --file tomcat/pom.xml
\ No newline at end of file
+
+ - name: Set up Maven for local ACT
+ uses: stCarolas/setup-maven@v4
+ if: ${{ env.ACT }}
+ with:
+ maven-version: 3.8.1
+
+ - name: build jib
+ run: |
+ mvn -B package jib:dockerBuild jib:buildTar -Djib-maven-image=tomcat-operator --file tomcat/pom.xml -DskipTests
+ kind load image-archive tomcat/target/jib-image.tar --name=${{ env.KIND_CL_NAME }}
+
+ - name: Apply CRDs
+ run: kubectl apply -f tomcat/k8s/crd.yaml
+
+ - name: install tomcat operator
+ run: |
+ kubectl apply -f tomcat/k8s/operator.yaml
+
+ - name: create ns tomcatoperator-sample
+ run: kubectl create ns tomcatoperator-sample
+
+ - name: debug local kind
+ if: ${{ env.ACT }}
+ run: |
+ kubectl get pods -n tomcat-operator -l app=tomcat-operator -o yaml | tee -a debug.log
+
+ - name: wait for operators ready
+ run: |
+ LOOP=0 &&\
+ while [[ $(kubectl get pods -n tomcat-operator -l app=tomcat-operator -o 'jsonpath={..status.conditions[?(@.type=="Ready")].status}') != "True" ]]; do \
+ echo "waiting for pod" &&\
+ kubectl logs -n tomcat-operator -l app=tomcat-operator &&\
+ (( LOOP++ )) &&\
+ if [[ $LOOP -gt 10 ]]; then exit 1; fi &&\
+ echo "loop number $LOOP" &&\
+ sleep 5; \
+ done
+
+ - name: install sample operators
+ run: |
+ for sample in $(ls tomcat/k8s/*sample*); do
+ kubectl -n tomcatoperator-sample apply -f $sample;
+ done
+
+ - name: check pod correctly started
+ run: |
+ LOOP=0 &&\
+ while [[ $(kubectl get pods -n tomcatoperator-sample -o 'jsonpath={..status.conditions[?(@.type=="Ready")].status}') != "True True True True True True" ]]; do \
+ echo "waiting for pod" &&\
+ kubectl logs -n tomcat-operator -l app=tomcat-operator &&\
+ kubectl get pods -n tomcatoperator-sample &&\
+ (( LOOP++ )) &&\
+ if [[ $LOOP -gt 10 ]]; then exit 1; fi &&\
+ echo "loop number $LOOP" &&\
+ sleep 10; \
+ done
+ #Waiting 5 seconds for Tomcat to unpack the downloaded war
+ sleep 5;
+
+ - name: Get webapps
+ run: |
+ kubectl get tomcats,webapps -A -o yaml | tee -a debug
+ kubectl -n tomcatoperator-sample -c tomcat logs -l app=test-tomcat1 | grep startup
+
+ - name: check code
+ run: |
+ kubectl -n tomcatoperator-sample run sample1 --labels=app=curl --image=curlimages/curl:7.78.0 --restart=Never --timeout=30s --command -- curl -s -v http://test-tomcat1/mysample/;
+ kubectl -n tomcatoperator-sample run sample2 --labels=app=curl --image=curlimages/curl:7.78.0 --restart=Never --timeout=30s --command -- curl -s -v http://test-tomcat2/othercontext/;
+ LOOP=0 &&\
+ while [[ $(kubectl get pods -n tomcatoperator-sample -l app=curl -o 'jsonpath={..status.phase}') != "Succeeded Succeeded" ]]; do \
+ echo "waiting for pod" &&\
+ kubectl logs -n tomcatoperator-sample -l app=curl &&\
+ (( LOOP++ )) &&\
+ if [[ $LOOP -gt 5 ]]; then exit 1; fi &&\
+ echo "loop number $LOOP" &&\
+ sleep 5; \
+ done
+ if [[ $(kubectl logs -n tomcatoperator-sample sample1 --tail=500 | grep tomcat.gif | wc -l) -ne 1 ]]; then exit 1; fi
+ if [[ $(kubectl logs -n tomcatoperator-sample sample2 --tail=500 | grep dog.jpeg | wc -l) -ne 1 ]]; then exit 1; fi
diff --git a/tomcat/k8s/crd.yaml b/tomcat/k8s/crd.yaml
index 44533a3..509c77b 100644
--- a/tomcat/k8s/crd.yaml
+++ b/tomcat/k8s/crd.yaml
@@ -80,6 +80,10 @@ spec:
properties:
deployedArtifact:
type: string
+ deploymentStatus:
+ type: array
+ items:
+ type: string
required: [spec]
# either Namespaced or Cluster
scope: Namespaced
diff --git a/tomcat/k8s/operator.yaml b/tomcat/k8s/operator.yaml
index c3e5776..a88b651 100644
--- a/tomcat/k8s/operator.yaml
+++ b/tomcat/k8s/operator.yaml
@@ -65,8 +65,27 @@ metadata:
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/tomcat/k8s/webapp-sample2.yaml b/tomcat/k8s/webapp-sample2.yaml
index a548451..e0415f9 100644
--- a/tomcat/k8s/webapp-sample2.yaml
+++ b/tomcat/k8s/webapp-sample2.yaml
@@ -5,4 +5,4 @@ metadata:
spec:
tomcat: test-tomcat2
url: charlottemach.com/assets/jax.war
- contextPath: mysample
+ contextPath: othercontext
diff --git a/tomcat/pom.xml b/tomcat/pom.xml
index 5f3069d..0e2a8c4 100644
--- a/tomcat/pom.xml
+++ b/tomcat/pom.xml
@@ -19,14 +19,15 @@
11
11
- 2.7.1
+ 3.1.4
+
io.javaoperatorsdk
operator-framework
- 1.8.4
+ 1.9.2
org.apache.logging.log4j
@@ -72,6 +73,16 @@
maven-compiler-plugin
3.8.1
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.0.0-M5
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+ 3.0.0-M5
+
diff --git a/tomcat/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentEvent.java b/tomcat/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentEvent.java
index a213cac..d8e9190 100644
--- a/tomcat/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentEvent.java
+++ b/tomcat/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentEvent.java
@@ -2,9 +2,9 @@
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.fabric8.kubernetes.client.Watcher;
-import io.javaoperatorsdk.operator.processing.event.AbstractEvent;
+import io.javaoperatorsdk.operator.processing.event.DefaultEvent;
-public class DeploymentEvent extends AbstractEvent {
+public class DeploymentEvent extends DefaultEvent {
private final Watcher.Action action;
private final Deployment deployment;
diff --git a/tomcat/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentEventSource.java b/tomcat/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentEventSource.java
index 30d97e5..55a85c8 100644
--- a/tomcat/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentEventSource.java
+++ b/tomcat/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentEventSource.java
@@ -11,6 +11,10 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+/**
+ * Used by the TomcatController to watch changes on Deployment objects. As the Pods of the Deployment start up
+ * the TomcatController updates the status.readyReplicas field.
+ */
public class DeploymentEventSource extends AbstractEventSource implements Watcher {
private static final Logger log = LoggerFactory.getLogger(DeploymentEventSource.class);
diff --git a/tomcat/src/main/java/io/javaoperatorsdk/operator/sample/TomcatController.java b/tomcat/src/main/java/io/javaoperatorsdk/operator/sample/TomcatController.java
index 6652d8e..12f1a1e 100644
--- a/tomcat/src/main/java/io/javaoperatorsdk/operator/sample/TomcatController.java
+++ b/tomcat/src/main/java/io/javaoperatorsdk/operator/sample/TomcatController.java
@@ -19,6 +19,10 @@
import java.util.Objects;
import java.util.Optional;
+/**
+ * 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.
+ */
@Controller
public class TomcatController implements ResourceController {
diff --git a/tomcat/src/main/java/io/javaoperatorsdk/operator/sample/TomcatEvent.java b/tomcat/src/main/java/io/javaoperatorsdk/operator/sample/TomcatEvent.java
new file mode 100644
index 0000000..b10f56d
--- /dev/null
+++ b/tomcat/src/main/java/io/javaoperatorsdk/operator/sample/TomcatEvent.java
@@ -0,0 +1,49 @@
+package io.javaoperatorsdk.operator.sample;
+
+import io.fabric8.kubernetes.client.Watcher;
+import io.javaoperatorsdk.operator.processing.event.DefaultEvent;
+
+public class TomcatEvent extends DefaultEvent {
+
+ private final Watcher.Action action;
+ private final Tomcat tomcat;
+
+ public TomcatEvent(
+ Watcher.Action action, Tomcat resource, TomcatEventSource tomcatEventSource, String webappUid) {
+ super(webappUid, tomcatEventSource);
+ this.action = action;
+ this.tomcat = resource;
+ }
+
+ public Watcher.Action getAction() {
+ return action;
+ }
+
+ public String resourceUid() {
+ return getTomcat().getMetadata().getUid();
+ }
+
+ @Override
+ public String toString() {
+ return "CustomResourceEvent{"
+ + "action="
+ + action
+ + ", resource=[ name="
+ + getTomcat().getMetadata().getName()
+ + ", kind="
+ + getTomcat().getKind()
+ + ", apiVersion="
+ + getTomcat().getApiVersion()
+ + " ,resourceVersion="
+ + getTomcat().getMetadata().getResourceVersion()
+ + ", markedForDeletion: "
+ + (getTomcat().getMetadata().getDeletionTimestamp() != null
+ && !getTomcat().getMetadata().getDeletionTimestamp().isEmpty())
+ + " ]"
+ + '}';
+ }
+
+ public Tomcat getTomcat() {
+ return tomcat;
+ }
+}
diff --git a/tomcat/src/main/java/io/javaoperatorsdk/operator/sample/TomcatEventSource.java b/tomcat/src/main/java/io/javaoperatorsdk/operator/sample/TomcatEventSource.java
new file mode 100644
index 0000000..fe9bf6f
--- /dev/null
+++ b/tomcat/src/main/java/io/javaoperatorsdk/operator/sample/TomcatEventSource.java
@@ -0,0 +1,80 @@
+package io.javaoperatorsdk.operator.sample;
+
+import io.fabric8.kubernetes.client.KubernetesClient;
+import io.fabric8.kubernetes.client.Watcher;
+import io.fabric8.kubernetes.client.WatcherException;
+import io.javaoperatorsdk.operator.processing.event.AbstractEventSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Optional;
+
+import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID;
+import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion;
+
+/**
+ * Used by the WebappController to watch changes on Tomcat objects
+ */
+public class TomcatEventSource extends AbstractEventSource implements Watcher {
+ private static final Logger log = LoggerFactory.getLogger(TomcatEventSource.class);
+
+ private final KubernetesClient client;
+
+ public static TomcatEventSource createAndRegisterWatch(KubernetesClient client) {
+ TomcatEventSource tomcatEventSource = new TomcatEventSource(client);
+ tomcatEventSource.registerWatch();
+ return tomcatEventSource;
+ }
+
+ private TomcatEventSource(KubernetesClient client) {
+ this.client = client;
+ }
+
+ private void registerWatch() {
+ var tomcatClient = client.customResources(Tomcat.class);
+ tomcatClient.inAnyNamespace().watch(this);
+ }
+
+ @Override
+ public void eventReceived(Action action, Tomcat tomcat) {
+ log.info("Event received for action: {}, Tomcat: {}", action.name(), tomcat.getMetadata().getName());
+
+ if (action == Action.ERROR) {
+ log.warn(
+ "Skipping {} event for custom resource uid: {}, version: {}",
+ action,
+ getUID(tomcat),
+ getVersion(tomcat));
+ return;
+ }
+
+ var webappClient = client.customResources(Webapp.class);
+ Optional webapp = webappClient.inNamespace(tomcat.getMetadata().getNamespace())
+ .list().getItems().stream()
+ .filter(wapp -> wapp.getSpec().getTomcat().equals(tomcat.getMetadata().getName()))
+ .findFirst();
+
+ if (webapp.isPresent()) {
+ eventHandler.handleEvent(new TomcatEvent(action, tomcat, this,
+ webapp.get().getMetadata().getUid()));
+ } else {
+ log.debug("Webapp not found for Tomcat {}", tomcat.getMetadata().getName());
+ }
+ }
+
+ @Override
+ public void onClose(WatcherException e) {
+ if (e == null) {
+ return;
+ }
+ if (e.isHttpGone()) {
+ log.warn("Received error for watch, will try to reconnect.", e);
+ registerWatch();
+ } else {
+ // Note that this should not happen normally, since fabric8 client handles reconnect.
+ // In case it tries to reconnect this method is not called.
+ log.error("Unexpected error happened with watch. Will exit.", e);
+ System.exit(1);
+ }
+ }
+}
diff --git a/tomcat/src/main/java/io/javaoperatorsdk/operator/sample/WatchedResource.java b/tomcat/src/main/java/io/javaoperatorsdk/operator/sample/WatchedResource.java
deleted file mode 100644
index cdebcbf..0000000
--- a/tomcat/src/main/java/io/javaoperatorsdk/operator/sample/WatchedResource.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package io.javaoperatorsdk.operator.sample;
-
-import io.fabric8.kubernetes.api.model.HasMetadata;
-import java.util.Objects;
-import org.apache.commons.lang3.builder.EqualsBuilder;
-
-class WatchedResource {
- private final String type;
- private final String name;
-
- public WatchedResource(String type, String name) {
- this.type = type;
- this.name = name;
- }
-
- public static WatchedResource fromResource(HasMetadata resource) {
- return new WatchedResource(resource.getKind(), resource.getMetadata().getName());
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
-
- if (o == null || getClass() != o.getClass()) return false;
-
- WatchedResource that = (WatchedResource) o;
-
- return new EqualsBuilder().append(type, that.type).append(name, that.name).isEquals();
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(type, name);
- }
-
- @Override
- public String toString() {
- return "WatchedResource{" + "type='" + type + '\'' + ", name='" + name + '\'' + '}';
- }
-}
diff --git a/tomcat/src/main/java/io/javaoperatorsdk/operator/sample/Webapp.java b/tomcat/src/main/java/io/javaoperatorsdk/operator/sample/Webapp.java
index 75e7a64..1238264 100644
--- a/tomcat/src/main/java/io/javaoperatorsdk/operator/sample/Webapp.java
+++ b/tomcat/src/main/java/io/javaoperatorsdk/operator/sample/Webapp.java
@@ -5,6 +5,9 @@
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 {}
diff --git a/tomcat/src/main/java/io/javaoperatorsdk/operator/sample/WebappController.java b/tomcat/src/main/java/io/javaoperatorsdk/operator/sample/WebappController.java
index c94dbb8..682ca2a 100644
--- a/tomcat/src/main/java/io/javaoperatorsdk/operator/sample/WebappController.java
+++ b/tomcat/src/main/java/io/javaoperatorsdk/operator/sample/WebappController.java
@@ -3,13 +3,23 @@
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.processing.event.EventSourceManager;
+import okhttp3.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.nio.charset.Charset;
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;
@Controller
public class WebappController implements ResourceController {
@@ -22,30 +32,65 @@ public WebappController(KubernetesClient kubernetesClient) {
this.kubernetesClient = kubernetesClient;
}
+ @Override
+ public void init(EventSourceManager eventSourceManager) {
+ TomcatEventSource tomcatEventSource = TomcatEventSource.createAndRegisterWatch(kubernetesClient);
+ eventSourceManager.registerEventSource("tomcat-event-source", tomcatEventSource);
+ }
+
+ /**
+ * This method will be called not only on changes to Webapp objects but also when Tomcat objects change.
+ */
@Override
public UpdateControl createOrUpdateResource(Webapp webapp, Context context) {
if (webapp.getStatus() != null && Objects.equals(webapp.getSpec().getUrl(), webapp.getStatus().getDeployedArtifact())) {
return UpdateControl.noUpdate();
}
- String[] command = new String[] {"wget", "-O", "/data/" + webapp.getSpec().getContextPath() + ".war", webapp.getSpec().getUrl()};
+ 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());
+ }
- executeCommandInAllPods(kubernetesClient, webapp, command);
+ 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);
- //webapp.getStatus().setDeployedArtifact(webapp.getSpec().getUrl());
- return UpdateControl.updateStatusSubResource(webapp);
+ 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 deleteResource(Webapp webapp, Context context) {
String[] command = new String[] {"rm", "/data/" + webapp.getSpec().getContextPath() + ".war"};
- executeCommandInAllPods(kubernetesClient, webapp, command);
+ String[] commandStatusInAllPods = executeCommandInAllPods(kubernetesClient, webapp, command);
+ if (webapp.getStatus() != null) {
+ webapp.getStatus().setDeployedArtifact(null);
+ webapp.getStatus().setDeploymentStatus(commandStatusInAllPods);
+ }
return DeleteControl.DEFAULT_DELETE;
}
- private void executeCommandInAllPods(
+ private String[] executeCommandInAllPods(
KubernetesClient kubernetesClient, Webapp webapp, String[] command) {
+ String[] status = new String[0];
+
Deployment deployment =
kubernetesClient
.apps()
@@ -62,20 +107,67 @@ private void executeCommandInAllPods(
.withLabels(deployment.getSpec().getSelector().getMatchLabels())
.list()
.getItems();
- for (Pod pod : pods) {
+ status = new String[pods.size()];
+ for (int i=0; i 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(new ByteArrayOutputStream())
- .writingError(new ByteArrayOutputStream())
+ .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/tomcat/src/main/java/io/javaoperatorsdk/operator/sample/WebappStatus.java b/tomcat/src/main/java/io/javaoperatorsdk/operator/sample/WebappStatus.java
index 53e71fe..8267abe 100644
--- a/tomcat/src/main/java/io/javaoperatorsdk/operator/sample/WebappStatus.java
+++ b/tomcat/src/main/java/io/javaoperatorsdk/operator/sample/WebappStatus.java
@@ -11,4 +11,14 @@ public String getDeployedArtifact() {
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/tomcat/src/main/resources/log4j2.xml b/tomcat/src/main/resources/log4j2.xml
index 0e6991e..a99aaf3 100644
--- a/tomcat/src/main/resources/log4j2.xml
+++ b/tomcat/src/main/resources/log4j2.xml
@@ -2,7 +2,7 @@
-
+
diff --git a/tomcat/src/test/java/io/javaoperatorsdk/operator/sample/SimpleTest.java b/tomcat/src/test/java/io/javaoperatorsdk/operator/sample/SimpleTest.java
new file mode 100644
index 0000000..f3ecae0
--- /dev/null
+++ b/tomcat/src/test/java/io/javaoperatorsdk/operator/sample/SimpleTest.java
@@ -0,0 +1,12 @@
+package io.javaoperatorsdk.operator.sample;
+
+import org.junit.Assert;
+import org.junit.Test;
+// this is for reference
+// on target regular test should be set with io.fabric8.kubernetes.client.server.mock.KubernetesMockServer;
+public class SimpleTest {
+ @Test
+ public void test() {
+ Assert.assertSame("foo","foo");
+ }
+}
diff --git a/tomcat/src/test/java/io/javaoperatorsdk/operator/sample/SimpleWarIT.java b/tomcat/src/test/java/io/javaoperatorsdk/operator/sample/SimpleWarIT.java
new file mode 100644
index 0000000..319889b
--- /dev/null
+++ b/tomcat/src/test/java/io/javaoperatorsdk/operator/sample/SimpleWarIT.java
@@ -0,0 +1,120 @@
+package io.javaoperatorsdk.operator.sample;
+
+import io.fabric8.kubernetes.api.model.*;
+import io.fabric8.kubernetes.client.Config;
+import io.fabric8.kubernetes.client.ConfigBuilder;
+import io.fabric8.kubernetes.client.*;
+import io.fabric8.kubernetes.client.extended.run.RunConfigBuilder;
+import io.javaoperatorsdk.operator.Operator;
+import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService;
+import io.javaoperatorsdk.operator.test.IntegrationTest;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.notNullValue;
+
+@Category(IntegrationTest.class)
+public class SimpleWarIT {
+
+ final static String TEST_NS = "tomcat-test";
+
+ final static Logger log = LoggerFactory.getLogger(SimpleWarIT.class);
+
+ @Test
+ public void test() {
+ Config config = new ConfigBuilder().withNamespace(null).build();
+ KubernetesClient client = new DefaultKubernetesClient(config);
+
+ Operator operator = new Operator(client, DefaultConfigurationService.instance());
+ operator.register(new TomcatController(client));
+ operator.register(new WebappController(client));
+
+ 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 && client.namespaces().withName(TEST_NS).isReady() == true ) {
+ // 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 resources");
+ tomcatClient.inNamespace(TEST_NS).create(tomcat);
+ webappClient.inNamespace(TEST_NS).create(webapp1);
+
+ log.info("Waiting 2 minutes for Tomcat and Webapp CR statuses to be updated");
+ await().atMost(2, 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()));
+ });
+
+ log.info("Waiting 60 seconds for Tomcat to unpack the downloaded war");
+ // this delays is du to allows the tomcat to unpack
+ // kubectl -n tomcat-test -c war-downloader logs -l app=test-tomcat1
+ // Deployment of web application archive [/usr/local/tomcat/webapps/webapp1.war] has finished in [xxx] ms
+ try {
+ Thread.sleep(60*1000);
+ } catch (InterruptedException e) {
+ log.warn(e.getMessage(),e);
+ }
+
+ String url = "http://" + tomcat.getMetadata().getName() + "/" + webapp1.getSpec().getContextPath() + "/";
+ log.info("Starting curl Pod and waiting 2 minutes for GET of {} to return 200", url);
+ 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();
+ await().atMost(2, MINUTES).untilAsserted(() -> {
+ try {
+ //let's do som tries
+ String curlOutput = client.pods().inNamespace(TEST_NS).withName(curlPod.getMetadata().getName()).getLog();
+ assertThat(curlOutput, equalTo("200"));
+ } catch (KubernetesClientException ex) {
+ throw new AssertionError(ex);
+ }
+ });
+ }
+
+}
diff --git a/tomcat/src/test/java/sample/IntegrationTest.java b/tomcat/src/test/java/sample/IntegrationTest.java
deleted file mode 100644
index 3af8cd0..0000000
--- a/tomcat/src/test/java/sample/IntegrationTest.java
+++ /dev/null
@@ -1,71 +0,0 @@
-package sample;
-
-import io.fabric8.kubernetes.api.model.KubernetesResourceList;
-import io.fabric8.kubernetes.api.model.Namespace;
-import io.fabric8.kubernetes.api.model.NamespaceBuilder;
-import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
-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.fabric8.kubernetes.client.dsl.MixedOperation;
-import io.fabric8.kubernetes.client.dsl.Resource;
-import io.fabric8.kubernetes.client.utils.Serialization;
-import io.javaoperatorsdk.operator.Operator;
-import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService;
-import io.javaoperatorsdk.operator.sample.Tomcat;
-import io.javaoperatorsdk.operator.sample.TomcatController;
-import io.javaoperatorsdk.operator.sample.WebappController;
-import org.junit.Test;
-
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-
-import static java.util.concurrent.TimeUnit.SECONDS;
-import static org.awaitility.Awaitility.await;
-import static org.junit.Assert.*;
-
-public class IntegrationTest {
- @Test
- public void test() throws InterruptedException {
- Config config = new ConfigBuilder().withNamespace(null).build();
- KubernetesClient client = new DefaultKubernetesClient(config);
- Operator operator = new Operator(client, DefaultConfigurationService.instance());
-
- TomcatController tomcatController = new TomcatController(client);
- operator.register(tomcatController);
-
- operator.register(new WebappController(client));
-
- Tomcat tomcat = loadYaml(Tomcat.class, "tomcat-sample1.yaml");
-
- tomcat.getSpec().setReplicas(3);
- tomcat.getMetadata().setNamespace("tomcat-test");
-
- MixedOperation, Resource> tomcatClient = client.customResources(Tomcat.class);
-
- Namespace tt_ns = new NamespaceBuilder().withMetadata(new ObjectMetaBuilder().withName("tomcat-test").build()).build();
-
- client.namespaces().delete(tt_ns);
-
- await().atMost(300, SECONDS).until(() -> client.namespaces().withName("tomcat-test").get() == null);
-
- client.namespaces().createOrReplace(tt_ns);
-
- tomcatClient.inNamespace("tomcat-test").create(tomcat);
-
- await().atMost(60, SECONDS).until(() -> {
- Tomcat updatedTomcat = tomcatClient.inNamespace("tomcat-test").withName("test-tomcat1").get();
- return updatedTomcat.getStatus() != null && (int) updatedTomcat.getStatus().getReadyReplicas() == 3;
- });
- }
-
- private T loadYaml(Class clazz, String yaml) {
- try (InputStream is = new FileInputStream("k8s/" + yaml)) {
- return Serialization.unmarshal(is, clazz);
- } catch (IOException ex) {
- throw new IllegalStateException("Cannot find yaml on classpath: " + yaml);
- }
- }
-}
diff --git a/tomcat/src/test/resources/log4j2.xml b/tomcat/src/test/resources/log4j2.xml
new file mode 100644
index 0000000..a99aaf3
--- /dev/null
+++ b/tomcat/src/test/resources/log4j2.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/webserver/k8s/operator.yaml b/webserver/k8s/operator.yaml
index a6e56ef..926b2c3 100644
--- a/webserver/k8s/operator.yaml
+++ b/webserver/k8s/operator.yaml
@@ -72,5 +72,27 @@ rules:
- 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:
- '*'