From d1410183cb01be1d595dadcdb6525bb405e3ab00 Mon Sep 17 00:00:00 2001 From: "Michael N. Lipp" Date: Tue, 25 Jul 2023 12:26:42 +0200 Subject: [PATCH] Working PVC for disks reconciliation. --- deploy/crds/vmoperator-crd.yaml | 265 ++++++++++++++++++ deploy/test-vm.yaml | 9 +- org.jdrupes.vmoperator.manager/build.gradle | 3 +- .../jdrupes/vmoperator/manager/Constants.java | 16 +- .../jdrupes/vmoperator/manager/GsonPtr.java | 253 +++++++++++++++++ .../vmoperator/manager/Reconciler.java | 97 +++++-- .../jdrupes/vmoperator/manager/VmWatcher.java | 2 +- .../.settings/org.eclipse.jdt.core.prefs | 10 +- 8 files changed, 625 insertions(+), 30 deletions(-) create mode 100644 org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/GsonPtr.java diff --git a/deploy/crds/vmoperator-crd.yaml b/deploy/crds/vmoperator-crd.yaml index b5792b9..a3ba663 100644 --- a/deploy/crds/vmoperator-crd.yaml +++ b/deploy/crds/vmoperator-crd.yaml @@ -153,6 +153,271 @@ spec: properties: hostDevice: type: string + volumeClaimTemplate: + description: >- + A PVC spec to be used to provide the disk. The easiest + way to use a volume that cannot be automatically provisioned + (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. + properties: + annotations: + additionalProperties: + type: string + 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 + labels: + additionalProperties: + type: string + 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 + name: + description: >- + Name must be unique within a namespace. + Is required when creating resources, although some resources + may allow a client to request the generation of an appropriate + name automatically. Name is primarily intended for creation + idempotence and configuration definition. Cannot be + updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names. + The name is generated automatically but can be overriden. + type: string + 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 + type: object + spec: + description: >- + Spec defines the desired characteristics of + a volume requested by a pod author. More info: + https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + properties: + accessModes: + description: >- + accessModes contains the desired access + modes the volume should have. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + items: + type: string + type: array + default: [ "ReadWriteOnce" ] + dataSource: + description: >- + dataSource field can be used to specify + either: * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) + * An existing PVC (PersistentVolumeClaim) If the provisioner + or an external controller can support the specified + data source, it will create a new volume based on the + contents of the specified data source. If the AnyVolumeDataSource + feature gate is enabled, this field will always have + the same contents as the DataSourceRef field. + properties: + apiGroup: + description: >- + APIGroup is the group for the resource + being referenced. If APIGroup is not specified, + the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: >- + Kind is the type of resource being referenced + type: string + name: + description: >- + Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: >- + dataSourceRef specifies the object from + which to populate the volume with data, if a non-empty + volume is desired. This may be any local object from + a non-empty API group (non core object) or a PersistentVolumeClaim + object. When this field is specified, volume binding + will only succeed if the type of the specified object + matches some installed volume populator or dynamic provisioner. + This field will replace the functionality of the DataSource + field and as such if both fields are non-empty, they + must have the same value. For backwards compatibility, + both fields (DataSource and DataSourceRef) will be set + to the same value automatically if one of them is empty + and the other is non-empty. There are two important + differences between DataSource and DataSourceRef: * + While DataSource only allows two specific types of objects, + DataSourceRef allows any non-core object, as well as + PersistentVolumeClaim objects. * While DataSource ignores + disallowed values (dropping them), DataSourceRef preserves + all values, and generates an error if a disallowed value + is specified. (Beta) Using this field requires the AnyVolumeDataSource + feature gate to be enabled. + properties: + apiGroup: + description: >- + APIGroup is the group for the resource + being referenced. If APIGroup is not specified, + the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: >- + Kind is the type of resource being referenced + type: string + name: + description: >- + Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + resources: + description: >- + resources represents the minimum resources + the volume should have. If RecoverVolumeExpansionFailure + feature is enabled users are allowed to specify resource + requirements that are lower than previous value but + must still be higher than capacity recorded in the status + field of the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: >- + Limits describes the maximum amount + of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: >- + Requests describes the minimum amount + of compute resources required. If Requests is omitted + for a container, it defaults to Limits if that is + explicitly specified, otherwise to an implementation-defined + value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + selector: + description: >- + selector is a label query over volumes to + consider for binding. + properties: + matchExpressions: + description: >- + matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: >- + A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: >- + key is the label key that the selector + applies to. + type: string + operator: + description: >- + operator represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists and DoesNotExist. + type: string + values: + description: >- + values is an array of string values. + If the operator is In or NotIn, the values + array must be non-empty. If the operator is + Exists or DoesNotExist, the values array must + be empty. This array is replaced during a + strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: >- + matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field + is "key", the operator is "In", and the values array + contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: >- + storageClassName is the name of the StorageClass + required by the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1 + type: string + volumeMode: + description: >- + volumeMode defines what type of volume is + required by the claim. + type: string + default: Block + volumeName: + description: >- + volumeName is the binding reference to the + PersistentVolume backing this claim. + type: string + type: object + type: object bootindex: type: integer displays: diff --git a/deploy/test-vm.yaml b/deploy/test-vm.yaml index e964360..feecc6d 100644 --- a/deploy/test-vm.yaml +++ b/deploy/test-vm.yaml @@ -1,6 +1,7 @@ apiVersion: "vmoperator.jdrupes.org/v1" kind: VirtualMachine metadata: + namespace: qemu-vms name: test-vm spec: image: @@ -18,7 +19,13 @@ spec: - tap: mac: "00:16:3e:33:59:10" disks: - - hostDevice: /dev/vgmain/test-vm + - volumeClaimTemplate: + spec: + resources: + requests: + storage: 40Gi + +# - hostDevice: /dev/vgmain/test-vm displays: - spice: port: 5910 diff --git a/org.jdrupes.vmoperator.manager/build.gradle b/org.jdrupes.vmoperator.manager/build.gradle index 7a96ae3..3118a5f 100644 --- a/org.jdrupes.vmoperator.manager/build.gradle +++ b/org.jdrupes.vmoperator.manager/build.gradle @@ -17,7 +17,8 @@ dependencies { implementation project(':org.jdrupes.vmoperator.util') implementation 'commons-cli:commons-cli:1.5.0' - implementation 'io.kubernetes:client-java:18.0.0' + implementation 'io.kubernetes:client-java:[18.0.0,19)' + implementation 'io.kubernetes:client-java-extended:[18.0.0,19)' runtimeOnly 'org.slf4j:slf4j-jdk14:[2.0.7,3)' } diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Constants.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Constants.java index 0d03ea6..e5de23f 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Constants.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Constants.java @@ -23,8 +23,18 @@ package org.jdrupes.vmoperator.manager; */ public class Constants { - static final String VM_OP_GROUP = "vmoperator.jdrupes.org"; - static final String VM_OP_VERSION = "v1"; - static final String VM_OP_KIND_VM = "VirtualMachine"; + /** The Constant VM_OP_NAME. */ + public static final String VM_OP_NAME = "vm-operator"; + /** The Constant VM_OP_GROUP. */ + public static final String VM_OP_GROUP = "vmoperator.jdrupes.org"; + + /** The Constant VM_OP_VERSION. */ + public static final String VM_OP_VERSION = "v1"; + + /** The Constant VM_OP_KIND_VM. */ + public static final String VM_OP_KIND_VM = "VirtualMachine"; + + /** The Constant APP_NAME. */ + public static final String APP_NAME = "vm-runner"; } 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 new file mode 100644 index 0000000..f653fd9 --- /dev/null +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/GsonPtr.java @@ -0,0 +1,253 @@ +/* + * 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.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import java.util.Optional; +import java.util.function.Supplier; + +/** + * Utility class for pointing to elements on a Gson (Json) tree. + */ +@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", + "PMD.ClassWithOnlyPrivateConstructorsShouldBeFinal" }) +public class GsonPtr { + + private final JsonElement position; + + private GsonPtr(JsonElement root) { + this.position = root; + } + + /** + * Create a new instance pointing to the given element. + * + * @param root the root + * @return the Gson pointer + */ + @SuppressWarnings("PMD.ShortMethodName") + public static GsonPtr to(JsonElement root) { + return new GsonPtr(root); + } + + /** + * 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 JsonObject}, a new member (of type {@link JsonObject} + * is added. + * + * @param selectors the selectors + * @return the Gson pointer + */ + @SuppressWarnings({ "PMD.ShortMethodName", "PMD.PreserveStackTrace" }) + public GsonPtr to(Object... selectors) { + JsonElement element = position; + for (Object sel : selectors) { + if (element instanceof JsonObject obj + && sel instanceof String member) { + element = Optional.ofNullable(obj.get(member)).orElseGet(() -> { + @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") + var child = new JsonObject(); + obj.add(member, child); + return child; + }); + continue; + } + if (element instanceof JsonArray arr + && sel instanceof Integer index) { + try { + element = arr.get(index); + } catch (IndexOutOfBoundsException e) { + throw new IllegalStateException("Selected array index" + + " may not be empty."); + } + continue; + } + throw new IllegalStateException("Invalid selection"); + } + return new GsonPtr(element); + } + + /** + * Returns {@link JsonElement} that the pointer points to. + * + * @return the result + */ + public JsonElement get() { + return position; + } + + /** + * Returns {@link JsonElement} that the pointer points to, + * casted to the given type. + * + * @param the generic type + * @param cls the cls + * @return the result + */ + @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop" }) + public T get(Class cls) { + if (cls.isAssignableFrom(position.getClass())) { + return cls.cast(position); + } + throw new IllegalArgumentException("Not positioned at element" + + " of desired type."); + } + + /** + * Returns the selected {@link JsonElement}, cast to the class + * specified. + * + * @param the generic type + * @param cls the cls + * @param selectors the selectors + * @return the optional + */ + @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop" }) + public Optional + get(Class cls, Object... selectors) { + JsonElement element = position; + for (Object sel : selectors) { + if (element instanceof JsonObject obj + && sel instanceof String member) { + element = obj.get(member); + if (element == null) { + return Optional.empty(); + } + continue; + } + if (element instanceof JsonArray arr + && sel instanceof Integer index) { + try { + element = arr.get(index); + } catch (IndexOutOfBoundsException e) { + return Optional.empty(); + } + continue; + } + return Optional.empty(); + } + if (cls.isAssignableFrom(element.getClass())) { + return Optional.of(cls.cast(element)); + } + return Optional.empty(); + } + + /** + * Returns the String value of the selected {@link JsonPrimitive}. + * + * @param selectors the selectors + * @return the as string + */ + public Optional getAsString(Object... selectors) { + return get(JsonPrimitive.class, selectors) + .map(JsonPrimitive::getAsString); + } + + /** + * Sets the selected value. This pointer must point to a + * {@link JsonObject} or {@link JsonArray}. The selector must + * be a {@link String} or an integer respectively. + * + * @param selector the selector + * @param value the value + * @return the Gson pointer + */ + public GsonPtr set(Object selector, JsonElement value) { + if (position instanceof JsonObject obj + && selector instanceof String member) { + obj.add(member, value); + return this; + } + if (position instanceof JsonArray arr + && selector instanceof Integer index) { + if (index >= arr.size()) { + arr.add(value); + } else { + arr.set(index, value); + } + return this; + } + throw new IllegalStateException("Invalid selection"); + } + + /** + * Short for `set(selector, new JsonPrimitive(value))`. + * + * @param selector the selector + * @param value the value + * @return the gson ptr + * @see #set(Object, JsonElement) + */ + public GsonPtr set(Object selector, String value) { + return set(selector, new JsonPrimitive(value)); + } + + /** + * Same as {@link #set(Object, JsonElement)}, but sets the value + * only if it doesn't exist yet, else returns the existing value. + * If this pointer points to a {@link JsonArray} and the selector + * if larger than or equal to the size of the array, the supplied + * value will be appended. + * + * @param the generic type + * @param selector the selector + * @param supplier the supplier of the missing value + * @return the existing or supplied value + */ + @SuppressWarnings("unchecked") + public T + computeIfAbsent(Object selector, Supplier supplier) { + if (position instanceof JsonObject obj + && selector instanceof String member) { + return Optional.ofNullable((T) obj.get(member)).orElseGet(() -> { + var res = supplier.get(); + obj.add(member, res); + return res; + }); + } + if (position instanceof JsonArray arr + && selector instanceof Integer index) { + if (index >= arr.size()) { + var res = supplier.get(); + arr.add(res); + return res; + } + return (T) arr.get(index); + } + throw new IllegalStateException("Invalid selection"); + } + + /** + * Short for `computeIfAbsent(selector, () -> new JsonPrimitive(value))`. + * + * @param selector the selector + * @param value the value + * @return the Gson pointer + */ + public GsonPtr computeIfAbsent(Object selector, String value) { + computeIfAbsent(selector, () -> new JsonPrimitive(value)); + return this; + } + +} 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 2cbf29d..9b7b2a9 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 @@ -18,9 +18,19 @@ package org.jdrupes.vmoperator.manager; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import io.kubernetes.client.custom.V1Patch; +import io.kubernetes.client.extended.kubectl.exception.KubectlException; 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.PatchOptions; +import java.util.Collections; +import static org.jdrupes.vmoperator.manager.Constants.APP_NAME; import static org.jdrupes.vmoperator.manager.Constants.VM_OP_GROUP; +import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME; import static org.jdrupes.vmoperator.manager.Constants.VM_OP_VERSION; import org.jgrapes.core.Channel; import org.jgrapes.core.Component; @@ -29,6 +39,7 @@ import org.jgrapes.core.annotation.Handler; /** * Adapts Kubenetes resources to changes in VM definitions (CRs). */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") public class Reconciler extends Component { /** @@ -46,6 +57,7 @@ public class Reconciler extends Component { * @param event the event * @param channel the channel * @throws ApiException the api exception + * @throws KubectlException */ @Handler public void onVmDefChanged(VmDefChanged event, WatchChannel channel) @@ -53,28 +65,75 @@ public class Reconciler extends Component { DynamicKubernetesApi vmDefApi = new DynamicKubernetesApi(VM_OP_GROUP, VM_OP_VERSION, event.crd().getName(), channel.client()); var defMeta = event.metadata(); - var vmDef = vmDefApi.get(defMeta.getNamespace(), defMeta.getName()); + var vmDef = vmDefApi.get(defMeta.getNamespace(), defMeta.getName()) + .getObject(); -// DynamicKubernetesApi cmApi = new DynamicKubernetesApi("", "v1", -// "configmaps", channel.client()); -// var cm = new DynamicKubernetesObject(); -// cm.setApiVersion("v1"); -// cm.setKind("ConfigMap"); -// V1ObjectMeta metadata = new V1ObjectMeta(); -// metadata.setNamespace("default"); -// metadata.setName("test"); -// cm.setMetadata(metadata); -// JsonObject data = new JsonObject(); -// data.addProperty("test", "value"); -// cm.getRaw().add("data", data); -// -// var response = cmApi.create("default", cm, new CreateOptions()) -// .throwsApiException(); + @SuppressWarnings("PMD.AvoidDuplicateLiterals") + var disks = GsonPtr.to(vmDef.getRaw()) + .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); + } + } -// var obj = channel.coa().getNamespacedCustomObject(VM_OP_GROUP, VM_OP_VERSION, -// md.getNamespace(), event.crd().getName(), md.getName()); - event = null; + @SuppressWarnings({ "PMD.AvoidDuplicateLiterals", "PMD.ConfusingTernary" }) + private void reconcileDisk(DynamicKubernetesObject vmDefinition, + int index, JsonObject diskDef, WatchChannel channel) + throws ApiException { + var pvcObject = new DynamicKubernetesObject(); + pvcObject.setApiVersion("v1"); + pvcObject.setKind("PersistentVolumeClaim"); + var pvcDef = GsonPtr.to(pvcObject.getRaw()); + var vmDef = GsonPtr.to(vmDefinition.getRaw()); + var pvcTpl = GsonPtr.to(diskDef).to("volumeClaimTemplate"); + // Copy metadata from template and add missing/additional data. + var vmName = vmDef.getAsString("metadata", "name").orElse("default"); + pvcDef.get(JsonObject.class).add("metadata", + pvcTpl.to("metadata").get(JsonObject.class).deepCopy()); + var defMeta = pvcDef.to("metadata"); + defMeta.computeIfAbsent("namespace", () -> new JsonPrimitive( + vmDef.getAsString("metadata", "namespace").orElse("default"))); + defMeta.computeIfAbsent("name", () -> new JsonPrimitive( + vmName + "-disk-" + index)); + var pvcLbls = pvcDef.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 = pvcApi.get(defMeta.getAsString("namespace").get(), + defMeta.getAsString("name").get()); + + // If PVC does not exist, create. Else patch (apply) + if (!existing.isSuccess()) { + // PVC does not exist yet, copy spec from template + pvcDef.get(JsonObject.class).add("spec", + pvcTpl.to("spec").get(JsonObject.class).deepCopy()); + // Add missing + pvcDef.to("spec").computeIfAbsent("accessModes", + () -> GsonPtr.to(new JsonArray()).set(0, "ReadWriteOnce") + .get()); + pvcDef.to("spec").computeIfAbsent("volumeMode", "Block"); + pvcApi.create(pvcObject); + } else { + // spec is immutable, so mix in existing spec + pvcDef.set("spec", GsonPtr.to(existing.getObject().getRaw()) + .to("spec").get().deepCopy()); + PatchOptions opts = new PatchOptions(); + opts.setForce(false); + opts.setFieldManager("kubernetes-java-kubectl-apply"); + pvcApi.patch(pvcObject.getMetadata().getNamespace(), + pvcObject.getMetadata().getName(), + V1Patch.PATCH_FORMAT_APPLY_YAML, + new V1Patch(channel.client().getJSON().serialize(pvcObject)), + 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 fc9933f..e3562cd 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 @@ -50,7 +50,7 @@ public class VmWatcher extends Component { private ApiClient client; private V1APIResource vmsCrd; - private String managedNamespace = "default"; + private String managedNamespace = "qemu-vms"; private final Map channels = new ConcurrentHashMap<>(); diff --git a/org.jdrupes.vmoperator.util/.settings/org.eclipse.jdt.core.prefs b/org.jdrupes.vmoperator.util/.settings/org.eclipse.jdt.core.prefs index 0dab961..2b91307 100644 --- a/org.jdrupes.vmoperator.util/.settings/org.eclipse.jdt.core.prefs +++ b/org.jdrupes.vmoperator.util/.settings/org.eclipse.jdt.core.prefs @@ -1,5 +1,5 @@ # -#Fri Jul 21 17:39:36 CEST 2023 +#Mon Jul 24 15:40:37 CEST 2023 org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert @@ -22,12 +22,12 @@ org.eclipse.jdt.core.formatter.blank_lines_after_package=1 org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16 org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true org.eclipse.jdt.core.formatter.comment.indent_root_tags=false +org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position=false -org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16 org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16 org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert @@ -43,8 +43,8 @@ org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invoc org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=true +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16 org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0 @@ -63,8 +63,8 @@ org.eclipse.jdt.core.formatter.alignment_for_type_parameters=16 org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=false org.eclipse.jdt.core.formatter.parentheses_positions_in_annotation=common_lines org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert