Working PVC for disks reconciliation.
This commit is contained in:
parent
f1b1b2c059
commit
d1410183cb
8 changed files with 625 additions and 30 deletions
|
|
@ -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)'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<>();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue