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: - '*'