Refactor.
This commit is contained in:
parent
f3e6384db6
commit
fc60c3edf1
7 changed files with 441 additions and 197 deletions
|
|
@ -0,0 +1,8 @@
|
||||||
|
add_header=true
|
||||||
|
eclipse.preferences.version=1
|
||||||
|
header_text=/*\n * VM-Operator\n * Copyright (C) 2023 Michael N. Lipp\n * \n * This program is free software\: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https\://www.gnu.org/licenses/>.\n */
|
||||||
|
project_specific_settings=true
|
||||||
|
replacements=<?xml version\="1.0" standalone\="yes"?>\n\n<replacements>\n<replacement key\="get" scope\="1" mode\="0">Returns the</replacement>\n<replacement key\="set" scope\="1" mode\="0">Sets the</replacement>\n<replacement key\="add" scope\="1" mode\="0">Adds the</replacement>\n<replacement key\="edit" scope\="1" mode\="0">Edits the</replacement>\n<replacement key\="remove" scope\="1" mode\="0">Removes the</replacement>\n<replacement key\="init" scope\="1" mode\="0">Inits the</replacement>\n<replacement key\="parse" scope\="1" mode\="0">Parses the</replacement>\n<replacement key\="create" scope\="1" mode\="0">Creates the</replacement>\n<replacement key\="build" scope\="1" mode\="0">Builds the</replacement>\n<replacement key\="is" scope\="1" mode\="0">Checks if is</replacement>\n<replacement key\="print" scope\="1" mode\="0">Prints the</replacement>\n<replacement key\="has" scope\="1" mode\="0">Checks for</replacement>\n</replacements>\n\n
|
||||||
|
visibility_package=false
|
||||||
|
visibility_private=false
|
||||||
|
visibility_protected=false
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
/*
|
||||||
|
* 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 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.DynamicKubernetesObject;
|
||||||
|
import io.kubernetes.client.util.generic.dynamic.Dynamics;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.jdrupes.vmoperator.manager.VmDefChanged.Type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delegee for reconciling the config map
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
|
||||||
|
/* default */ class CmReconciler {
|
||||||
|
|
||||||
|
private final Configuration fmConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new config map reconciler.
|
||||||
|
*
|
||||||
|
* @param fmConfig the fm config
|
||||||
|
*/
|
||||||
|
public CmReconciler(Configuration fmConfig) {
|
||||||
|
this.fmConfig = fmConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reconcile.
|
||||||
|
*
|
||||||
|
* @param event the event
|
||||||
|
* @param model the model
|
||||||
|
* @param channel the channel
|
||||||
|
* @return the dynamic kubernetes object
|
||||||
|
* @throws IOException Signals that an I/O exception has occurred.
|
||||||
|
* @throws TemplateException the template exception
|
||||||
|
* @throws ApiException the api exception
|
||||||
|
*/
|
||||||
|
public DynamicKubernetesObject reconcile(VmDefChanged event,
|
||||||
|
Map<String, Object> model, WatchChannel channel)
|
||||||
|
throws IOException, TemplateException, ApiException {
|
||||||
|
// Get API and check if exists
|
||||||
|
DynamicKubernetesApi cmApi = new DynamicKubernetesApi("", "v1",
|
||||||
|
"configmaps", channel.client());
|
||||||
|
var existing = K8s.get(cmApi, event.metadata());
|
||||||
|
|
||||||
|
// If deleted, delete
|
||||||
|
if (event.type() == Type.DELETED) {
|
||||||
|
if (existing.isPresent()) {
|
||||||
|
K8s.delete(cmApi, existing.get());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine template and data and parse result
|
||||||
|
var fmTemplate = fmConfig.getTemplate("runnerConfig.ftl.yaml");
|
||||||
|
StringWriter out = new StringWriter();
|
||||||
|
fmTemplate.process(model, out);
|
||||||
|
// Avoid Yaml.load due to
|
||||||
|
// https://github.com/kubernetes-client/java/issues/2741
|
||||||
|
var mapDef = Dynamics.newFromYaml(out.toString());
|
||||||
|
|
||||||
|
// Apply
|
||||||
|
return K8s.apply(cmApi, mapDef, out.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -37,4 +37,11 @@ public class Constants {
|
||||||
|
|
||||||
/** The Constant APP_NAME. */
|
/** The Constant APP_NAME. */
|
||||||
public static final String APP_NAME = "vmrunner";
|
public static final String APP_NAME = "vmrunner";
|
||||||
|
|
||||||
|
/** The Constant STATE_RUNNING. */
|
||||||
|
public static final String STATE_RUNNING = "Running";
|
||||||
|
|
||||||
|
/** The Constant STATE_STOPPED. */
|
||||||
|
public static final String STATE_STOPPED = "Stopped";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
/*
|
||||||
|
* 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.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<String, Object> model, WatchChannel 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
/*
|
||||||
|
* 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.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(DynamicKubernetesObject vmDef,
|
||||||
|
WatchChannel channel) throws ApiException {
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings({ "PMD.AvoidDuplicateLiterals", "PMD.ConfusingTernary" })
|
||||||
|
private void reconcileDisk(DynamicKubernetesObject vmDefinition,
|
||||||
|
int index, JsonObject diskDef, WatchChannel channel)
|
||||||
|
throws ApiException {
|
||||||
|
var pvcObject = new DynamicKubernetesObject();
|
||||||
|
var pvcRaw = GsonPtr.to(pvcObject.getRaw());
|
||||||
|
var vmRaw = GsonPtr.to(vmDefinition.getRaw());
|
||||||
|
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, WatchChannel channel)
|
||||||
|
throws ApiException {
|
||||||
|
// Get API and check and list related
|
||||||
|
var pvcApi = K8s.pvcApi(channel.client());
|
||||||
|
var pvcs = pvcApi.list(event.metadata().getNamespace(),
|
||||||
|
new ListOptions().labelSelector(
|
||||||
|
"app.kubernetes.io/managed-by=" + VM_OP_NAME
|
||||||
|
+ ",app.kubernetes.io/name=" + APP_NAME
|
||||||
|
+ ",app.kubernetes.io/instance="
|
||||||
|
+ event.metadata().getName()));
|
||||||
|
for (var pvc : pvcs.getObject().getItems()) {
|
||||||
|
K8s.delete(pvcApi, pvc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
/*
|
||||||
|
* 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.JsonObject;
|
||||||
|
import freemarker.template.Configuration;
|
||||||
|
import freemarker.template.TemplateException;
|
||||||
|
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 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 {
|
||||||
|
|
||||||
|
private final Configuration fmConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new config map reconciler.
|
||||||
|
*
|
||||||
|
* @param fmConfig the fm config
|
||||||
|
*/
|
||||||
|
public PodReconciler(Configuration fmConfig) {
|
||||||
|
this.fmConfig = fmConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reconcile pod.
|
||||||
|
*
|
||||||
|
* @param event the event
|
||||||
|
* @param model the model
|
||||||
|
* @param channel the channel
|
||||||
|
* @throws IOException Signals that an I/O exception has occurred.
|
||||||
|
* @throws TemplateException the template exception
|
||||||
|
* @throws ApiException the api exception
|
||||||
|
*/
|
||||||
|
public void reconcile(VmDefChanged event, Map<String, Object> model,
|
||||||
|
WatchChannel channel)
|
||||||
|
throws IOException, TemplateException, ApiException {
|
||||||
|
// Check if exists
|
||||||
|
DynamicKubernetesApi podApi = new DynamicKubernetesApi("", "v1",
|
||||||
|
"pods", channel.client());
|
||||||
|
var existing = K8s.get(podApi, event.metadata());
|
||||||
|
|
||||||
|
// Get state
|
||||||
|
var state = GsonPtr.to((JsonObject) model.get("cr")).to("spec", "vm")
|
||||||
|
.getAsString("state").get();
|
||||||
|
|
||||||
|
// If deleted or stopped, delete
|
||||||
|
if (event.type() == Type.DELETED || STATE_STOPPED.equals(state)) {
|
||||||
|
if (existing.isPresent()) {
|
||||||
|
K8s.delete(podApi, existing.get());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine template and data and parse result
|
||||||
|
var fmTemplate = fmConfig.getTemplate("runnerPod.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -18,9 +18,6 @@
|
||||||
|
|
||||||
package org.jdrupes.vmoperator.manager;
|
package org.jdrupes.vmoperator.manager;
|
||||||
|
|
||||||
import com.google.gson.JsonArray;
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import com.google.gson.JsonPrimitive;
|
|
||||||
import freemarker.core.ParseException;
|
import freemarker.core.ParseException;
|
||||||
import freemarker.template.Configuration;
|
import freemarker.template.Configuration;
|
||||||
import freemarker.template.DefaultObjectWrapperBuilder;
|
import freemarker.template.DefaultObjectWrapperBuilder;
|
||||||
|
|
@ -29,20 +26,13 @@ import freemarker.template.TemplateException;
|
||||||
import freemarker.template.TemplateExceptionHandler;
|
import freemarker.template.TemplateExceptionHandler;
|
||||||
import freemarker.template.TemplateHashModel;
|
import freemarker.template.TemplateHashModel;
|
||||||
import freemarker.template.TemplateNotFoundException;
|
import freemarker.template.TemplateNotFoundException;
|
||||||
import io.kubernetes.client.custom.V1Patch;
|
|
||||||
import io.kubernetes.client.openapi.ApiException;
|
import io.kubernetes.client.openapi.ApiException;
|
||||||
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi;
|
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi;
|
||||||
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
|
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
|
||||||
import io.kubernetes.client.util.generic.dynamic.Dynamics;
|
|
||||||
import io.kubernetes.client.util.generic.options.ListOptions;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.StringWriter;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
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_GROUP;
|
||||||
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME;
|
|
||||||
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_VERSION;
|
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_VERSION;
|
||||||
import org.jdrupes.vmoperator.manager.VmDefChanged.Type;
|
import org.jdrupes.vmoperator.manager.VmDefChanged.Type;
|
||||||
import org.jdrupes.vmoperator.util.ExtendedObjectWrapper;
|
import org.jdrupes.vmoperator.util.ExtendedObjectWrapper;
|
||||||
|
|
@ -57,9 +47,12 @@ import org.jgrapes.core.annotation.Handler;
|
||||||
"PMD.AvoidDuplicateLiterals" })
|
"PMD.AvoidDuplicateLiterals" })
|
||||||
public class Reconciler extends Component {
|
public class Reconciler extends Component {
|
||||||
|
|
||||||
private static final String STATE_RUNNING = "Running";
|
@SuppressWarnings("PMD.SingularField")
|
||||||
private static final String STATE_STOPPED = "Stopped";
|
|
||||||
private final Configuration fmConfig;
|
private final Configuration fmConfig;
|
||||||
|
private final CmReconciler cmReconciler;
|
||||||
|
private final DataReconciler dataReconciler;
|
||||||
|
private final DisksReconciler disksReconciler;
|
||||||
|
private final PodReconciler podReconciler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiates a new reconciler.
|
* Instantiates a new reconciler.
|
||||||
|
|
@ -78,6 +71,11 @@ public class Reconciler extends Component {
|
||||||
TemplateExceptionHandler.RETHROW_HANDLER);
|
TemplateExceptionHandler.RETHROW_HANDLER);
|
||||||
fmConfig.setLogTemplateExceptions(false);
|
fmConfig.setLogTemplateExceptions(false);
|
||||||
fmConfig.setClassForTemplateLoading(Reconciler.class, "");
|
fmConfig.setClassForTemplateLoading(Reconciler.class, "");
|
||||||
|
|
||||||
|
cmReconciler = new CmReconciler(fmConfig);
|
||||||
|
disksReconciler = new DisksReconciler();
|
||||||
|
dataReconciler = new DataReconciler(fmConfig);
|
||||||
|
podReconciler = new PodReconciler(fmConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -97,7 +95,8 @@ public class Reconciler extends Component {
|
||||||
@SuppressWarnings("PMD.ConfusingTernary")
|
@SuppressWarnings("PMD.ConfusingTernary")
|
||||||
public void onVmDefChanged(VmDefChanged event, WatchChannel channel)
|
public void onVmDefChanged(VmDefChanged event, WatchChannel channel)
|
||||||
throws ApiException, TemplateException, IOException {
|
throws ApiException, TemplateException, IOException {
|
||||||
DynamicKubernetesApi vmDefApi = new DynamicKubernetesApi(VM_OP_GROUP,
|
// Get complete VM (CR) definition
|
||||||
|
DynamicKubernetesApi vmCrApi = new DynamicKubernetesApi(VM_OP_GROUP,
|
||||||
VM_OP_VERSION, event.crd().getName(), channel.client());
|
VM_OP_VERSION, event.crd().getName(), channel.client());
|
||||||
var defMeta = event.metadata();
|
var defMeta = event.metadata();
|
||||||
|
|
||||||
|
|
@ -105,7 +104,7 @@ public class Reconciler extends Component {
|
||||||
DynamicKubernetesObject vmDef = null;
|
DynamicKubernetesObject vmDef = null;
|
||||||
Map<String, Object> model = null;
|
Map<String, Object> model = null;
|
||||||
if (event.type() != Type.DELETED) {
|
if (event.type() != Type.DELETED) {
|
||||||
vmDef = K8s.get(vmDefApi, defMeta).get();
|
vmDef = K8s.get(vmCrApi, defMeta).get();
|
||||||
|
|
||||||
// Prepare Freemarker model
|
// Prepare Freemarker model
|
||||||
model = new HashMap<>();
|
model = new HashMap<>();
|
||||||
|
|
@ -119,191 +118,15 @@ public class Reconciler extends Component {
|
||||||
|
|
||||||
// Reconcile
|
// Reconcile
|
||||||
if (event.type() != Type.DELETED) {
|
if (event.type() != Type.DELETED) {
|
||||||
reconcileDataPvc(model, channel);
|
dataReconciler.reconcile(model, channel);
|
||||||
reconcileDisks(vmDef, channel);
|
disksReconciler.reconcile(vmDef, channel);
|
||||||
var configMap = reconcileConfigMap(event, model, channel);
|
var configMap = cmReconciler.reconcile(event, model, channel);
|
||||||
model.put("cm", configMap.getRaw());
|
model.put("cm", configMap.getRaw());
|
||||||
reconcilePod(event, model, channel);
|
podReconciler.reconcile(event, model, channel);
|
||||||
} else {
|
} else {
|
||||||
reconcilePod(event, model, channel);
|
podReconciler.reconcile(event, model, channel);
|
||||||
reconcileConfigMap(event, model, channel);
|
cmReconciler.reconcile(event, model, channel);
|
||||||
deletePvcs(event, channel);
|
disksReconciler.deleteDisks(event, channel);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("PMD.ConfusingTernary")
|
|
||||||
private void reconcileDataPvc(Map<String, Object> model,
|
|
||||||
WatchChannel 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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void reconcileDisks(DynamicKubernetesObject vmDef,
|
|
||||||
WatchChannel channel) throws ApiException {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings({ "PMD.AvoidDuplicateLiterals", "PMD.ConfusingTernary" })
|
|
||||||
private void reconcileDisk(DynamicKubernetesObject vmDefinition,
|
|
||||||
int index, JsonObject diskDef, WatchChannel channel)
|
|
||||||
throws ApiException {
|
|
||||||
var pvcObject = new DynamicKubernetesObject();
|
|
||||||
var pvcRaw = GsonPtr.to(pvcObject.getRaw());
|
|
||||||
var vmRaw = GsonPtr.to(vmDefinition.getRaw());
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void deletePvcs(VmDefChanged event, WatchChannel channel)
|
|
||||||
throws ApiException {
|
|
||||||
// Get API and check and list related
|
|
||||||
var pvcApi = K8s.pvcApi(channel.client());
|
|
||||||
var pvcs = pvcApi.list(event.metadata().getNamespace(),
|
|
||||||
new ListOptions().labelSelector(
|
|
||||||
"app.kubernetes.io/managed-by=" + VM_OP_NAME
|
|
||||||
+ ",app.kubernetes.io/name=" + APP_NAME
|
|
||||||
+ ",app.kubernetes.io/instance="
|
|
||||||
+ event.metadata().getName()));
|
|
||||||
for (var pvc : pvcs.getObject().getItems()) {
|
|
||||||
K8s.delete(pvcApi, pvc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private DynamicKubernetesObject reconcileConfigMap(VmDefChanged event,
|
|
||||||
Map<String, Object> model, WatchChannel channel)
|
|
||||||
throws IOException, TemplateException, ApiException {
|
|
||||||
// Get API and check if exists
|
|
||||||
DynamicKubernetesApi cmApi = new DynamicKubernetesApi("", "v1",
|
|
||||||
"configmaps", channel.client());
|
|
||||||
var existing = K8s.get(cmApi, event.metadata());
|
|
||||||
|
|
||||||
// If deleted, delete
|
|
||||||
if (event.type() == Type.DELETED) {
|
|
||||||
if (existing.isPresent()) {
|
|
||||||
K8s.delete(cmApi, existing.get());
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Combine template and data and parse result
|
|
||||||
var fmTemplate = fmConfig.getTemplate("runnerConfig.ftl.yaml");
|
|
||||||
StringWriter out = new StringWriter();
|
|
||||||
fmTemplate.process(model, out);
|
|
||||||
// Avoid Yaml.load due to
|
|
||||||
// https://github.com/kubernetes-client/java/issues/2741
|
|
||||||
var mapDef = Dynamics.newFromYaml(out.toString());
|
|
||||||
|
|
||||||
// Apply
|
|
||||||
return K8s.apply(cmApi, mapDef, out.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void reconcilePod(VmDefChanged event, Map<String, Object> model,
|
|
||||||
WatchChannel channel)
|
|
||||||
throws IOException, TemplateException, ApiException {
|
|
||||||
// Check if exists
|
|
||||||
DynamicKubernetesApi podApi = new DynamicKubernetesApi("", "v1",
|
|
||||||
"pods", channel.client());
|
|
||||||
var existing = K8s.get(podApi, event.metadata());
|
|
||||||
|
|
||||||
// Get state
|
|
||||||
var state = GsonPtr.to((JsonObject) model.get("cr")).to("spec", "vm")
|
|
||||||
.getAsString("state").get();
|
|
||||||
|
|
||||||
// If deleted or stopped, delete
|
|
||||||
if (event.type() == Type.DELETED || STATE_STOPPED.equals(state)) {
|
|
||||||
if (existing.isPresent()) {
|
|
||||||
K8s.delete(podApi, existing.get());
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Combine template and data and parse result
|
|
||||||
var fmTemplate = fmConfig.getTemplate("runnerPod.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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue