From 4058fa6bda44b0db2e91cf419c6931623fcd8c5b Mon Sep 17 00:00:00 2001 From: "Michael N. Lipp" Date: Wed, 9 Aug 2023 15:19:29 +0200 Subject: [PATCH] Use statefulset for runner. --- deploy/crds/vms-crd.yaml | 27 ---- .../vmoperator/manager/runnerConfig.ftl.yaml | 6 +- .../vmoperator/manager/runnerDataPvc.ftl.yaml | 16 --- .../vmoperator/manager/runnerPod.ftl.yaml | 94 ------------- .../vmoperator/manager/runnerSts.ftl.yaml | 126 +++++++++++++++++ .../vmoperator/manager/DataReconciler.java | 86 ------------ .../vmoperator/manager/DisksReconciler.java | 128 ------------------ .../vmoperator/manager/Reconciler.java | 15 +- ...{PodReconciler.java => StsReconciler.java} | 68 ++++------ .../jdrupes/vmoperator/manager/VmWatcher.java | 39 ++++-- 10 files changed, 185 insertions(+), 420 deletions(-) delete mode 100644 org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerDataPvc.ftl.yaml delete mode 100644 org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerPod.ftl.yaml create mode 100644 org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerSts.ftl.yaml delete mode 100644 org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DataReconciler.java delete mode 100644 org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisksReconciler.java rename org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/{PodReconciler.java => StsReconciler.java} (54%) diff --git a/deploy/crds/vms-crd.yaml b/deploy/crds/vms-crd.yaml index 11240c8..834c2cd 100644 --- a/deploy/crds/vms-crd.yaml +++ b/deploy/crds/vms-crd.yaml @@ -178,39 +178,12 @@ spec: (for whatever reason) is to use a label selector alongside manually created PersistentVolumes. properties: - apiVersion: - description: >- - APIVersion defines the versioned schema of this - representation of an object. Servers should convert recognized - schemas to the latest internal value, and may reject unrecognized - values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - default: v1 - kind: - description: >- - Kind is a string value representing the REST - resource this object represents. Servers may infer this - from the endpoint the client submits requests to. Cannot - be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - default: PersistentVolumeClaim metadata: description: >- EmbeddedMetadata contains metadata relevant to an EmbeddedResource. type: object properties: - namespace: - description: >- - Namespace defines the space within which each - name must be unique. An empty namespace is equivalent to the - "default" namespace, but "default" is the canonical - representation. Not all objects are required to be scoped - to a namespace - the value of this field for those objects - will be empty. Must be a DNS_LABEL. Cannot be updated. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces. - The default value is the VM's namespace. - type: string name: description: >- Name must be unique within a namespace. diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml index 9c95635..ccd70d5 100644 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml +++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml @@ -139,13 +139,13 @@ data: <#if disk.volumeClaimTemplate?? && disk.volumeClaimTemplate.metadata?? && disk.volumeClaimTemplate.metadata.name??> - <#assign name = disk.volumeClaimTemplate.metadata.name.asString> + <#assign diskName = disk.volumeClaimTemplate.metadata.name.asString + "-disk"> <#else> - <#assign name = "" + drvCounter> + <#assign diskName = "disk-" + drvCounter> <#if disk.volumeClaimTemplate??> - type: raw - resource: /dev/disk-${ name } + resource: /dev/${ diskName } <#if disk.bootindex??> bootindex: ${ disk.bootindex.asInt?c } diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerDataPvc.ftl.yaml b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerDataPvc.ftl.yaml deleted file mode 100644 index a436d8f..0000000 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerDataPvc.ftl.yaml +++ /dev/null @@ -1,16 +0,0 @@ -kind: PersistentVolumeClaim -apiVersion: v1 -metadata: - namespace: ${ cr.metadata.namespace.asString } - name: ${ cr.metadata.name.asString + "-runner-data" } - labels: - app.kubernetes.io/name: ${ constants.APP_NAME } - app.kubernetes.io/instance: ${ cr.metadata.name.asString } - app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME } - -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 1Mi 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 deleted file mode 100644 index 92beea0..0000000 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerPod.ftl.yaml +++ /dev/null @@ -1,94 +0,0 @@ -kind: Pod -apiVersion: v1 -metadata: - namespace: ${ cr.metadata.namespace.asString } - name: ${ cr.metadata.name.asString } - labels: - app.kubernetes.io/name: ${ constants.APP_NAME } - app.kubernetes.io/instance: ${ cr.metadata.name.asString } - app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME } - 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.asString }" - -spec: - containers: - - name: ${ cr.metadata.name.asString } - <#assign image = cr.spec.image> - image: ${ image.repository.asString }/${ image.path.asString }:${ image.version.asString } - resources: {} - imagePullPolicy: ${ image.pullPolicy.asString } - volumeMounts: - # Not needed because pod is priviledged: - # - mountPath: /dev/kvm - # name: dev-kvm - # - mountPath: /dev/net/tun - # name: dev-tun - # - mountPath: /sys/fs/cgroup - # name: cgroup - - name: config - mountPath: /etc/opt/vmrunner - - name: vm-data - mountPath: /var/local/vm-data - - name: vmop-image-repository - mountPath: ${ constants.IMAGE_REPO_PATH } - volumeDevices: - <#assign diskCounter = 0/> - <#list cr.spec.vm.disks.asList() as disk> - <#if disk.volumeClaimTemplate??> - <#if disk.volumeClaimTemplate.metadata?? - && disk.volumeClaimTemplate.metadata.name??> - <#assign diskName = "disk-" + disk.volumeClaimTemplate.metadata.name.asString> - <#else> - <#assign diskName = "disk-" + diskCounter> - - - name: ${ diskName } - devicePath: /dev/${ diskName } - <#assign diskCounter = diskCounter + 1/> - - - securityContext: - privileged: true - volumes: - # Not needed because pod is priviledged: - # - name: dev-kvm - # hostPath: - # path: /dev/kvm - # type: CharDevice - # - hostPath: - # path: /dev/net/tun - # type: CharDevice - # name: dev-tun - # - name: cgroup - # hostPath: - # path: /sys/fs/cgroup - - name: config - configMap: - name: ${ cr.metadata.name.asString } - - name: vm-data - persistentVolumeClaim: - claimName: ${ cr.metadata.name.asString }-runner-data - - name: vmop-image-repository - persistentVolumeClaim: - claimName: vmop-image-repository - <#assign diskCounter = 0/> - <#list cr.spec.vm.disks.asList() as disk> - <#if disk.volumeClaimTemplate??> - <#if disk.volumeClaimTemplate.metadata?? - && disk.volumeClaimTemplate.metadata.name??> - <#assign claimName = disk.volumeClaimTemplate.metadata.name.asString> - <#assign diskName = "disk-" + claimName> - <#else> - <#assign claimName = cr.metadata.name.asString + "-disk-" + diskCounter> - <#assign diskName = "disk-" + diskCounter> - - - name: ${ diskName } - persistentVolumeClaim: - claimName: ${ claimName } - <#assign diskCounter = diskCounter + 1/> - - - hostNetwork: true - terminationGracePeriodSeconds: ${ (cr.spec.vm.powerdownTimeout.asInt + 5)?c } - restartPolicy: Never diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerSts.ftl.yaml b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerSts.ftl.yaml new file mode 100644 index 0000000..963edce --- /dev/null +++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerSts.ftl.yaml @@ -0,0 +1,126 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + namespace: ${ cr.metadata.namespace.asString } + name: ${ cr.metadata.name.asString } + labels: + app.kubernetes.io/name: ${ constants.APP_NAME } + app.kubernetes.io/instance: ${ cr.metadata.name.asString } + app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME } +spec: + selector: + matchLabels: + app.kubernetes.io/name: ${ constants.APP_NAME } + app.kubernetes.io/instance: ${ cr.metadata.name.asString } + replicas: ${ (cr.spec.vm.state.asString == "Running")?then(1, 0) } + template: + metadata: + namespace: ${ cr.metadata.namespace.asString } + name: ${ cr.metadata.name.asString } + labels: + app.kubernetes.io/name: ${ constants.APP_NAME } + app.kubernetes.io/instance: ${ cr.metadata.name.asString } + app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME } + 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.asString }" + spec: + containers: + - name: ${ cr.metadata.name.asString } + <#assign image = cr.spec.image> + image: ${ image.repository.asString }/${ image.path.asString }:${ image.version.asString } + resources: {} + imagePullPolicy: ${ image.pullPolicy.asString } + volumeMounts: + # Not needed because pod is priviledged: + # - mountPath: /dev/kvm + # name: dev-kvm + # - mountPath: /dev/net/tun + # name: dev-tun + # - mountPath: /sys/fs/cgroup + # name: cgroup + - name: config + mountPath: /etc/opt/vmrunner + - name: runner-data + mountPath: /var/local/vm-data + - name: vmop-image-repository + mountPath: ${ constants.IMAGE_REPO_PATH } + volumeDevices: + <#assign diskCounter = 0/> + <#list cr.spec.vm.disks.asList() as disk> + <#if disk.volumeClaimTemplate??> + <#if disk.volumeClaimTemplate.metadata?? + && disk.volumeClaimTemplate.metadata.name??> + <#assign diskName = disk.volumeClaimTemplate.metadata.name.asString + "-disk"> + <#else> + <#assign diskName = "disk-" + diskCounter> + + - name: ${ diskName } + devicePath: /dev/${ diskName } + <#assign diskCounter = diskCounter + 1/> + + + securityContext: + privileged: true + volumes: + # Not needed because pod is priviledged: + # - name: dev-kvm + # hostPath: + # path: /dev/kvm + # type: CharDevice + # - hostPath: + # path: /dev/net/tun + # type: CharDevice + # name: dev-tun + # - name: cgroup + # hostPath: + # path: /sys/fs/cgroup + - name: config + configMap: + name: ${ cr.metadata.name.asString } + - name: vmop-image-repository + persistentVolumeClaim: + claimName: vmop-image-repository + hostNetwork: true + terminationGracePeriodSeconds: ${ (cr.spec.vm.powerdownTimeout.asInt + 5)?c } + volumeClaimTemplates: + - metadata: + namespace: ${ cr.metadata.namespace.asString } + name: runner-data + labels: + app.kubernetes.io/name: ${ constants.APP_NAME } + app.kubernetes.io/instance: ${ cr.metadata.name.asString } + app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME } + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Mi + <#assign diskCounter = 0/> + <#list cr.spec.vm.disks.asList() as disk> + <#if disk.volumeClaimTemplate??> + <#if disk.volumeClaimTemplate.metadata?? + && disk.volumeClaimTemplate.metadata.name??> + <#assign diskName = disk.volumeClaimTemplate.metadata.name.asString + "-disk"> + <#else> + <#assign diskName = "disk-" + diskCounter> + + - metadata: + namespace: ${ cr.metadata.namespace.asString } + name: ${ diskName } + labels: + app.kubernetes.io/name: ${ constants.APP_NAME } + app.kubernetes.io/instance: ${ cr.metadata.name.asString } + app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME } + <#if disk.volumeClaimTemplate.metadata?? + && disk.volumeClaimTemplate.metadata.annotations??> + annotations: + ${ disk.volumeClaimTemplate.metadata.annotations.toString() } + + spec: + ${ disk.volumeClaimTemplate.spec.toString() } + <#assign diskCounter = diskCounter + 1/> + + diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DataReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DataReconciler.java deleted file mode 100644 index 75f4486..0000000 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DataReconciler.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -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.util.generic.dynamic.DynamicKubernetesApi; -import io.kubernetes.client.util.generic.dynamic.Dynamics; -import java.io.IOException; -import java.io.StringWriter; -import java.util.Map; - -/** - * Delegee for reconciling the data PVC - */ -@SuppressWarnings("PMD.DataflowAnomalyAnalysis") -/* default */ class DataReconciler { - - private final Configuration fmConfig; - - /** - * Instantiates a new config map reconciler. - * - * @param fmConfig the fm config - */ - public DataReconciler(Configuration fmConfig) { - this.fmConfig = fmConfig; - } - - /** - * Reconcile. - * - * @param model the model - * @param channel the channel - * @throws TemplateException the template exception - * @throws ApiException the api exception - * @throws IOException Signals that an I/O exception has occurred. - */ - @SuppressWarnings("PMD.ConfusingTernary") - public void reconcile(Map model, VmChannel channel) - throws TemplateException, ApiException, IOException { - // Combine template and data and parse result - var fmTemplate = fmConfig.getTemplate("runnerDataPvc.ftl.yaml"); - StringWriter out = new StringWriter(); - fmTemplate.process(model, out); - // Avoid Yaml.load due to - // https://github.com/kubernetes-client/java/issues/2741 - var pvcDef = Dynamics.newFromYaml(out.toString()); - - // Get API and check if PVC exists - DynamicKubernetesApi pvcApi = new DynamicKubernetesApi("", "v1", - "persistentvolumeclaims", channel.client()); - var existing = K8s.get(pvcApi, pvcDef.getMetadata()); - - // If PVC does not exist, create. Else patch (apply) - if (existing.isEmpty()) { - pvcApi.create(pvcDef); - } else { - // spec is immutable, so mix in existing spec - GsonPtr.to(pvcDef.getRaw()).set("spec", GsonPtr - .to(existing.get().getRaw()).get(JsonObject.class, "spec") - .get().deepCopy()); - K8s.apply(pvcApi, existing.get(), - channel.client().getJSON().serialize(pvcDef)); - } - } - -} diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisksReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisksReconciler.java deleted file mode 100644 index d2a3956..0000000 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisksReconciler.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager; - -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi; -import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; -import io.kubernetes.client.util.generic.options.ListOptions; -import java.util.Collections; -import static org.jdrupes.vmoperator.manager.Constants.APP_NAME; -import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME; - -/** - * Delegee for reconciling the PVCs for the disks - */ -@SuppressWarnings("PMD.DataflowAnomalyAnalysis") -/* default */ class DisksReconciler { - - /** - * Reconcile disks. - * - * @param vmDef the vm def - * @param channel the channel - * @throws ApiException the api exception - */ - public void reconcile(JsonObject vmDef, - VmChannel channel) throws ApiException { - @SuppressWarnings("PMD.AvoidDuplicateLiterals") - var disks = GsonPtr.to(vmDef) - .get(JsonArray.class, "spec", "vm", "disks") - .map(JsonArray::asList).orElse(Collections.emptyList()); - int index = 0; - for (var disk : disks) { - reconcileDisk(vmDef, index++, (JsonObject) disk, channel); - } - } - - @SuppressWarnings({ "PMD.AvoidDuplicateLiterals", "PMD.ConfusingTernary" }) - private void reconcileDisk(JsonObject vmDefinition, - int index, JsonObject diskDef, VmChannel channel) - throws ApiException { - if (!diskDef.has("volumeClaimTemplate")) { - return; - } - var pvcObject = new DynamicKubernetesObject(); - var pvcRaw = GsonPtr.to(pvcObject.getRaw()); - var vmRaw = GsonPtr.to(vmDefinition); - var pvcTpl = GsonPtr.to(diskDef).to("volumeClaimTemplate"); - - // Copy base and metadata from template and add missing/additional data. - pvcObject.setApiVersion(pvcTpl.getAsString("apiVersion").get()); - pvcObject.setKind(pvcTpl.getAsString("kind").get()); - var vmName = vmRaw.getAsString("metadata", "name").orElse("default"); - pvcRaw.get(JsonObject.class).add("metadata", - pvcTpl.to("metadata").get(JsonObject.class).deepCopy()); - var defMeta = pvcRaw.to("metadata"); - defMeta.computeIfAbsent("namespace", () -> new JsonPrimitive( - vmRaw.getAsString("metadata", "namespace").orElse("default"))); - defMeta.computeIfAbsent("name", () -> new JsonPrimitive( - vmName + "-disk-" + index)); - var pvcLbls = pvcRaw.to("metadata", "labels"); - pvcLbls.set("app.kubernetes.io/name", APP_NAME); - pvcLbls.set("app.kubernetes.io/instance", vmName); - pvcLbls.set("app.kubernetes.io/component", "disk"); - pvcLbls.set("app.kubernetes.io/managed-by", VM_OP_NAME); - - // Get API and check if PVC exists - DynamicKubernetesApi pvcApi = new DynamicKubernetesApi("", "v1", - "persistentvolumeclaims", channel.client()); - var existing = K8s.get(pvcApi, pvcObject.getMetadata()); - - // If PVC does not exist, create. Else patch (apply) - if (existing.isEmpty()) { - // PVC does not exist yet, copy spec from template - pvcRaw.get(JsonObject.class).add("spec", - pvcTpl.to("spec").get(JsonObject.class).deepCopy()); - pvcApi.create(pvcObject); - } else { - // spec is immutable, so mix in existing spec - pvcRaw.set("spec", GsonPtr.to(existing.get().getRaw()) - .to("spec").get().deepCopy()); - K8s.apply(pvcApi, existing.get(), - channel.client().getJSON().serialize(pvcObject)); - } - } - - /** - * Delete the PVCs generated from the defined disks. - * - * @param event the event - * @param channel the channel - * @throws ApiException the api exception - */ - public void deleteDisks(VmDefChanged event, VmChannel channel) - throws ApiException { - // Get API and check and list related - var pvcApi = K8s.pvcApi(channel.client()); - var pvcs = pvcApi.list(event.object().getMetadata().getNamespace(), - new ListOptions().labelSelector( - "app.kubernetes.io/managed-by=" + VM_OP_NAME - + ",app.kubernetes.io/name=" + APP_NAME - + ",app.kubernetes.io/instance=" - + event.object().getMetadata().getName())); - for (var pvc : pvcs.getObject().getItems()) { - K8s.delete(pvcApi, pvc); - } - } - -} 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 c9b80db..387a041 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 @@ -50,9 +50,7 @@ public class Reconciler extends Component { @SuppressWarnings("PMD.SingularField") private final Configuration fmConfig; private final CmReconciler cmReconciler; - private final DataReconciler dataReconciler; - private final DisksReconciler disksReconciler; - private final PodReconciler podReconciler; + private final StsReconciler stsReconciler; /** * Instantiates a new reconciler. @@ -73,9 +71,7 @@ public class Reconciler extends Component { fmConfig.setClassForTemplateLoading(Reconciler.class, ""); cmReconciler = new CmReconciler(fmConfig); - disksReconciler = new DisksReconciler(); - dataReconciler = new DataReconciler(fmConfig); - podReconciler = new PodReconciler(fmConfig); + stsReconciler = new StsReconciler(fmConfig); } /** @@ -120,15 +116,12 @@ public class Reconciler extends Component { // Reconcile if (event.type() != Type.DELETED) { - dataReconciler.reconcile(model, channel); - disksReconciler.reconcile(vmDef, channel); var configMap = cmReconciler.reconcile(event, model, channel); model.put("cm", configMap.getRaw()); - podReconciler.reconcile(event, model, channel); + stsReconciler.reconcile(event, model, channel); } else { - podReconciler.reconcile(event, model, channel); + stsReconciler.reconcile(event, model, channel); cmReconciler.reconcile(event, model, channel); - disksReconciler.deleteDisks(event, channel); } } diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PodReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/StsReconciler.java similarity index 54% rename from org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PodReconciler.java rename to org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/StsReconciler.java index 281697f..3276523 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PodReconciler.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/StsReconciler.java @@ -25,18 +25,17 @@ import io.kubernetes.client.custom.V1Patch; import io.kubernetes.client.openapi.ApiException; import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi; import io.kubernetes.client.util.generic.dynamic.Dynamics; -import io.kubernetes.client.util.generic.options.DeleteOptions; +import io.kubernetes.client.util.generic.options.PatchOptions; import java.io.IOException; import java.io.StringWriter; import java.util.Map; -import static org.jdrupes.vmoperator.manager.Constants.STATE_STOPPED; import org.jdrupes.vmoperator.manager.VmDefChanged.Type; /** * Delegee for reconciling the pod. */ @SuppressWarnings("PMD.DataflowAnomalyAnalysis") -/* default */ class PodReconciler { +/* default */ class StsReconciler { private final Configuration fmConfig; @@ -45,12 +44,12 @@ import org.jdrupes.vmoperator.manager.VmDefChanged.Type; * * @param fmConfig the fm config */ - public PodReconciler(Configuration fmConfig) { + public StsReconciler(Configuration fmConfig) { this.fmConfig = fmConfig; } /** - * Reconcile pod. + * Reconcile stateful set. * * @param event the event * @param model the model @@ -63,51 +62,38 @@ import org.jdrupes.vmoperator.manager.VmDefChanged.Type; VmChannel channel) throws IOException, TemplateException, ApiException { // Check if exists - DynamicKubernetesApi podApi = new DynamicKubernetesApi("", "v1", - "pods", channel.client()); - var existing = K8s.get(podApi, event.object().getMetadata()); + DynamicKubernetesApi stsApi = new DynamicKubernetesApi("apps", "v1", + "statefulsets", channel.client()); + // var existing = K8s.get(stsApi, event.object().getMetadata()); - // Get desired state. - var delete = event.type() == Type.DELETED - || GsonPtr.to((JsonObject) model.get("cr")).to("spec", "vm") - .getAsString("state").orElse("").equals(STATE_STOPPED); - - // If deleted or stopped, delete - if (delete) { - if (existing.isPresent()) { - var opts = new DeleteOptions(); - opts.setGracePeriodSeconds( - GsonPtr.to((JsonObject) model.get("cr")).to("spec", "vm") - .getAsLong("powerdownTimeout").get() + 1); - K8s.delete(podApi, existing.get(), opts); - } + if (event.type() == Type.DELETED) { + var meta = GsonPtr.to((JsonObject) model.get("cr")).to("metadata"); + PatchOptions opts = new PatchOptions(); + opts.setFieldManager("kubernetes-java-kubectl-apply"); + stsApi.patch(meta.getAsString("namespace").get(), + meta.getAsString("name").get(), V1Patch.PATCH_FORMAT_JSON_PATCH, + new V1Patch("[{\"op\": \"replace\", " + + "\"path\": \"/spec/replicas\", \"value\": 0}]"), + opts).throwsApiException(); + stsApi.delete(meta.getAsString("namespace").get(), + meta.getAsString("name").get()).throwsApiException(); return; } // Combine template and data and parse result - var fmTemplate = fmConfig.getTemplate("runnerPod.ftl.yaml"); + var fmTemplate = fmConfig.getTemplate("runnerSts.ftl.yaml"); StringWriter out = new StringWriter(); fmTemplate.process(model, out); // Avoid Yaml.load due to // https://github.com/kubernetes-client/java/issues/2741 - var podDef = Dynamics.newFromYaml(out.toString()); - - // Check if update - if (existing.isEmpty()) { - podApi.create(podDef); - } else { - // only annotations are updated - var metadata = new JsonObject(); - metadata.add("annotations", GsonPtr.to(podDef.getRaw()) - .to("metadata").get(JsonObject.class, "annotations").get()); - var patch = new JsonObject(); - patch.add("metadata", metadata); - podApi.patch(existing.get().getMetadata().getNamespace(), - existing.get().getMetadata().getName(), - V1Patch.PATCH_FORMAT_JSON_MERGE_PATCH, - new V1Patch(channel.client().getJSON().serialize(patch))) - .throwsApiException(); - } + var stsDef = Dynamics.newFromYaml(out.toString()); + PatchOptions opts = new PatchOptions(); + opts.setForce(false); + opts.setFieldManager("kubernetes-java-kubectl-apply"); + stsApi.patch(stsDef.getMetadata().getNamespace(), + stsDef.getMetadata().getName(), V1Patch.PATCH_FORMAT_APPLY_YAML, + new V1Patch(channel.client().getJSON().serialize(stsDef)), + opts).throwsApiException(); } } diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmWatcher.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmWatcher.java index 483f18e..b9e8614 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmWatcher.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmWatcher.java @@ -34,6 +34,7 @@ import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi; import io.kubernetes.client.util.generic.options.ListOptions; import java.io.IOException; import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; @@ -100,6 +101,8 @@ public class VmWatcher extends Component { } } + @SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops", + "PMD.CognitiveComplexity" }) private void purge(CustomObjectsApi coa, List vmOpApiVersions) throws ApiException { // Get existing CRs (VMs) @@ -117,20 +120,28 @@ public class VmWatcher extends Component { opts.setLabelSelector( "app.kubernetes.io/managed-by=vmoperator," + "app.kubernetes.io/name=vmrunner"); - for (var version : vmOpApiVersions) { - for (String resource : List.of("pods", "configmaps", - "persistentvolumeclaims", "secrets")) { - // Get resources, selected by label - @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") - var api - = new DynamicKubernetesApi("", version, resource, client); - for (var obj : api.list(managedNamespace, opts).getObject() - .getItems()) { - String instance = obj.getMetadata().getLabels() - .get("app.kubernetes.io/instance"); - if (!known.contains(instance)) { - api.delete(managedNamespace, - obj.getMetadata().getName()); + for (String resource : List.of("apps/v1/statefulsets", + "v1/persistentvolumeclaims", "v1/configmaps", "v1/secrets")) { + var resParts = new LinkedList<>(List.of(resource.split("/"))); + var group = resParts.size() == 3 ? resParts.poll() : ""; + var version = resParts.poll(); + var plural = resParts.poll(); + // Get resources, selected by label + @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") + var api = new DynamicKubernetesApi(group, version, plural, client); + var listObj = api.list(managedNamespace, opts).getObject(); + if (listObj == null) { + continue; + } + for (var obj : listObj.getItems()) { + String instance = obj.getMetadata().getLabels() + .get("app.kubernetes.io/instance"); + if (!known.contains(instance)) { + var resName = obj.getMetadata().getName(); + var result = api.delete(managedNamespace, resName); + if (!result.isSuccess()) { + logger.warning(() -> "Cannot cleanup resource \"" + + resName + "\": " + result.toString()); } } }