Skip to content

Refactor WebPage Sample #976

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Feb 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 <R> Managed resource
* @param <P> Primary Resource
*/
public abstract class CrudKubernetesDependentResource<R extends HasMetadata, P extends HasMetadata>
extends
KubernetesDependentResource<R, P> implements Creator<R, P>, Updater<R, P>, Deleter<P> {
}
4 changes: 3 additions & 1 deletion sample-operators/webpage/k8s/webpage.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
apiVersion: "sample.javaoperatorsdk/v1"
kind: WebPage
metadata:
labels:
low-level: "true"
name: hellows
spec:
html: |
Expand All @@ -9,6 +11,6 @@ spec:
<title>Hello Operator World</title>
</head>
<body>
Hello World!
Hello World!
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,12 @@ public class WebPage extends CustomResource<WebPageSpec, WebPageStatus>
protected WebPageStatus initStatus() {
return new WebPageStatus();
}

@Override
public String toString() {
return "WebPage{" +
"spec=" + spec +
", status=" + status +
'}';
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<WebPage>, ErrorStatusHandler<WebPage>, EventSourceInitializer<WebPage> {

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<ConfigMap, WebPage> configMapDR;
private KubernetesDependentResource<Deployment, WebPage> deploymentDR;
private KubernetesDependentResource<Service, WebPage> serviceDR;
private final KubernetesClient kubernetesClient;

public WebPageReconciler(KubernetesClient kubernetesClient) {
this.kubernetesClient = kubernetesClient;
createDependentResources(kubernetesClient);
}

InformerEventSource configMapEventSource;

@Override
public List<EventSource> prepareEventSources(EventSourceContext<WebPage> 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<WebPage> 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<WebPage> 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<Deployment> 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<String, String> labels = new HashMap<>();
labels.put("app", deploymentName(webPage));
service.getSpec().setSelector(labels);
return service;
}

@Override
protected Class<Service> 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<String, String> 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<String, String> lowLevelLabel() {
Map<String, String> labels = new HashMap<>();
labels.put(LOW_LEVEL_LABEL_KEY, "true");
return labels;
}

private static String configMapName(WebPage nginx) {
Expand All @@ -142,45 +209,10 @@ private static String serviceName(WebPage nginx) {
return nginx.getMetadata().getName();
}

private class ConfigMapDependentResource extends KubernetesDependentResource<ConfigMap, WebPage>
implements
AssociatedSecondaryResourceIdentifier<WebPage>, Updater<ConfigMap, WebPage> {

@Override
protected ConfigMap desired(WebPage webPage, Context context) {
Map<String, String> 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<WebPage> updateErrorStatus(
WebPage resource, RetryInfo retryInfo, RuntimeException e) {
resource.getStatus().setErrorMessage("Error: " + e.getMessage());
return Optional.of(resource);
}
}
Loading