diff --git a/dev-example/test-vm.tpl.yaml b/dev-example/test-vm.tpl.yaml index b6aa65a..0a667d8 100644 --- a/dev-example/test-vm.tpl.yaml +++ b/dev-example/test-vm.tpl.yaml @@ -8,9 +8,9 @@ metadata: spec: image: - source: ghcr.io/mnlipp/org.jdrupes.vmoperator.runner.qemu-arch:latest +# source: ghcr.io/mnlipp/org.jdrupes.vmoperator.runner.qemu-arch:latest # source: registry.mnl.de/org/jdrupes/vm-operator/org.jdrupes.vmoperator.runner.qemu-arch:testing -# source: docker-registry.lan.mnl.de/vmoperator/org.jdrupes.vmoperator.runner.qemu-arch:latest + source: docker-registry.lan.mnl.de/vmoperator/org.jdrupes.vmoperator.runner.qemu-arch:feature-auto-login pullPolicy: Always permissions: diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerPod.ftl.yaml b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerPod.ftl.yaml index f000c70..7518ad3 100644 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerPod.ftl.yaml +++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerPod.ftl.yaml @@ -11,7 +11,7 @@ metadata: annotations: # Triggers update of config map mounted in pod # See https://ahmet.im/blog/kubernetes-secret-volumes-delay/ - vmrunner.jdrupes.org/cmVersion: "${ cm.metadata.resourceVersion }" + vmrunner.jdrupes.org/cmVersion: "${ configMapResourceVersion }" vmoperator.jdrupes.org/version: ${ managerVersion } ownerReferences: - apiVersion: ${ cr.apiVersion() } diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ConfigMapReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ConfigMapReconciler.java index 9c6dc3e..e136a31 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ConfigMapReconciler.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ConfigMapReconciler.java @@ -19,11 +19,17 @@ package org.jdrupes.vmoperator.manager; import com.google.gson.JsonObject; +import freemarker.template.AdapterTemplateModel; import freemarker.template.Configuration; import freemarker.template.TemplateException; +import freemarker.template.TemplateMethodModelEx; +import freemarker.template.TemplateModel; +import freemarker.template.TemplateModelException; +import freemarker.template.utility.DeepUnwrap; import io.kubernetes.client.custom.V1Patch; import io.kubernetes.client.openapi.ApiClient; import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.models.V1ObjectMeta; import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi; import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; import io.kubernetes.client.util.generic.dynamic.Dynamics; @@ -31,7 +37,11 @@ import io.kubernetes.client.util.generic.options.ListOptions; import io.kubernetes.client.util.generic.options.PatchOptions; import java.io.IOException; import java.io.StringWriter; +import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.logging.Logger; import org.jdrupes.vmoperator.common.K8s; import static org.jdrupes.vmoperator.manager.Constants.APP_NAME; @@ -66,48 +76,59 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; * * @param model the model * @param channel the channel - * @return the dynamic kubernetes object * @throws IOException Signals that an I/O exception has occurred. * @throws TemplateException the template exception * @throws ApiException the api exception */ - public Map reconcile(Map model, - VmChannel channel) + @SuppressWarnings("PMD.AvoidDuplicateLiterals") + public void reconcile(Map model, VmChannel channel) throws IOException, TemplateException, ApiException { // Combine template and data and parse result + model.put("adjustCloudInitMeta", adjustCloudInitMetaModel); var fmTemplate = fmConfig.getTemplate("runnerConfig.ftl.yaml"); StringWriter out = new StringWriter(); fmTemplate.process(model, out); // Avoid Yaml.load due to // https://github.com/kubernetes-client/java/issues/2741 - var mapDef = Dynamics.newFromYaml( + var newCm = Dynamics.newFromYaml( new Yaml(new SafeConstructor(new LoaderOptions())), out.toString()); // Maybe override logging.properties from reconciler configuration. DataPath. get(model, "reconciler", "loggingProperties") .ifPresent(props -> { - GsonPtr.to(mapDef.getRaw()).getAs(JsonObject.class, "data") + GsonPtr.to(newCm.getRaw()).getAs(JsonObject.class, "data") .get().addProperty("logging.properties", props); }); // Maybe override logging.properties from VM definition. DataPath. get(model, "cr", "spec", "loggingProperties") .ifPresent(props -> { - GsonPtr.to(mapDef.getRaw()).getAs(JsonObject.class, "data") + GsonPtr.to(newCm.getRaw()).getAs(JsonObject.class, "data") .get().addProperty("logging.properties", props); }); - // Get API + // Look for changes + var oldCm = channel + .associated(getClass(), DynamicKubernetesObject.class).orElse(null); + channel.setAssociated(getClass(), newCm); + if (oldCm != null && Objects.equals(oldCm.getRaw().get("data"), + newCm.getRaw().get("data"))) { + logger.finer(() -> "No changes in config map for " + + DataPath. get(model, "cr", "name").get()); + model.put("configMapResourceVersion", + oldCm.getMetadata().getResourceVersion()); + return; + } + + // Get API and update DynamicKubernetesApi cmApi = new DynamicKubernetesApi("", "v1", "configmaps", channel.client()); // Apply and maybe force pod update - var newState = K8s.apply(cmApi, mapDef, mapDef.getRaw().toString()); - maybeForceUpdate(channel.client(), newState); - @SuppressWarnings("unchecked") - var res = (Map) channel.client().getJSON().getGson() - .fromJson(newState.getRaw(), Map.class); - return res; + var updatedCm = K8s.apply(cmApi, newCm, newCm.getRaw().toString()); + maybeForceUpdate(channel.client(), updatedCm); + model.put("configMapResourceVersion", + updatedCm.getMetadata().getResourceVersion()); } /** @@ -153,4 +174,28 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; } } + private final TemplateMethodModelEx adjustCloudInitMetaModel + = new TemplateMethodModelEx() { + @Override + @SuppressWarnings("PMD.PreserveStackTrace") + public Object exec(@SuppressWarnings("rawtypes") List arguments) + throws TemplateModelException { + @SuppressWarnings("unchecked") + var res = new HashMap<>((Map) DeepUnwrap + .unwrap((TemplateModel) arguments.get(0))); + var metadata + = (V1ObjectMeta) ((AdapterTemplateModel) arguments.get(1)) + .getAdaptedObject(Object.class); + if (!res.containsKey("instance-id")) { + res.put("instance-id", + Optional.ofNullable(metadata.getGeneration()) + .map(s -> "v" + s).orElse("v1")); + } + if (!res.containsKey("local-hostname")) { + res.put("local-hostname", metadata.getName()); + } + return res; + } + }; + } diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciler.java index 8df5c88..700e068 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciler.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciler.java @@ -27,12 +27,9 @@ import freemarker.template.SimpleScalar; import freemarker.template.TemplateException; import freemarker.template.TemplateExceptionHandler; import freemarker.template.TemplateMethodModelEx; -import freemarker.template.TemplateModel; import freemarker.template.TemplateModelException; -import freemarker.template.utility.DeepUnwrap; import io.kubernetes.client.custom.Quantity; import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.openapi.models.V1ObjectMeta; import io.kubernetes.client.util.generic.options.ListOptions; import java.io.IOException; import java.lang.reflect.Modifier; @@ -226,13 +223,12 @@ public class Reconciler extends Component { // Create model for processing templates Map model = prepareModel(channel.client(), event.vmDefinition()); - var configMap = cmReconciler.reconcile(model, channel); + cmReconciler.reconcile(model, channel); // The remaining reconcilers depend only on changes of the spec part. if (!event.specChanged()) { return; } - model.put("cm", configMap); dsReconciler.reconcile(event, model, channel); // Manage (eventual) removal of stateful set. stsReconciler.reconcile(event, model, channel); @@ -279,7 +275,6 @@ public class Reconciler extends Component { model.put("parseQuantity", parseQuantityModel); model.put("formatMemory", formatMemoryModel); model.put("imageLocation", imgageLocationModel); - model.put("adjustCloudInitMeta", adjustCloudInitMetaModel); model.put("toJson", toJsonModel); return model; } @@ -422,30 +417,6 @@ public class Reconciler extends Component { } }; - private final TemplateMethodModelEx adjustCloudInitMetaModel - = new TemplateMethodModelEx() { - @Override - @SuppressWarnings("PMD.PreserveStackTrace") - public Object exec(@SuppressWarnings("rawtypes") List arguments) - throws TemplateModelException { - @SuppressWarnings("unchecked") - var res = new HashMap<>((Map) DeepUnwrap - .unwrap((TemplateModel) arguments.get(0))); - var metadata - = (V1ObjectMeta) ((AdapterTemplateModel) arguments.get(1)) - .getAdaptedObject(Object.class); - if (!res.containsKey("instance-id")) { - res.put("instance-id", - Optional.ofNullable(metadata.getResourceVersion()) - .map(s -> "v" + s).orElse("v1")); - } - if (!res.containsKey("local-hostname")) { - res.put("local-hostname", metadata.getName()); - } - return res; - } - }; - private final TemplateMethodModelEx toJsonModel = new TemplateMethodModelEx() { @Override