Support additional metadata for services.

This commit is contained in:
Michael Lipp 2023-08-17 13:38:19 +02:00
parent 477db06f8d
commit 0e3bb88497
7 changed files with 117 additions and 7 deletions

View file

@ -923,6 +923,38 @@ spec:
type: string type: string
update: update:
type: boolean 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: vm:
type: object type: object
description: Defines the VM. description: Defines the VM.

View file

@ -5,3 +5,7 @@
namespace: vmop-dev namespace: vmop-dev
runnerData: runnerData:
storageClassName: null storageClassName: null
additionalServiceMetadata:
labels:
test1: remains
test2: deleted

View file

@ -11,6 +11,11 @@ spec:
runnerTemplate: runnerTemplate:
update: true update: true
additionalServiceMetadata:
labels:
test2: null
test3: added
vm: vm:
state: Running state: Running

View file

@ -5,12 +5,20 @@
# Values used when creating the PVC for the runner's data # Values used when creating the PVC for the runner's data
runnerData: runnerData:
storageClassName: null storageClassName: null
# Amount by which the current cpu count is devided when generating # Amount by which the current cpu count is devided when generating
# the resource properties. # the resource properties.
cpuOvercommit: 2 cpuOvercommit: 2
# Amount by which the current ram size is devided when generating # Amount by which the current ram size is devided when generating
# the resource properties. # the resource properties.
ramOvercommit: 1.5 ramOvercommit: 1.5
# Additional metadata (labels and annotations) to be merged
# into the service
# additionalServiceMetdata:
# labels: {}
# annotations: {}
# Only for development: # Only for development:
# namespace: vmop-dev # namespace: vmop-dev

View file

@ -52,7 +52,7 @@ public class GsonPtr {
/** /**
* Create a new instance pointing to the {@link JsonElement} * Create a new instance pointing to the {@link JsonElement}
* selected by the given selectors. If a selector of type * 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} * {@link JsonObject}, a new member (of type {@link JsonObject}
* is added. * is added.
* *

View file

@ -96,9 +96,7 @@ public class Reconciler extends Component {
public void onConfigurationUpdate(ConfigurationUpdate event) { public void onConfigurationUpdate(ConfigurationUpdate event) {
event.structured(Components.manager(parent()).componentPath()) event.structured(Components.manager(parent()).componentPath())
.ifPresent(c -> { .ifPresent(c -> {
if (c.containsKey("runnerData")) { config.putAll(c);
config.put("runnerData", c.get("runnerData"));
}
}); });
} }

View file

@ -18,14 +18,20 @@
package org.jdrupes.vmoperator.manager; package org.jdrupes.vmoperator.manager;
import com.google.gson.JsonObject;
import freemarker.template.Configuration; import freemarker.template.Configuration;
import freemarker.template.TemplateException; import freemarker.template.TemplateException;
import io.kubernetes.client.openapi.ApiException; 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.DynamicKubernetesApi;
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
import io.kubernetes.client.util.generic.dynamic.Dynamics; import io.kubernetes.client.util.generic.dynamic.Dynamics;
import java.io.IOException; import java.io.IOException;
import java.io.StringWriter; import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.logging.Logger; import java.util.logging.Logger;
import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.Yaml;
@ -37,6 +43,11 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
@SuppressWarnings("PMD.DataflowAnomalyAnalysis") @SuppressWarnings("PMD.DataflowAnomalyAnalysis")
/* default */ class ServiceReconciler { /* 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()); protected final Logger logger = Logger.getLogger(getClass().getName());
private final Configuration fmConfig; private final Configuration fmConfig;
@ -72,11 +83,63 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
fmTemplate.process(model, out); fmTemplate.process(model, out);
// Avoid Yaml.load due to // Avoid Yaml.load due to
// https://github.com/kubernetes-client/java/issues/2741 // https://github.com/kubernetes-client/java/issues/2741
var mapDef = Dynamics.newFromYaml( var svcDef = Dynamics.newFromYaml(
new Yaml(new SafeConstructor(new LoaderOptions())), out.toString()); new Yaml(new SafeConstructor(new LoaderOptions())), out.toString());
mergeMetadata(svcDef, model, channel);
// Apply // Apply
K8s.apply(svcApi, mapDef, out.toString()); K8s.apply(svcApi, svcDef, svcDef.getRaw().toString());
}
private void mergeMetadata(DynamicKubernetesObject svcDef,
Map<String, Object> model, VmChannel channel) {
// Get metadata from config
@SuppressWarnings("unchecked")
var asmData = Optional.of(model)
.map(m -> (Map<String, Object>) m.get("config"))
.map(c -> (Map<String, Object>) 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());
}
} }
} }