From 0e3bb88497ff040a50999c555304de880c974f74 Mon Sep 17 00:00:00 2001 From: "Michael N. Lipp" Date: Thu, 17 Aug 2023 13:38:19 +0200 Subject: [PATCH] Support additional metadata for services. --- deploy/crds/vms-crd.yaml | 32 +++++++++ dev-example/config.yaml | 4 ++ dev-example/test-vm.yaml | 5 ++ .../config-sample.yaml | 10 ++- .../jdrupes/vmoperator/manager/GsonPtr.java | 2 +- .../vmoperator/manager/Reconciler.java | 4 +- .../vmoperator/manager/ServiceReconciler.java | 67 ++++++++++++++++++- 7 files changed, 117 insertions(+), 7 deletions(-) diff --git a/deploy/crds/vms-crd.yaml b/deploy/crds/vms-crd.yaml index e5ae82f..ffda025 100644 --- a/deploy/crds/vms-crd.yaml +++ b/deploy/crds/vms-crd.yaml @@ -923,6 +923,38 @@ spec: type: string update: type: boolean + additionalServiceMetadata: + description: >- + Data to be merged with the additionalServiceMetadata + defined in the manager's configuration. Values + specified here override values from the manager's + configuration. If the value of a label or an annotation + is null, the property with the corresponding key is + deleted from the properties defined in the manager's + configuration. + type: object + properties: + labels: + description: >- + Map of string keys and values that can be + used to organize and categorize (scope and select) objects. + May match selectors of replication controllers and services. + More info: http://kubernetes.io/docs/user-guide/labels + type: object + additionalProperties: + type: string + nullable: true + annotations: + description: >- + Annotations is an unstructured key value + map stored with a resource that may be set by external + tools to store and retrieve arbitrary metadata. They + are not queryable and should be preserved when modifying + objects. More info: http://kubernetes.io/docs/user-guide/annotations + type: object + additionalProperties: + type: string + nullable: true vm: type: object description: Defines the VM. diff --git a/dev-example/config.yaml b/dev-example/config.yaml index 45a33bc..ee46781 100644 --- a/dev-example/config.yaml +++ b/dev-example/config.yaml @@ -5,3 +5,7 @@ namespace: vmop-dev runnerData: storageClassName: null + additionalServiceMetadata: + labels: + test1: remains + test2: deleted diff --git a/dev-example/test-vm.yaml b/dev-example/test-vm.yaml index 673dbcb..15fa7e7 100644 --- a/dev-example/test-vm.yaml +++ b/dev-example/test-vm.yaml @@ -11,6 +11,11 @@ spec: runnerTemplate: update: true + + additionalServiceMetadata: + labels: + test2: null + test3: added vm: state: Running diff --git a/org.jdrupes.vmoperator.manager/config-sample.yaml b/org.jdrupes.vmoperator.manager/config-sample.yaml index abc229f..1ab38ab 100644 --- a/org.jdrupes.vmoperator.manager/config-sample.yaml +++ b/org.jdrupes.vmoperator.manager/config-sample.yaml @@ -5,12 +5,20 @@ # Values used when creating the PVC for the runner's data runnerData: storageClassName: null + # Amount by which the current cpu count is devided when generating # the resource properties. cpuOvercommit: 2 + # Amount by which the current ram size is devided when generating # the resource properties. ramOvercommit: 1.5 - + + # Additional metadata (labels and annotations) to be merged + # into the service + # additionalServiceMetdata: + # labels: {} + # annotations: {} + # Only for development: # namespace: vmop-dev diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/GsonPtr.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/GsonPtr.java index 6d9070c..7d0f403 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/GsonPtr.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/GsonPtr.java @@ -52,7 +52,7 @@ public class GsonPtr { /** * Create a new instance pointing to the {@link JsonElement} * selected by the given selectors. If a selector of type - * {@link String} denoted a non-existant member of a + * {@link String} denotes a non-existant member of a * {@link JsonObject}, a new member (of type {@link JsonObject} * is added. * 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 e9dffa0..c8ff09b 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 @@ -96,9 +96,7 @@ public class Reconciler extends Component { public void onConfigurationUpdate(ConfigurationUpdate event) { event.structured(Components.manager(parent()).componentPath()) .ifPresent(c -> { - if (c.containsKey("runnerData")) { - config.put("runnerData", c.get("runnerData")); - } + config.putAll(c); }); } diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ServiceReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ServiceReconciler.java index 061ddcc..3a9ed98 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ServiceReconciler.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ServiceReconciler.java @@ -18,14 +18,20 @@ package org.jdrupes.vmoperator.manager; +import com.google.gson.JsonObject; import freemarker.template.Configuration; import freemarker.template.TemplateException; import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.models.V1APIService; +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; import java.io.IOException; import java.io.StringWriter; +import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.logging.Logger; import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; @@ -37,6 +43,11 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; @SuppressWarnings("PMD.DataflowAnomalyAnalysis") /* default */ class ServiceReconciler { + private static final String METADATA + = V1APIService.SERIALIZED_NAME_METADATA; + private static final String LABELS = V1ObjectMeta.SERIALIZED_NAME_LABELS; + private static final String ANNOTATIONS + = V1ObjectMeta.SERIALIZED_NAME_ANNOTATIONS; protected final Logger logger = Logger.getLogger(getClass().getName()); private final Configuration fmConfig; @@ -72,11 +83,63 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; fmTemplate.process(model, out); // Avoid Yaml.load due to // https://github.com/kubernetes-client/java/issues/2741 - var mapDef = Dynamics.newFromYaml( + var svcDef = Dynamics.newFromYaml( new Yaml(new SafeConstructor(new LoaderOptions())), out.toString()); + mergeMetadata(svcDef, model, channel); // Apply - K8s.apply(svcApi, mapDef, out.toString()); + K8s.apply(svcApi, svcDef, svcDef.getRaw().toString()); + } + + private void mergeMetadata(DynamicKubernetesObject svcDef, + Map model, VmChannel channel) { + // Get metadata from config + @SuppressWarnings("unchecked") + var asmData = Optional.of(model) + .map(m -> (Map) m.get("config")) + .map(c -> (Map) c.get("additionalServiceMetadata")) + .orElseGet(() -> new HashMap<>()); + var json = channel.client().getJSON(); + JsonObject cfgMeta + = json.deserialize(json.serialize(asmData), JsonObject.class); + + // Get metadata from VM definition + var vmMeta = GsonPtr.to(channel.vmDefinition()).to("spec") + .get(JsonObject.class, "additionalServiceMetadata") + .map(JsonObject::deepCopy).orElseGet(() -> new JsonObject()); + + // Merge Data from VM definition into config data + mergeReplace(GsonPtr.to(cfgMeta).to(LABELS).get(JsonObject.class), + GsonPtr.to(vmMeta).to(LABELS).get(JsonObject.class)); + mergeReplace( + GsonPtr.to(cfgMeta).to(ANNOTATIONS).get(JsonObject.class), + GsonPtr.to(vmMeta).to(ANNOTATIONS).get(JsonObject.class)); + + // Merge additional data into service definition + var svcMeta = GsonPtr.to(svcDef.getRaw()).to(METADATA); + mergeIfAbsent(svcMeta.to(LABELS).get(JsonObject.class), + GsonPtr.to(cfgMeta).to(LABELS).get(JsonObject.class)); + mergeIfAbsent(svcMeta.to(ANNOTATIONS).get(JsonObject.class), + GsonPtr.to(cfgMeta).to(ANNOTATIONS).get(JsonObject.class)); + } + + private void mergeReplace(JsonObject dest, JsonObject src) { + for (var e : src.entrySet()) { + if (e.getValue().isJsonNull()) { + dest.remove(e.getKey()); + continue; + } + dest.add(e.getKey(), e.getValue()); + } + } + + private void mergeIfAbsent(JsonObject dest, JsonObject src) { + for (var e : src.entrySet()) { + if (dest.has(e.getKey())) { + continue; + } + dest.add(e.getKey(), e.getValue()); + } } }