diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CrudKubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CrudKubernetesDependentResource.java new file mode 100644 index 0000000000..88e9bdc7ae --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CrudKubernetesDependentResource.java @@ -0,0 +1,17 @@ +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; + +/** + * Adaptor Class for standalone mode for resources that manages Create, Update and Delete + * + * @param Managed resource + * @param

Primary Resource + */ +public abstract class CrudKubernetesDependentResource + extends + KubernetesDependentResource implements Creator, Updater, Deleter

{ +} diff --git a/sample-operators/webpage/k8s/webpage.yaml b/sample-operators/webpage/k8s/webpage.yaml index 382da972cb..d59c67eee1 100644 --- a/sample-operators/webpage/k8s/webpage.yaml +++ b/sample-operators/webpage/k8s/webpage.yaml @@ -1,6 +1,8 @@ apiVersion: "sample.javaoperatorsdk/v1" kind: WebPage metadata: + labels: + low-level: "true" name: hellows spec: html: | @@ -9,6 +11,6 @@ spec: Hello Operator World - Hello World! + Hello World! diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPage.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPage.java index 6c10ffe3af..52ef6a938b 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPage.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPage.java @@ -14,4 +14,12 @@ public class WebPage extends CustomResource protected WebPageStatus initStatus() { return new WebPageStatus(); } + + @Override + public String toString() { + return "WebPage{" + + "spec=" + spec + + ", status=" + status + + '}'; + } } diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java index 768fd26a72..b61b34334e 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java @@ -1,6 +1,7 @@ package io.javaoperatorsdk.operator.sample; import java.io.IOException; +import java.util.Arrays; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,7 +27,11 @@ 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 WebPageReconciler(client)); + if (Arrays.stream(args).anyMatch(arg -> arg.equals("--classic"))) { + operator.register(new WebPageReconciler(client)); + } else { + operator.register(new WebPageReconcilerDependentResources(client)); + } operator.installShutdownHook(); operator.start(); diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index beef42599c..b2727e065b 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -9,125 +9,192 @@ import io.fabric8.kubernetes.api.model.*; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; -import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; -import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; -import static io.javaoperatorsdk.operator.ReconcilerUtils.loadYaml; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; -@ControllerConfiguration(finalizerName = NO_FINALIZER) +/** Shows how to implement reconciler using the low level api directly. */ +@ControllerConfiguration( + finalizerName = NO_FINALIZER, + labelSelector = WebPageReconciler.LOW_LEVEL_LABEL_KEY) public class WebPageReconciler implements Reconciler, ErrorStatusHandler, EventSourceInitializer { - private final Logger log = LoggerFactory.getLogger(getClass()); + public static final String LOW_LEVEL_LABEL_KEY = "low-level"; + public static final String INDEX_HTML = "index.html"; - private final KubernetesClient kubernetesClient; + private static final Logger log = LoggerFactory.getLogger(WebPageReconciler.class); - private KubernetesDependentResource configMapDR; - private KubernetesDependentResource deploymentDR; - private KubernetesDependentResource serviceDR; + private final KubernetesClient kubernetesClient; public WebPageReconciler(KubernetesClient kubernetesClient) { this.kubernetesClient = kubernetesClient; - createDependentResources(kubernetesClient); } + InformerEventSource configMapEventSource; + @Override public List prepareEventSources(EventSourceContext context) { - return List.of( - configMapDR.eventSource(context), - deploymentDR.eventSource(context), - serviceDR.eventSource(context)); + configMapEventSource = + new InformerEventSource<>(InformerConfiguration.from(context, ConfigMap.class) + .withLabelSelector(LOW_LEVEL_LABEL_KEY) + .build(), context); + var deploymentEventSource = + new InformerEventSource<>(InformerConfiguration.from(context, Deployment.class) + .withLabelSelector(LOW_LEVEL_LABEL_KEY) + .build(), context); + var serviceEventSource = + new InformerEventSource<>(InformerConfiguration.from(context, Service.class) + .withLabelSelector(LOW_LEVEL_LABEL_KEY) + .build(), context); + return List.of(configMapEventSource, deploymentEventSource, serviceEventSource); } @Override public UpdateControl reconcile(WebPage webPage, Context context) { + log.info("Reconciling web page: {}", webPage); if (webPage.getSpec().getHtml().contains("error")) { throw new ErrorSimulationException("Simulating error"); } + String ns = webPage.getMetadata().getNamespace(); + String configMapName = configMapName(webPage); + String deploymentName = deploymentName(webPage); + + + ConfigMap desiredHtmlConfigMap = makeDesiredHtmlConfigMap(ns, configMapName, webPage); + Deployment desiredDeployment = + makeDesiredDeployment(webPage, deploymentName, ns, configMapName); + Service desiredService = makeDesiredService(webPage, ns, desiredDeployment); + + var previousConfigMap = context.getSecondaryResource(ConfigMap.class).orElse(null); + if (!match(desiredHtmlConfigMap, previousConfigMap)) { + log.info( + "Creating or updating ConfigMap {} in {}", + desiredHtmlConfigMap.getMetadata().getName(), + ns); + kubernetesClient.configMaps().inNamespace(ns).createOrReplace(desiredHtmlConfigMap); + } - configMapDR.reconcile(webPage, context); - deploymentDR.reconcile(webPage, context); - serviceDR.reconcile(webPage, context); + var existingDeployment = context.getSecondaryResource(Deployment.class).orElse(null); + if (!match(desiredDeployment, existingDeployment)) { + log.info( + "Creating or updating Deployment {} in {}", + desiredDeployment.getMetadata().getName(), + ns); + kubernetesClient.apps().deployments().inNamespace(ns).createOrReplace(desiredDeployment); + } - WebPageStatus status = new WebPageStatus(); + var existingService = context.getSecondaryResource(Service.class).orElse(null); + if (!match(desiredService, existingService)) { + log.info( + "Creating or updating Deployment {} in {}", + desiredDeployment.getMetadata().getName(), + ns); + kubernetesClient.services().inNamespace(ns).createOrReplace(desiredService); + } + + if (previousConfigMap != null && !StringUtils.equals( + previousConfigMap.getData().get(INDEX_HTML), + desiredHtmlConfigMap.getData().get(INDEX_HTML))) { + log.info("Restarting pods because HTML has changed in {}", ns); + kubernetesClient.pods().inNamespace(ns).withLabel("app", deploymentName(webPage)).delete(); + } + webPage.setStatus(createStatus(desiredHtmlConfigMap.getMetadata().getName())); + return UpdateControl.updateStatus(webPage); + } - status.setHtmlConfigMap(configMapDR.getResource(webPage).orElseThrow().getMetadata().getName()); + private WebPageStatus createStatus(String configMapName) { + WebPageStatus status = new WebPageStatus(); + status.setHtmlConfigMap(configMapName); status.setAreWeGood("Yes!"); status.setErrorMessage(null); - webPage.setStatus(status); + return status; + } - return UpdateControl.updateStatus(webPage); + private boolean match(Deployment desiredDeployment, Deployment deployment) { + if (deployment == null) { + return false; + } else { + return desiredDeployment.getSpec().getReplicas().equals(deployment.getSpec().getReplicas()) && + desiredDeployment.getSpec().getTemplate().getSpec().getContainers().get(0).getImage() + .equals( + deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getImage()); + } } - @Override - public Optional updateErrorStatus( - WebPage resource, RetryInfo retryInfo, RuntimeException e) { - resource.getStatus().setErrorMessage("Error: " + e.getMessage()); - return Optional.of(resource); + private boolean match(Service desiredService, Service service) { + if (service == null) { + return false; + } + return desiredService.getSpec().getSelector().equals(service.getSpec().getSelector()); } - private void createDependentResources(KubernetesClient client) { - this.configMapDR = new ConfigMapDependentResource(); - - this.deploymentDR = - new KubernetesDependentResource<>() { - - @Override - protected Deployment desired(WebPage webPage, Context context) { - var deploymentName = deploymentName(webPage); - Deployment deployment = loadYaml(Deployment.class, getClass(), "deployment.yaml"); - deployment.getMetadata().setName(deploymentName); - deployment.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); - deployment.getSpec().getSelector().getMatchLabels().put("app", deploymentName); - - deployment - .getSpec() - .getTemplate() - .getMetadata() - .getLabels() - .put("app", deploymentName); - deployment - .getSpec() - .getTemplate() - .getSpec() - .getVolumes() - .get(0) - .setConfigMap( - new ConfigMapVolumeSourceBuilder().withName(configMapName(webPage)).build()); - return deployment; - } - - @Override - protected Class resourceType() { - return Deployment.class; - } - }; - - this.serviceDR = - new KubernetesDependentResource<>() { - - @Override - protected Service desired(WebPage webPage, Context context) { - Service service = loadYaml(Service.class, getClass(), "service.yaml"); - service.getMetadata().setName(serviceName(webPage)); - service.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); - Map labels = new HashMap<>(); - labels.put("app", deploymentName(webPage)); - service.getSpec().setSelector(labels); - return service; - } - - @Override - protected Class resourceType() { - return Service.class; - } - }; + private boolean match(ConfigMap desiredHtmlConfigMap, ConfigMap existingConfigMap) { + if (existingConfigMap == null) { + return false; + } else { + return desiredHtmlConfigMap.getData().equals(existingConfigMap.getData()); + } + } + + private Service makeDesiredService(WebPage webPage, String ns, Deployment desiredDeployment) { + Service desiredService = ReconcilerUtils.loadYaml(Service.class, getClass(), "service.yaml"); + desiredService.getMetadata().setName(serviceName(webPage)); + desiredService.getMetadata().setNamespace(ns); + desiredService.getMetadata().setLabels(lowLevelLabel()); + desiredService + .getSpec() + .setSelector(desiredDeployment.getSpec().getTemplate().getMetadata().getLabels()); + desiredService.addOwnerReference(webPage); + return desiredService; + } + + private Deployment makeDesiredDeployment(WebPage webPage, String deploymentName, String ns, + String configMapName) { + Deployment desiredDeployment = + ReconcilerUtils.loadYaml(Deployment.class, getClass(), "deployment.yaml"); + desiredDeployment.getMetadata().setName(deploymentName); + desiredDeployment.getMetadata().setNamespace(ns); + desiredDeployment.getMetadata().setLabels(lowLevelLabel()); + desiredDeployment.getSpec().getSelector().getMatchLabels().put("app", deploymentName); + desiredDeployment.getSpec().getTemplate().getMetadata().getLabels().put("app", deploymentName); + desiredDeployment + .getSpec() + .getTemplate() + .getSpec() + .getVolumes() + .get(0) + .setConfigMap(new ConfigMapVolumeSourceBuilder().withName(configMapName).build()); + desiredDeployment.addOwnerReference(webPage); + return desiredDeployment; + } + + private ConfigMap makeDesiredHtmlConfigMap(String ns, String configMapName, WebPage webPage) { + Map data = new HashMap<>(); + data.put("index.html", webPage.getSpec().getHtml()); + ConfigMap configMap = + new ConfigMapBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName(configMapName) + .withNamespace(ns) + .withLabels(lowLevelLabel()) + .build()) + .withData(data) + .build(); + configMap.addOwnerReference(webPage); + return configMap; + } + + private Map lowLevelLabel() { + Map labels = new HashMap<>(); + labels.put(LOW_LEVEL_LABEL_KEY, "true"); + return labels; } private static String configMapName(WebPage nginx) { @@ -142,45 +209,10 @@ private static String serviceName(WebPage nginx) { return nginx.getMetadata().getName(); } - private class ConfigMapDependentResource extends KubernetesDependentResource - implements - AssociatedSecondaryResourceIdentifier, Updater { - - @Override - protected ConfigMap desired(WebPage webPage, Context context) { - Map data = new HashMap<>(); - data.put("index.html", webPage.getSpec().getHtml()); - return new ConfigMapBuilder() - .withMetadata( - new ObjectMetaBuilder() - .withName(WebPageReconciler.configMapName(webPage)) - .withNamespace(webPage.getMetadata().getNamespace()) - .build()) - .withData(data) - .build(); - } - - @Override - public boolean match(ConfigMap actual, ConfigMap target, Context context) { - return StringUtils.equals( - actual.getData().get("index.html"), target.getData().get("index.html")); - } - - @Override - public void update(ConfigMap actual, ConfigMap target, WebPage primary, Context context) { - super.update(actual, target, primary, context); - var ns = actual.getMetadata().getNamespace(); - log.info("Restarting pods because HTML has changed in {}", ns); - kubernetesClient - .pods() - .inNamespace(ns) - .withLabel("app", deploymentName(primary)) - .delete(); - } - - @Override - public ResourceID associatedSecondaryID(WebPage primary) { - return new ResourceID(configMapName(primary), primary.getMetadata().getNamespace()); - } + @Override + public Optional updateErrorStatus( + WebPage resource, RetryInfo retryInfo, RuntimeException e) { + resource.getStatus().setErrorMessage("Error: " + e.getMessage()); + return Optional.of(resource); } } diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java new file mode 100644 index 0000000000..a313a0bf3d --- /dev/null +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java @@ -0,0 +1,199 @@ +package io.javaoperatorsdk.operator.sample; + +import java.util.*; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.*; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CrudKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; + +import static io.javaoperatorsdk.operator.ReconcilerUtils.loadYaml; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; + +/** + * Shows how to implement reconciler using standalone dependent resources. + */ +@ControllerConfiguration(finalizerName = NO_FINALIZER, + labelSelector = WebPageReconcilerDependentResources.DEPENDENT_RESOURCE_LABEL_SELECTOR) +public class WebPageReconcilerDependentResources + implements Reconciler, ErrorStatusHandler, EventSourceInitializer { + + public static final String DEPENDENT_RESOURCE_LABEL_SELECTOR = "!low-level"; + private static final Logger log = + LoggerFactory.getLogger(WebPageReconcilerDependentResources.class); + private final KubernetesClient kubernetesClient; + + private KubernetesDependentResource configMapDR; + private KubernetesDependentResource deploymentDR; + private KubernetesDependentResource serviceDR; + + public WebPageReconcilerDependentResources(KubernetesClient kubernetesClient) { + this.kubernetesClient = kubernetesClient; + createDependentResources(kubernetesClient); + } + + @Override + public List prepareEventSources(EventSourceContext context) { + return List.of( + configMapDR.eventSource(context), + deploymentDR.eventSource(context), + serviceDR.eventSource(context)); + } + + @Override + public UpdateControl reconcile(WebPage webPage, Context context) { + if (webPage.getSpec().getHtml().contains("error")) { + // special case just to showcase error if doing a demo + throw new ErrorSimulationException("Simulating error"); + } + + configMapDR.reconcile(webPage, context); + deploymentDR.reconcile(webPage, context); + serviceDR.reconcile(webPage, context); + + webPage.setStatus( + createStatus(configMapDR.getResource(webPage).orElseThrow().getMetadata().getName())); + return UpdateControl.updateStatus(webPage); + } + + private WebPageStatus createStatus(String configMapName) { + WebPageStatus status = new WebPageStatus(); + status.setHtmlConfigMap(configMapName); + status.setAreWeGood("Yes!"); + status.setErrorMessage(null); + return status; + } + + @Override + public Optional updateErrorStatus( + WebPage resource, RetryInfo retryInfo, RuntimeException e) { + resource.getStatus().setErrorMessage("Error: " + e.getMessage()); + return Optional.of(resource); + } + + private void createDependentResources(KubernetesClient client) { + this.configMapDR = new ConfigMapDependentResource(); + this.configMapDR.setKubernetesClient(client); + configMapDR.configureWith(new KubernetesDependentResourceConfig() + .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); + + this.deploymentDR = + new CrudKubernetesDependentResource<>() { + + @Override + protected Deployment desired(WebPage webPage, Context context) { + var deploymentName = deploymentName(webPage); + Deployment deployment = loadYaml(Deployment.class, getClass(), "deployment.yaml"); + deployment.getMetadata().setName(deploymentName); + deployment.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); + deployment.getSpec().getSelector().getMatchLabels().put("app", deploymentName); + + deployment + .getSpec() + .getTemplate() + .getMetadata() + .getLabels() + .put("app", deploymentName); + deployment + .getSpec() + .getTemplate() + .getSpec() + .getVolumes() + .get(0) + .setConfigMap( + new ConfigMapVolumeSourceBuilder().withName(configMapName(webPage)).build()); + return deployment; + } + + @Override + protected Class resourceType() { + return Deployment.class; + } + }; + deploymentDR.setKubernetesClient(client); + deploymentDR.configureWith(new KubernetesDependentResourceConfig() + .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); + + this.serviceDR = + new CrudKubernetesDependentResource<>() { + + @Override + protected Service desired(WebPage webPage, Context context) { + Service service = loadYaml(Service.class, getClass(), "service.yaml"); + service.getMetadata().setName(serviceName(webPage)); + service.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); + Map labels = new HashMap<>(); + labels.put("app", deploymentName(webPage)); + service.getSpec().setSelector(labels); + return service; + } + + @Override + protected Class resourceType() { + return Service.class; + } + }; + serviceDR.setKubernetesClient(client); + serviceDR.configureWith(new KubernetesDependentResourceConfig() + .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); + } + + private static String configMapName(WebPage nginx) { + return nginx.getMetadata().getName() + "-html"; + } + + private static String deploymentName(WebPage nginx) { + return nginx.getMetadata().getName(); + } + + private static String serviceName(WebPage nginx) { + return nginx.getMetadata().getName(); + } + + private class ConfigMapDependentResource + extends CrudKubernetesDependentResource + implements + AssociatedSecondaryResourceIdentifier { + + @Override + protected ConfigMap desired(WebPage webPage, Context context) { + Map data = new HashMap<>(); + data.put("index.html", webPage.getSpec().getHtml()); + return new ConfigMapBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName(WebPageReconcilerDependentResources.configMapName(webPage)) + .withNamespace(webPage.getMetadata().getNamespace()) + .build()) + .withData(data) + .build(); + } + + @Override + public void update(ConfigMap actual, ConfigMap target, WebPage primary, Context context) { + super.update(actual, target, primary, context); + var ns = actual.getMetadata().getNamespace(); + log.info("Restarting pods because HTML has changed in {}", ns); + kubernetesClient + .pods() + .inNamespace(ns) + .withLabel("app", deploymentName(primary)) + .delete(); + } + + @Override + public ResourceID associatedSecondaryID(WebPage primary) { + return new ResourceID(configMapName(primary), primary.getMetadata().getNamespace()); + } + } +} diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageSpec.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageSpec.java index db50be6c2d..562cda291d 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageSpec.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageSpec.java @@ -11,4 +11,11 @@ public String getHtml() { public void setHtml(String html) { this.html = html; } + + @Override + public String toString() { + return "WebPageSpec{" + + "html='" + html + '\'' + + '}'; + } } diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java index 2b2e2a23c8..3750e2f7c7 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java @@ -34,4 +34,13 @@ public WebPageStatus setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; return this; } + + @Override + public String toString() { + return "WebPageStatus{" + + "htmlConfigMap='" + htmlConfigMap + '\'' + + ", areWeGood='" + areWeGood + '\'' + + ", errorMessage='" + errorMessage + '\'' + + '}'; + } }