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