Working PVC for disks reconciliation.

This commit is contained in:
Michael Lipp 2023-07-25 12:26:42 +02:00
parent f1b1b2c059
commit d1410183cb
8 changed files with 625 additions and 30 deletions

View file

@ -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)'
}

View file

@ -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";
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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 <T> the generic type
* @param cls the cls
* @return the result
*/
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop" })
public <T extends JsonElement> T get(Class<T> 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 <T> the generic type
* @param cls the cls
* @param selectors the selectors
* @return the optional
*/
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop" })
public <T extends JsonElement> Optional<T>
get(Class<T> 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<String> 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 <T> 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 extends JsonElement> T
computeIfAbsent(Object selector, Supplier<T> 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;
}
}

View file

@ -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();
}
}
}

View file

@ -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<String, WatchChannel> channels
= new ConcurrentHashMap<>();