| {{ localize("maximumCpus") }} |
- {{ entry.spec.vm.maximumCpus }} |
+ {{ maximumCpus(entry) }} |
| {{ localize("requestedCpus") }} |
diff --git a/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/VmConlet-functions.ts b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/VmConlet-functions.ts
index cf4740a..44d6471 100644
--- a/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/VmConlet-functions.ts
+++ b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/VmConlet-functions.ts
@@ -147,9 +147,22 @@ window.orgJDrupesVmOperatorVmConlet.initView = (viewDom: HTMLElement,
const cic = new ConditionlInputController(submitCallback);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const maximumCpus = (vmDef: any) => {
+ if (vmDef.spec.vm["maximumCpus"]) {
+ return vmDef.spec.vm.maximumCpus;
+ }
+ const topo = vmDef.spec.vm.cpuTopology;
+ return Math.max(1, topo.coresPerDie)
+ * Math.max(1, topo.diesPerSocket)
+ * Math.max(1, topo.sockets)
+ * Math.max(1, topo.threadsPerCore);
+ }
+
return {
controller, vmInfos, filteredData, detailsByName, localize,
shortDateTime, formatMemory, vmAction, cic, parseMemory,
+ maximumCpus,
scopedId: (id: string) => { return idScope.scopedId(id); }
};
}
From bf1e05d5979f3bdf6f40b8198401f565c3a26913 Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Sat, 4 Nov 2023 12:39:18 +0100
Subject: [PATCH 006/379] Rename method.
---
.../src/org/jdrupes/vmoperator/vmconlet/VmConlet.java | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/VmConlet.java b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/VmConlet.java
index 4f9c0a9..f096485 100644
--- a/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/VmConlet.java
+++ b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/VmConlet.java
@@ -195,7 +195,7 @@ public class VmConlet extends FreeMarkerConlet {
}
}
} else {
- var vmDef = prepareForSending(event);
+ var vmDef = convertQuantities(event);
var def = JsonBeanDecoder.create(vmDef.getRaw().toString())
.readObject();
for (var entry : conletIdsByConsoleConnection().entrySet()) {
@@ -217,7 +217,7 @@ public class VmConlet extends FreeMarkerConlet {
}
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
- private DynamicKubernetesObject prepareForSending(VmDefChanged event) {
+ private DynamicKubernetesObject convertQuantities(VmDefChanged event) {
// Clone and remove managed fields
var vmDef = new DynamicKubernetesObject(
event.vmDefinition().getRaw().deepCopy());
From 01e392fc9dba3b1f3d72e1e2e13c6f7897f9ca31 Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Sat, 4 Nov 2023 12:40:00 +0100
Subject: [PATCH 007/379] Ensure that json does not change after association
with channel.
---
.../org/jdrupes/vmoperator/manager/VmWatcher.java | 14 ++++++++------
1 file changed, 8 insertions(+), 6 deletions(-)
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 a09b4b2..7f8b0c4 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
@@ -277,11 +277,14 @@ public class VmWatcher extends Component {
DynamicKubernetesApi vmCrApi = new DynamicKubernetesApi(VM_OP_GROUP,
apiVersion, vmsCrd.getName(), channel.client());
var curVmDef = K8s.get(vmCrApi, metadata);
- curVmDef.ifPresent(def -> channel.setVmDefinition(def));
+ curVmDef.ifPresent(def -> {
+ // Augment with "dynamic" data and associate with channel
+ addDynamicData(channel.client(), def);
+ channel.setVmDefinition(def);
+ });
- // Get eventual definition and augment with "dynamic" data.
+ // Get eventual definition to use
var vmDef = curVmDef.orElse(channel.vmDefinition());
- addDynamicData(channel, vmDef);
// Create and fire event
channel.pipeline().fire(new VmDefChanged(VmDefChanged.Type
@@ -291,7 +294,7 @@ public class VmWatcher extends Component {
vmsCrd, vmDef), channel);
}
- private void addDynamicData(VmChannel channel,
+ private void addDynamicData(ApiClient client,
DynamicKubernetesObject vmDef) {
var rootNode = GsonPtr.to(vmDef.getRaw()).get(JsonObject.class);
rootNode.addProperty("nodeName", "");
@@ -313,8 +316,7 @@ public class VmWatcher extends Component {
podSearch.setLabelSelector("app.kubernetes.io/name=" + APP_NAME
+ ",app.kubernetes.io/component=" + APP_NAME
+ ",app.kubernetes.io/instance=" + vmDef.getMetadata().getName());
- var podList = K8s.podApi(channel.client()).list(namespaceToWatch,
- podSearch);
+ var podList = K8s.podApi(client).list(namespaceToWatch, podSearch);
podList.getObject().getItems().stream().forEach(pod -> {
rootNode.addProperty("nodeName", pod.getSpec().getNodeName());
});
From 6e2d23d979a152f4fe08f48ace94ceb10066d8e8 Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Sat, 4 Nov 2023 14:37:26 +0100
Subject: [PATCH 008/379] Use default log settings by default.
---
deploy/vmop-config-map.yaml | 14 --------------
1 file changed, 14 deletions(-)
diff --git a/deploy/vmop-config-map.yaml b/deploy/vmop-config-map.yaml
index 2b94f19..69df680 100644
--- a/deploy/vmop-config-map.yaml
+++ b/deploy/vmop-config-map.yaml
@@ -8,17 +8,3 @@ metadata:
data:
config.yaml: |
"/Manager": {}
-
- logging.properties: |
- handlers=java.util.logging.ConsoleHandler, \
- org.jgrapes.webconlet.logviewer.LogViewerHandler
-
- org.jgrapes.level=FINE
- org.jdrupes.vmoperator.manager.level=FINE
-
- java.util.logging.ConsoleHandler.level=ALL
- java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter
- java.util.logging.SimpleFormatter.format=%1$tb %1$td %1$tT %4$s %5$s%6$s%n
-
- org.jgrapes.webconlet.logviewer.LogViewerHandler.level=FINE
-
\ No newline at end of file
From 886c5b436e9bbf6fb8be8e53856ebbdf88882a43 Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Sat, 4 Nov 2023 14:41:12 +0100
Subject: [PATCH 009/379] Restrict viewer log level to config.
---
deploy/vmop-config-map.yaml | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/deploy/vmop-config-map.yaml b/deploy/vmop-config-map.yaml
index 69df680..12d9ccf 100644
--- a/deploy/vmop-config-map.yaml
+++ b/deploy/vmop-config-map.yaml
@@ -8,3 +8,17 @@ metadata:
data:
config.yaml: |
"/Manager": {}
+
+ logging.properties: |
+ handlers=java.util.logging.ConsoleHandler, \
+ org.jgrapes.webconlet.logviewer.LogViewerHandler
+
+ org.jgrapes.level=FINE
+ org.jdrupes.vmoperator.manager.level=FINE
+
+ java.util.logging.ConsoleHandler.level=ALL
+ java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter
+ java.util.logging.SimpleFormatter.format=%1$tb %1$td %1$tT %4$s %5$s%6$s%n
+
+ org.jgrapes.webconlet.logviewer.LogViewerHandler.level=CONFIG
+
\ No newline at end of file
From 7dd9921dbf51671eb0ebc760a9b5772c055b8c33 Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Sat, 4 Nov 2023 14:53:48 +0100
Subject: [PATCH 010/379] Provide default logging configuration as resource.
---
.../org/jdrupes/vmoperator/manager}/logging.properties | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename org.jdrupes.vmoperator.manager/{ => resources/org/jdrupes/vmoperator/manager}/logging.properties (100%)
diff --git a/org.jdrupes.vmoperator.manager/logging.properties b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/logging.properties
similarity index 100%
rename from org.jdrupes.vmoperator.manager/logging.properties
rename to org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/logging.properties
From 203ea00c7b40e6258a5f49b10c8a7ef55a6dd69e Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Sun, 5 Nov 2023 12:37:41 +0100
Subject: [PATCH 011/379] Fix checkstyle configuration.
---
checkstyle.xml | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/checkstyle.xml b/checkstyle.xml
index b5a60d3..015ef09 100644
--- a/checkstyle.xml
+++ b/checkstyle.xml
@@ -50,10 +50,9 @@
-
-
+
From 03dfa7a4d8bc5cb51154ccfbe0c6577d5e8b91a9 Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Sun, 5 Nov 2023 15:08:51 +0100
Subject: [PATCH 012/379] Provide more context information.
---
dev-example/config.yaml | 2 +
dev-example/kustomization.yaml | 1 +
.../vmoperator/manager/console-brand.ftl.html | 5 +-
.../vmoperator/manager/Controller.java | 2 +
.../jdrupes/vmoperator/manager/Manager.java | 80 ++++++++++++++++---
.../vmoperator/manager/package-info.java | 1 +
6 files changed, 79 insertions(+), 12 deletions(-)
diff --git a/dev-example/config.yaml b/dev-example/config.yaml
index 4a471a8..cf43692 100644
--- a/dev-example/config.yaml
+++ b/dev-example/config.yaml
@@ -1,6 +1,8 @@
# Used for running manager outside Kubernetes.
# Keep in sync with kustomize.yaml
"/Manager":
+ # If provided, is shown at top left before namespace
+ # clusterName: "test"
# The controller manages the VM
"/Controller":
namespace: vmop-dev
diff --git a/dev-example/kustomization.yaml b/dev-example/kustomization.yaml
index ae36fe1..70c6ae6 100644
--- a/dev-example/kustomization.yaml
+++ b/dev-example/kustomization.yaml
@@ -29,6 +29,7 @@ patches:
# Keep in sync with config.yaml
config.yaml: |
"/Manager":
+ # clusterName: "test"
"/Controller":
namespace: vmop-dev
"/Reconciler":
diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/console-brand.ftl.html b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/console-brand.ftl.html
index a81ee0a..9c9de88 100644
--- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/console-brand.ftl.html
+++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/console-brand.ftl.html
@@ -1,2 +1,5 @@
${_("consoleTitle")}
\ No newline at end of file
+ src="${renderSupport.consoleResource('VM-Operator.svg')}"
+ >${_("consoleTitle")}
+ (<#if clusterName()??>${clusterName() + "/"}#if>${ namespace() })
\ No newline at end of file
diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java
index 3f7badb..d865ba4 100644
--- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java
+++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java
@@ -138,6 +138,8 @@ public class Controller extends Component {
.of("/var/run/secrets/kubernetes.io/serviceaccount/namespace");
if (Files.isReadable(path)) {
namespace = Files.lines(path).findFirst().orElse(null);
+ fire(new ConfigurationUpdate().add(componentPath(), "namespace",
+ namespace));
}
}
if (namespace == null) {
diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Manager.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Manager.java
index d39718f..1175d8b 100644
--- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Manager.java
+++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Manager.java
@@ -18,6 +18,8 @@
package org.jdrupes.vmoperator.manager;
+import freemarker.template.TemplateMethodModelEx;
+import freemarker.template.TemplateModelException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@@ -27,6 +29,9 @@ import java.net.URISyntaxException;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
@@ -54,6 +59,7 @@ import org.jgrapes.net.SocketServer;
import org.jgrapes.util.ComponentCollector;
import org.jgrapes.util.FileSystemWatcher;
import org.jgrapes.util.YamlConfigurationStore;
+import org.jgrapes.util.events.ConfigurationUpdate;
import org.jgrapes.util.events.WatchFile;
import org.jgrapes.webconlet.locallogin.LoginConlet;
import org.jgrapes.webconsole.base.BrowserLocalBackedKVStore;
@@ -69,9 +75,13 @@ import org.jgrapes.webconsole.vuejs.VueJsConsoleWeblet;
/**
* The application class.
*/
+@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports" })
public class Manager extends Component {
+ private static String version;
private static Manager app;
+ private String clusterName;
+ private String namespace = "unknown";
/**
* Instantiates a new manager.
@@ -79,26 +89,26 @@ public class Manager extends Component {
*
* @throws IOException Signals that an I/O exception has occurred.
*/
- @SuppressWarnings("PMD.TooFewBranchesForASwitchStatement")
+ @SuppressWarnings({ "PMD.TooFewBranchesForASwitchStatement",
+ "PMD.NcssCount" })
public Manager(CommandLine cmdLine) throws IOException {
+ super(new NamedChannel("manager"));
// Prepare component tree
attach(new NioDispatcher());
- Channel mgrChannel = new NamedChannel("manager");
- attach(new FileSystemWatcher(mgrChannel));
- attach(new Controller(mgrChannel));
+ attach(new FileSystemWatcher(channel()));
+ attach(new Controller(channel()));
// Configuration store with file in /etc/opt (default)
File cfgFile = new File(cmdLine.getOptionValue('c',
- "/etc/opt/" + VM_OP_NAME.replace("-", "") + "/config.yaml"))
- .getCanonicalFile();
+ "/etc/opt/" + VM_OP_NAME.replace("-", "") + "/config.yaml"));
logger.config(() -> "Using configuration from: " + cfgFile.getPath());
// Don't rely on night config to produce a good exception
// for this simple case
if (!Files.isReadable(cfgFile.toPath())) {
throw new IOException("Cannot read configuration file " + cfgFile);
}
- attach(new YamlConfigurationStore(mgrChannel, cfgFile, false));
- fire(new WatchFile(cfgFile.toPath()));
+ attach(new YamlConfigurationStore(channel(), cfgFile, false));
+ fire(new WatchFile(cfgFile.toPath()), channel());
// Prepare GUI
Channel httpTransport = new NamedChannel("guiTransport");
@@ -126,7 +136,12 @@ public class Manager extends Component {
return;
}
ConsoleWeblet consoleWeblet = guiHttpServer
- .attach(new VueJsConsoleWeblet(httpChannel, Channel.SELF, rootUri))
+ .attach(new VueJsConsoleWeblet(httpChannel, Channel.SELF, rootUri) {
+ @Override
+ protected Map createConsoleBaseModel() {
+ return augmentBaseModel(super.createConsoleBaseModel());
+ }
+ })
.prependClassTemplateLoader(getClass())
.prependResourceBundleProvider(getClass())
.prependConsoleResourceProvider(getClass());
@@ -154,6 +169,47 @@ public class Manager extends Component {
}));
}
+ private Map augmentBaseModel(Map base) {
+ base.put("version", version);
+ base.put("clusterName", new TemplateMethodModelEx() {
+ @Override
+ public Object exec(@SuppressWarnings("rawtypes") List arguments)
+ throws TemplateModelException {
+ return clusterName;
+ }
+ });
+ base.put("namespace", new TemplateMethodModelEx() {
+ @Override
+ public Object exec(@SuppressWarnings("rawtypes") List arguments)
+ throws TemplateModelException {
+ return namespace;
+ }
+ });
+ return base;
+ }
+
+ /**
+ * Configure the component.
+ *
+ * @param event the event
+ */
+ @Handler
+ @SuppressWarnings("PMD.DataflowAnomalyAnalysis")
+ public void onConfigurationUpdate(ConfigurationUpdate event) {
+ event.structured(componentPath()).ifPresent(c -> {
+ if (c.containsKey("clusterName")) {
+ clusterName = (String) c.get("clusterName");
+ } else {
+ clusterName = null;
+ }
+ });
+ event.structured(componentPath() + "/Controller").ifPresent(c -> {
+ if (c.containsKey("namespace")) {
+ namespace = (String) c.get("namespace");
+ }
+ });
+ }
+
/**
* Log the exception when a handling error is reported.
*
@@ -207,8 +263,10 @@ public class Manager extends Component {
try {
// Instance logger is not available yet.
var logger = Logger.getLogger(Manager.class.getName());
- logger.config(() -> "Version: "
- + Manager.class.getPackage().getImplementationVersion());
+ version = Optional.ofNullable(
+ Manager.class.getPackage().getImplementationVersion())
+ .orElse("unknown");
+ logger.config(() -> "Version: " + version);
logger.config(() -> "running on "
+ System.getProperty("java.vm.name")
+ " (" + System.getProperty("java.vm.version") + ")"
diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/package-info.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/package-info.java
index 48bc158..54d4efe 100644
--- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/package-info.java
+++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/package-info.java
@@ -140,6 +140,7 @@
* mgr .left. [FileSystemWatcher]
* mgr .right. [YamlConfigurationStore]
* mgr .. [Controller]
+ * mgr .up. [Manager]
* mgr .up. [VmWatcher]
* mgr .. [Reconciler]
*
From 9b89bf8a968a118fd60d7b35c2c7f0e097702e55 Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Mon, 8 Jan 2024 15:33:43 +0100
Subject: [PATCH 013/379] Remove mavenLocal (should never have been committed).
---
.../src/org.jdrupes.vmoperator.java-common-conventions.gradle | 1 -
1 file changed, 1 deletion(-)
diff --git a/buildSrc/src/org.jdrupes.vmoperator.java-common-conventions.gradle b/buildSrc/src/org.jdrupes.vmoperator.java-common-conventions.gradle
index 50aebae..e09814c 100644
--- a/buildSrc/src/org.jdrupes.vmoperator.java-common-conventions.gradle
+++ b/buildSrc/src/org.jdrupes.vmoperator.java-common-conventions.gradle
@@ -21,7 +21,6 @@ plugins {
repositories {
// Use Maven Central for resolving dependencies.
mavenCentral()
- mavenLocal()
}
dependencies {
From 545106510ef8736cf900bfac31bf92114614fe81 Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Mon, 8 Jan 2024 22:58:43 +0100
Subject: [PATCH 014/379] Update.
---
.settings/org.eclipse.buildship.core.prefs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.settings/org.eclipse.buildship.core.prefs b/.settings/org.eclipse.buildship.core.prefs
index d0fed22..44c5061 100644
--- a/.settings/org.eclipse.buildship.core.prefs
+++ b/.settings/org.eclipse.buildship.core.prefs
@@ -1,11 +1,11 @@
-arguments=--init-script /home/mnl/.config/Code/User/globalStorage/redhat.java/1.18.0/config_linux/org.eclipse.osgi/51/0/.cp/gradle/init/init.gradle --init-script /home/mnl/.config/Code/User/globalStorage/redhat.java/1.18.0/config_linux/org.eclipse.osgi/51/0/.cp/gradle/protobuf/init.gradle
+arguments=--init-script /home/mnl/.config/Code/User/globalStorage/redhat.java/1.24.0/config_linux/org.eclipse.osgi/55/0/.cp/gradle/init/init.gradle --init-script /home/mnl/.config/Code/User/globalStorage/redhat.java/1.24.0/config_linux/org.eclipse.osgi/55/0/.cp/gradle/protobuf/init.gradle
auto.sync=false
build.scans.enabled=false
connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
connection.project.dir=
eclipse.preferences.version=1
gradle.user.home=
-java.home=
+java.home=/usr/lib/jvm/java-17-openjdk-17.0.8.0.7-1.fc37.x86_64
jvm.arguments=
offline.mode=false
override.workspace.settings=true
From 41a0ef1adb8d6e2c657c9075dd64f7f715d055a4 Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Tue, 9 Jan 2024 10:19:04 +0100
Subject: [PATCH 015/379] Fail if API server cannot be contacted.
---
.../vmoperator/runner/qemu/Runner.java | 17 ++++++++
.../vmoperator/runner/qemu/StatusUpdater.java | 23 +++++++---
.../vmoperator/runner/qemu/events/Exit.java | 43 +++++++++++++++++++
3 files changed, 78 insertions(+), 5 deletions(-)
create mode 100644 org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/Exit.java
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java
index 4e4ba18..b93ffd6 100644
--- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java
@@ -52,6 +52,7 @@ import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
import org.jdrupes.vmoperator.runner.qemu.commands.QmpCont;
+import org.jdrupes.vmoperator.runner.qemu.events.Exit;
import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand;
import org.jdrupes.vmoperator.runner.qemu.events.QmpConfigured;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerConfigurationUpdate;
@@ -185,6 +186,7 @@ public class Runner extends Component {
= "Standard-VM-latest.ftl.yaml";
private static final String SAVED_TEMPLATE = "VM.ftl.yaml";
private static final String FW_VARS = "fw-vars.fd";
+ private static int exitStatus;
private EventPipeline rep;
private final ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());
@@ -582,6 +584,16 @@ public class Runner extends Component {
"The VM has been shut down"));
}
+ /**
+ * On exit.
+ *
+ * @param event the event
+ */
+ @Handler
+ public void onExit(Exit event) {
+ exitStatus = event.exitStatus();
+ }
+
private void shutdown() {
if (state != State.TERMINATING) {
fire(new Stop());
@@ -650,6 +662,11 @@ public class Runner extends Component {
// Start the application
Components.start(app);
+
+ // Wait for (regular) termination
+ Components.awaitExhaustion();
+ System.exit(exitStatus);
+
} catch (IOException | InterruptedException
| org.apache.commons.cli.ParseException e) {
Logger.getLogger(Runner.class.getName()).log(Level.SEVERE, e,
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java
index 19e252f..4edcb46 100644
--- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java
@@ -41,6 +41,7 @@ import java.util.logging.Level;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM;
import org.jdrupes.vmoperator.runner.qemu.events.BalloonChangeEvent;
+import org.jdrupes.vmoperator.runner.qemu.events.Exit;
import org.jdrupes.vmoperator.runner.qemu.events.HotpluggableCpuStatus;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerConfigurationUpdate;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange;
@@ -57,6 +58,7 @@ import org.jgrapes.util.events.InitialConfiguration;
/**
* Updates the CR status.
*/
+@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class StatusUpdater extends Component {
private static final Set RUNNING_STATES
@@ -135,12 +137,21 @@ public class StatusUpdater extends Component {
* @throws ApiException
*/
@Handler
- @SuppressWarnings({ "PMD.DataflowAnomalyAnalysis",
- "PMD.AvoidInstantiatingObjectsInLoops", "PMD.AvoidDuplicateLiterals" })
- public void onStart(Start event) throws IOException, ApiException {
+ public void onStart(Start event) {
if (namespace == null) {
return;
}
+ try {
+ initVmCrApi(event);
+ } catch (IOException | ApiException e) {
+ logger.log(Level.SEVERE, e,
+ () -> "Cannot access VM's CR, terminating.");
+ event.cancel(true);
+ fire(new Exit(1));
+ }
+ }
+
+ private void initVmCrApi(Start event) throws IOException, ApiException {
var client = Config.defaultClient();
var apis = new ApisApi(client).getAPIVersions();
var crdVersions = apis.getGroups().stream()
@@ -155,6 +166,7 @@ public class StatusUpdater extends Component {
if (crdApiRes.isEmpty()) {
continue;
}
+ @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
var crApi = new DynamicKubernetesApi(VM_OP_GROUP,
crdVersion, crdApiRes.get().getName(), client);
var vmCr = crApi.get(namespace, vmName);
@@ -166,8 +178,9 @@ public class StatusUpdater extends Component {
}
}
if (vmCrApi == null) {
- logger.warning(() -> "Cannot find VM's CR, status will not"
- + " be updated.");
+ logger.severe(() -> "Cannot find VM's CR, terminating.");
+ event.cancel(true);
+ fire(new Exit(1));
}
}
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/Exit.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/Exit.java
new file mode 100644
index 0000000..bb608f6
--- /dev/null
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/Exit.java
@@ -0,0 +1,43 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2024 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.runner.qemu.events;
+
+import org.jgrapes.core.events.Stop;
+
+/**
+ * Like {@link Stop}, but sets an exit status.
+ */
+@SuppressWarnings("PMD.ShortClassName")
+public class Exit extends Stop {
+
+ private final int exitStatus;
+
+ /**
+ * Instantiates a new exit.
+ *
+ * @param exitStatus the exit status
+ */
+ public Exit(int exitStatus) {
+ this.exitStatus = exitStatus;
+ }
+
+ public int exitStatus() {
+ return exitStatus;
+ }
+}
From 245fad39a94ea3af0a3b2080bccf63f0e827b5f5 Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Tue, 9 Jan 2024 13:56:19 +0100
Subject: [PATCH 016/379] Add event generation.
---
.../org/jdrupes/vmoperator/common/K8s.java | 17 ++++++++++++
.../vmoperator/runner/qemu/StatusUpdater.java | 27 +++++++++++++++++++
2 files changed, 44 insertions(+)
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8s.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8s.java
index 5a87ecd..f61b431 100644
--- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8s.java
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8s.java
@@ -30,12 +30,14 @@ import io.kubernetes.client.openapi.models.V1ConfigMap;
import io.kubernetes.client.openapi.models.V1ConfigMapList;
import io.kubernetes.client.openapi.models.V1GroupVersionForDiscovery;
import io.kubernetes.client.openapi.models.V1ObjectMeta;
+import io.kubernetes.client.openapi.models.V1ObjectReference;
import io.kubernetes.client.openapi.models.V1PersistentVolumeClaim;
import io.kubernetes.client.openapi.models.V1PersistentVolumeClaimList;
import io.kubernetes.client.openapi.models.V1Pod;
import io.kubernetes.client.openapi.models.V1PodList;
import io.kubernetes.client.util.generic.GenericKubernetesApi;
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi;
+import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
import io.kubernetes.client.util.generic.options.DeleteOptions;
import io.kubernetes.client.util.generic.options.PatchOptions;
import java.util.Optional;
@@ -204,4 +206,19 @@ public class K8s {
return response.getObject();
}
+ /**
+ * Create an object reference.
+ *
+ * @param object the object
+ * @return the v 1 object reference
+ */
+ public static V1ObjectReference
+ objectReference(DynamicKubernetesObject object) {
+ return new V1ObjectReference().apiVersion(object.getApiVersion())
+ .kind(object.getKind())
+ .namespace(object.getMetadata().getNamespace())
+ .name(object.getMetadata().getName())
+ .resourceVersion(object.getMetadata().getResourceVersion())
+ .uid(object.getMetadata().getUid());
+ }
}
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java
index 4edcb46..542fa06 100644
--- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java
@@ -24,8 +24,11 @@ import io.kubernetes.client.custom.Quantity.Format;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.apis.ApisApi;
import io.kubernetes.client.openapi.apis.CustomObjectsApi;
+import io.kubernetes.client.openapi.apis.EventsV1Api;
+import io.kubernetes.client.openapi.models.EventsV1Event;
import io.kubernetes.client.openapi.models.V1APIGroup;
import io.kubernetes.client.openapi.models.V1GroupVersionForDiscovery;
+import io.kubernetes.client.openapi.models.V1ObjectMeta;
import io.kubernetes.client.util.Config;
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi;
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
@@ -34,12 +37,15 @@ import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
+import java.time.OffsetDateTime;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.logging.Level;
+import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM;
+import org.jdrupes.vmoperator.common.K8s;
import org.jdrupes.vmoperator.runner.qemu.events.BalloonChangeEvent;
import org.jdrupes.vmoperator.runner.qemu.events.Exit;
import org.jdrupes.vmoperator.runner.qemu.events.HotpluggableCpuStatus;
@@ -67,6 +73,7 @@ public class StatusUpdater extends Component {
private String namespace;
private String vmName;
private DynamicKubernetesApi vmCrApi;
+ private EventsV1Api evtsApi;
private long observedGeneration;
/**
@@ -149,6 +156,14 @@ public class StatusUpdater extends Component {
event.cancel(true);
fire(new Exit(1));
}
+ try {
+ evtsApi = new EventsV1Api(Config.defaultClient());
+ } catch (IOException e) {
+ logger.log(Level.SEVERE, e,
+ () -> "Cannot access events API, terminating.");
+ event.cancel(true);
+ fire(new Exit(1));
+ }
}
private void initVmCrApi(Start event) throws IOException, ApiException {
@@ -252,6 +267,18 @@ public class StatusUpdater extends Component {
}
return status;
}).throwsApiException();
+
+ // Log event
+ var evt = new EventsV1Event().kind("Event")
+ .metadata(new V1ObjectMeta().namespace(namespace)
+ .generateName("vmrunner-"))
+ .reportingController(VM_OP_GROUP + "/" + APP_NAME)
+ .reportingInstance(vmCr.getMetadata().getName())
+ .eventTime(OffsetDateTime.now()).type("Normal")
+ .regarding(K8s.objectReference(vmCr))
+ .action("StatusUpdate").reason(event.reason())
+ .note(event.message());
+ evtsApi.createNamespacedEvent(namespace, evt, null, null, null, null);
}
private void updateRunningCondition(RunnerStateChange event,
From f6b70684cae686908c4c3f4f02a45b0cbb85c70e Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Tue, 9 Jan 2024 14:04:58 +0100
Subject: [PATCH 017/379] Avoid redundant Stop event.
---
.../src/org/jdrupes/vmoperator/runner/qemu/Runner.java | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java
index b93ffd6..979ac8e 100644
--- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java
@@ -42,6 +42,7 @@ import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
+import java.util.Set;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
@@ -595,7 +596,7 @@ public class Runner extends Component {
}
private void shutdown() {
- if (state != State.TERMINATING) {
+ if (!Set.of(State.TERMINATING, State.STOPPED).contains(state)) {
fire(new Stop());
}
try {
From 218825fb8c0c42a94c6372811c792979bd29248c Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Tue, 9 Jan 2024 18:11:32 +0100
Subject: [PATCH 018/379] Update build.
---
.../org/jdrupes/vmoperator/runner/qemu/Containerfile.arch | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Containerfile.arch b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Containerfile.arch
index 379537b..e3055c0 100644
--- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Containerfile.arch
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Containerfile.arch
@@ -1,8 +1,9 @@
-FROM archlinux/archlinux
+FROM archlinux/archlinux:latest
RUN systemd-firstboot
-RUN pacman -Suy --noconfirm \
+RUN pacman-key --init \
+ && pacman -Sy --noconfirm archlinux-keyring && pacman -Su --noconfirm \
&& pacman -S --noconfirm which qemu-full virtiofsd \
edk2-ovmf swtpm iproute2 bridge-utils jre17-openjdk-headless \
&& pacman -Scc --noconfirm
From 5ec67ba61aff0292cc636a128e16b04962fbc423 Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Tue, 9 Jan 2024 18:46:08 +0100
Subject: [PATCH 019/379] Allow event generation.
---
deploy/vmrunner-role.yaml | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/deploy/vmrunner-role.yaml b/deploy/vmrunner-role.yaml
index 54e8742..8aea4e2 100644
--- a/deploy/vmrunner-role.yaml
+++ b/deploy/vmrunner-role.yaml
@@ -18,3 +18,9 @@ rules:
- vms/status
verbs:
- patch
+- apiGroups:
+ - events.k8s.io
+ resources:
+ - events
+ verbs:
+ - create
From d85c957b3345d24a33319ba17076ecbd5050058e Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Fri, 12 Jan 2024 20:34:37 +0100
Subject: [PATCH 020/379] Exit with error on connection failure with kubernetes
API.
---
.../vmoperator/manager/events/Exit.java | 43 +++++++++++++++++++
.../vmoperator/manager/Controller.java | 4 +-
.../jdrupes/vmoperator/manager/Manager.java | 16 +++++++
.../jdrupes/vmoperator/manager/VmWatcher.java | 14 +++++-
4 files changed, 74 insertions(+), 3 deletions(-)
create mode 100644 org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/Exit.java
diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/Exit.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/Exit.java
new file mode 100644
index 0000000..1c11a4e
--- /dev/null
+++ b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/Exit.java
@@ -0,0 +1,43 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2024 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.events;
+
+import org.jgrapes.core.events.Stop;
+
+/**
+ * Like {@link Stop}, but sets an exit status.
+ */
+@SuppressWarnings("PMD.ShortClassName")
+public class Exit extends Stop {
+
+ private final int exitStatus;
+
+ /**
+ * Instantiates a new exit.
+ *
+ * @param exitStatus the exit status
+ */
+ public Exit(int exitStatus) {
+ this.exitStatus = exitStatus;
+ }
+
+ public int exitStatus() {
+ return exitStatus;
+ }
+}
diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java
index d865ba4..589affc 100644
--- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java
+++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java
@@ -30,6 +30,7 @@ import java.util.logging.Level;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM;
import org.jdrupes.vmoperator.common.K8s;
+import org.jdrupes.vmoperator.manager.events.Exit;
import org.jdrupes.vmoperator.manager.events.ModifyVm;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jgrapes.core.Channel;
@@ -37,7 +38,6 @@ import org.jgrapes.core.Component;
import org.jgrapes.core.annotation.Handler;
import org.jgrapes.core.events.HandlingError;
import org.jgrapes.core.events.Start;
-import org.jgrapes.core.events.Stop;
import org.jgrapes.util.events.ConfigurationUpdate;
/**
@@ -146,7 +146,7 @@ public class Controller extends Component {
logger.severe(() -> "Namespace to control not configured and"
+ " no file in kubernetes directory.");
event.cancel(true);
- fire(new Stop());
+ fire(new Exit(2));
return;
}
logger.fine(() -> "Controlling namespace \"" + namespace + "\".");
diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Manager.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Manager.java
index 1175d8b..8c8e0fd 100644
--- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Manager.java
+++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Manager.java
@@ -41,6 +41,7 @@ import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME;
+import org.jdrupes.vmoperator.manager.events.Exit;
import org.jdrupes.vmoperator.util.FsdUtils;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Component;
@@ -82,6 +83,7 @@ public class Manager extends Component {
private static Manager app;
private String clusterName;
private String namespace = "unknown";
+ private static int exitStatus;
/**
* Instantiates a new manager.
@@ -224,6 +226,16 @@ public class Manager extends Component {
event.stop();
}
+ /**
+ * On exit.
+ *
+ * @param event the event
+ */
+ @Handler
+ public void onExit(Exit event) {
+ exitStatus = event.exitStatus();
+ }
+
/**
* On stop.
*
@@ -294,6 +306,10 @@ public class Manager extends Component {
// Start the application
Components.start(app);
+
+ // Wait for (regular) termination
+ Components.awaitExhaustion();
+ System.exit(exitStatus);
} catch (IOException | InterruptedException
| org.apache.commons.cli.ParseException e) {
Logger.getLogger(Manager.class.getName()).log(Level.SEVERE, e,
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 7f8b0c4..a6c2f97 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
@@ -52,6 +52,7 @@ import org.jdrupes.vmoperator.common.K8s;
import static org.jdrupes.vmoperator.manager.Constants.APP_NAME;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_KIND_VM;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME;
+import org.jdrupes.vmoperator.manager.events.Exit;
import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jdrupes.vmoperator.manager.events.VmDefChanged.Type;
@@ -105,7 +106,18 @@ public class VmWatcher extends Component {
* @throws ApiException
*/
@Handler(priority = 10)
- public void onStart(Start event) throws IOException, ApiException {
+ public void onStart(Start event) {
+ try {
+ startWatching();
+ } catch (IOException | ApiException e) {
+ logger.log(Level.SEVERE, e,
+ () -> "Cannot watch VMs, terminating.");
+ event.cancel(true);
+ fire(new Exit(1));
+ }
+ }
+
+ private void startWatching() throws IOException, ApiException {
// Get namespace
if (namespaceToWatch == null) {
var path = Path
From 2d9d8de357964932944e027b050767a07e31d54d Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Fri, 12 Jan 2024 23:13:16 +0100
Subject: [PATCH 021/379] Add informative log message.
---
.../src/org/jdrupes/vmoperator/manager/VmWatcher.java | 2 ++
1 file changed, 2 insertions(+)
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 a6c2f97..2d99727 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
@@ -219,6 +219,8 @@ public class VmWatcher extends Component {
"PMD.AvoidCatchingThrowable", "PMD.AvoidCatchingGenericException" })
var watcher = new Thread(() -> {
try {
+ logger.info(() -> "Watching objects created from "
+ + crd.getName() + "." + VM_OP_GROUP + "/" + version);
// Watch sometimes terminates without apparent reason.
while (true) {
Instant startedAt = Instant.now();
From 57edc3698025887d685b8714db50eec3aaf9717d Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Thu, 1 Feb 2024 23:16:25 +0100
Subject: [PATCH 022/379] Use names consistently.
---
.../org/jdrupes/vmoperator/vmconlet/VmConlet-preview.ftl.html | 4 ++--
.../org/jdrupes/vmoperator/vmconlet/l10n_de.properties | 2 --
2 files changed, 2 insertions(+), 4 deletions(-)
diff --git a/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/VmConlet-preview.ftl.html b/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/VmConlet-preview.ftl.html
index 11cd882..0c6aa37 100644
--- a/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/VmConlet-preview.ftl.html
+++ b/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/VmConlet-preview.ftl.html
@@ -30,11 +30,11 @@
{{ vmSummary.runningVms }} / {{ vmSummary.totalVms }} |
- | {{ localize("Used CPUs") }}: |
+ {{ localize("currentCpus") }}: |
{{ vmSummary.usedCpus }} |
- | {{ localize("Used RAM") }}: |
+ {{ localize("currentRam") }}: |
{{ formatMemory(Number(vmSummary.usedRam)) }} |
diff --git a/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/l10n_de.properties b/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/l10n_de.properties
index d17bab6..1804b81 100644
--- a/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/l10n_de.properties
+++ b/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/l10n_de.properties
@@ -2,8 +2,6 @@ conletName = VM Anzeige
VMsSummary = VMs (gestartet/gesamt)
-Used\ CPUs = Verwendete CPUs
-Used\ RAM = Verwendetes RAM
Period = Zeitraum
Last\ hour = Letzte Stunde
Last\ day = Letzter Tag
From 49370b507b6ba9f1b6885fe514250bc07de0e5e2 Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Sun, 4 Feb 2024 22:03:02 +0100
Subject: [PATCH 023/379] More memory needed when handling a large number of
VMs.
---
org.jdrupes.vmoperator.manager/build.gradle | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/org.jdrupes.vmoperator.manager/build.gradle b/org.jdrupes.vmoperator.manager/build.gradle
index 9385a6c..63f54ec 100644
--- a/org.jdrupes.vmoperator.manager/build.gradle
+++ b/org.jdrupes.vmoperator.manager/build.gradle
@@ -39,7 +39,7 @@ dependencies {
application {
applicationName = 'vm-manager'
- applicationDefaultJvmArgs = ['-Xmx64m', '-XX:+UseParallelGC',
+ applicationDefaultJvmArgs = ['-Xmx128m', '-XX:+UseParallelGC',
'-Djava.util.logging.manager=org.jdrupes.vmoperator.util.LongLoggingManager'
]
// Define the main class for the application.
From b5622a459cd7178d1c7392fa5086870cc5f2464a Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Sat, 17 Feb 2024 14:26:26 +0100
Subject: [PATCH 024/379] Fix searching for executable.
---
.../org/jdrupes/vmoperator/runner/qemu/CommandDefinition.java | 1 +
1 file changed, 1 insertion(+)
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CommandDefinition.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CommandDefinition.java
index fadc4a0..9057606 100644
--- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CommandDefinition.java
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CommandDefinition.java
@@ -42,6 +42,7 @@ class CommandDefinition {
for (JsonNode path : jsonData.get("executable")) {
if (Files.isExecutable(Path.of(path.asText()))) {
command.add(path.asText());
+ break;
}
}
if (command.isEmpty()) {
From 24f762d28cc889d845337553dbbf294263f4336f Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Sat, 17 Feb 2024 14:37:12 +0100
Subject: [PATCH 025/379] Add cloud-init support in runner.
---
.../config-sample.yaml | 8 ++
.../vmoperator/runner/qemu/Configuration.java | 14 +++
.../runner/qemu/Containerfile.alpine | 2 +-
.../vmoperator/runner/qemu/Containerfile.arch | 1 +
.../vmoperator/runner/qemu/Runner.java | 97 ++++++++++++++++---
.../templates/Standard-VM-latest.ftl.yaml | 23 +++++
6 files changed, 132 insertions(+), 13 deletions(-)
diff --git a/org.jdrupes.vmoperator.runner.qemu/config-sample.yaml b/org.jdrupes.vmoperator.runner.qemu/config-sample.yaml
index 461e79b..34c1511 100644
--- a/org.jdrupes.vmoperator.runner.qemu/config-sample.yaml
+++ b/org.jdrupes.vmoperator.runner.qemu/config-sample.yaml
@@ -27,6 +27,14 @@
# be set when starting the runner during development e.g. from the IDE.
# "namespace": ...
+ # Defines data for generating a cloud-init ISO image that is
+ # attached to the VM.
+ # "cloudInit":
+ # "metaData":
+ # ...
+ # "userData":
+ # ...
+
# Define the VM (required)
"vm":
# The VM's name (required)
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java
index dda438b..ff60176 100644
--- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java
@@ -24,6 +24,7 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
+import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Level;
@@ -65,10 +66,23 @@ public class Configuration implements Dto {
/** The firmware vars. */
public Path firmwareVars;
+ /** Optional cloud-init data. */
+ public CloudInit cloudInit;
+
/** The vm. */
@SuppressWarnings("PMD.ShortVariable")
public Vm vm;
+ /**
+ * Subsection "cloud-init".
+ */
+ public static class CloudInit implements Dto {
+ @SuppressWarnings("PMD.UseConcurrentHashMap")
+ public Map metaData;
+ @SuppressWarnings("PMD.UseConcurrentHashMap")
+ public Map userData;
+ }
+
/**
* Subsection "vm".
*/
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Containerfile.alpine b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Containerfile.alpine
index def82ef..b87049e 100644
--- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Containerfile.alpine
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Containerfile.alpine
@@ -2,7 +2,7 @@ FROM docker.io/alpine
RUN apk update
-RUN apk add qemu-system-x86_64 qemu-modules ovmf swtpm openjdk17
+RUN apk add qemu-system-x86_64 qemu-modules ovmf swtpm openjdk17 mtools
RUN mkdir -p /etc/qemu && echo "allow all" > /etc/qemu/bridge.conf
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Containerfile.arch b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Containerfile.arch
index e3055c0..2ccb2f9 100644
--- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Containerfile.arch
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Containerfile.arch
@@ -6,6 +6,7 @@ RUN pacman-key --init \
&& pacman -Sy --noconfirm archlinux-keyring && pacman -Su --noconfirm \
&& pacman -S --noconfirm which qemu-full virtiofsd \
edk2-ovmf swtpm iproute2 bridge-utils jre17-openjdk-headless \
+ mtools \
&& pacman -Scc --noconfirm
# Remove all targets.
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java
index 979ac8e..fe3784f 100644
--- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java
@@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
import freemarker.core.ParseException;
import freemarker.template.MalformedTemplateNameException;
import freemarker.template.TemplateException;
@@ -40,6 +41,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Comparator;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@@ -178,9 +180,12 @@ import org.jgrapes.util.events.WatchFile;
*
*/
@SuppressWarnings({ "PMD.ExcessiveImports", "PMD.AvoidPrintStackTrace",
- "PMD.DataflowAnomalyAnalysis" })
+ "PMD.DataflowAnomalyAnalysis", "PMD.TooManyMethods" })
public class Runner extends Component {
+ private static final String QEMU = "qemu";
+ private static final String SWTPM = "swtpm";
+ private static final String CLOUD_INIT_IMG = "cloudInitImg";
private static final String TEMPLATE_DIR
= "/opt/" + APP_NAME.replace("-", "") + "/templates";
private static final String DEFAULT_TEMPLATE
@@ -190,16 +195,29 @@ public class Runner extends Component {
private static int exitStatus;
private EventPipeline rep;
- private final ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());
+ private final ObjectMapper yamlMapper = new ObjectMapper(YAMLFactory
+ .builder().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER)
+ .build());
private final JsonNode defaults;
@SuppressWarnings("PMD.UseConcurrentHashMap")
private Configuration config = new Configuration();
private final freemarker.template.Configuration fmConfig;
private CommandDefinition swtpmDefinition;
+ private CommandDefinition cloudInitImgDefinition;
private CommandDefinition qemuDefinition;
private final QemuMonitor qemuMonitor;
private State state = State.INITIALIZING;
+ /** Preparatory actions for QEMU start */
+ @SuppressWarnings("PMD.FieldNamingConventions")
+ private enum QemuPreps {
+ Config,
+ Tpm,
+ CloudInit
+ }
+
+ private final Set qemuLatch = new HashSet<>();
+
/**
* Instantiates a new runner.
*
@@ -293,10 +311,14 @@ public class Runner extends Component {
// Obtain more context data from template
var tplData = dataFromTemplate();
- swtpmDefinition = Optional.ofNullable(tplData.get("swtpm"))
- .map(d -> new CommandDefinition("swtpm", d)).orElse(null);
- qemuDefinition = Optional.ofNullable(tplData.get("qemu"))
- .map(d -> new CommandDefinition("qemu", d)).orElse(null);
+ swtpmDefinition = Optional.ofNullable(tplData.get(SWTPM))
+ .map(d -> new CommandDefinition(SWTPM, d)).orElse(null);
+ qemuDefinition = Optional.ofNullable(tplData.get(QEMU))
+ .map(d -> new CommandDefinition(QEMU, d)).orElse(null);
+ cloudInitImgDefinition
+ = Optional.ofNullable(tplData.get(CLOUD_INIT_IMG))
+ .map(d -> new CommandDefinition(CLOUD_INIT_IMG, d))
+ .orElse(null);
// Forward some values to child components
qemuMonitor.configure(config.monitorSocket,
@@ -360,6 +382,7 @@ public class Runner extends Component {
.map(Object::toString).orElse(null));
model.put("firmwareVars", Optional.ofNullable(config.firmwareVars)
.map(Object::toString).orElse(null));
+ model.put("cloudInit", config.cloudInit);
model.put("vm", config.vm);
if (Optional.ofNullable(config.vm.display)
.map(d -> d.spice).map(s -> s.ticket).isPresent()) {
@@ -430,12 +453,56 @@ public class Runner extends Component {
state = State.STARTING;
rep.fire(new RunnerStateChange(state, "RunnerStarted",
"Runner has been started"));
- // Start first process
+ // Start first process(es)
+ qemuLatch.add(QemuPreps.Config);
if (config.vm.useTpm && swtpmDefinition != null) {
startProcess(swtpmDefinition);
- return;
+ qemuLatch.add(QemuPreps.Tpm);
+ }
+ if (config.cloudInit != null) {
+ generateCloudInitImg();
+ qemuLatch.add(QemuPreps.CloudInit);
+ }
+ mayBeStartQemu(QemuPreps.Config);
+ }
+
+ private void mayBeStartQemu(QemuPreps done) {
+ synchronized (qemuLatch) {
+ if (qemuLatch.isEmpty()) {
+ return;
+ }
+ qemuLatch.remove(done);
+ if (qemuLatch.isEmpty()) {
+ startProcess(qemuDefinition);
+ }
+ }
+ }
+
+ private void generateCloudInitImg() {
+ try {
+ var cloudInitDir = config.dataDir.resolve("cloud-init");
+ cloudInitDir.toFile().mkdir();
+ var metaOut
+ = Files.newBufferedWriter(cloudInitDir.resolve("meta-data"));
+ if (config.cloudInit.metaData != null) {
+ yamlMapper.writer().writeValue(metaOut,
+ config.cloudInit.metaData);
+ }
+ metaOut.close();
+ var userOut
+ = Files.newBufferedWriter(cloudInitDir.resolve("user-data"));
+ userOut.write("#cloud-config\n");
+ if (config.cloudInit.userData != null) {
+ yamlMapper.writer().writeValue(userOut,
+ config.cloudInit.userData);
+ }
+ userOut.close();
+ startProcess(cloudInitImgDefinition);
+ } catch (IOException e) {
+ logger.log(Level.SEVERE, e,
+ () -> "Cannot start runner: " + e.getMessage());
+ fire(new Stop());
}
- startProcess(qemuDefinition);
}
private boolean startProcess(CommandDefinition toStart) {
@@ -456,8 +523,8 @@ public class Runner extends Component {
public void onFileChanged(FileChanged event) {
if (event.change() == Kind.CREATED
&& event.path().equals(config.swtpmSocket)) {
- // swtpm running, start qemu
- startProcess(qemuDefinition);
+ // swtpm running, maybe start qemu
+ mayBeStartQemu(QemuPreps.Tpm);
return;
}
}
@@ -545,7 +612,13 @@ public class Runner extends Component {
@Handler
public void onProcessExited(ProcessExited event, ProcessChannel channel) {
channel.associated(CommandDefinition.class).ifPresent(procDef -> {
- // No process(es) may exit during startup
+ if (procDef.equals(cloudInitImgDefinition)
+ && event.exitValue() == 0) {
+ // Cloud-init ISO generation was successful.
+ mayBeStartQemu(QemuPreps.CloudInit);
+ return;
+ }
+ // No other process(es) may exit during startup
if (state == State.STARTING) {
logger.severe(() -> "Process " + procDef.name
+ " has exited with value " + event.exitValue()
diff --git a/org.jdrupes.vmoperator.runner.qemu/templates/Standard-VM-latest.ftl.yaml b/org.jdrupes.vmoperator.runner.qemu/templates/Standard-VM-latest.ftl.yaml
index b21db8f..dff656a 100644
--- a/org.jdrupes.vmoperator.runner.qemu/templates/Standard-VM-latest.ftl.yaml
+++ b/org.jdrupes.vmoperator.runner.qemu/templates/Standard-VM-latest.ftl.yaml
@@ -11,6 +11,19 @@
- [ "--ctrl", "type=unixio,path=${ runtimeDir }/swtpm-sock,mode=0600" ]
- "--terminate"
+"cloudInitImg":
+ # Candidate paths for the executable
+ "executable": [ "/bin/sh", "/usr/bin/sh" ]
+
+ # Arguments may be specified as nested lists for better readability.
+ # The arguments are flattened before being passed to the process.
+ "arguments":
+ - "-c"
+ - >-
+ mformat -C -f 1440 -v CIDATA -i ${ runtimeDir }/cloud-init.img
+ && mcopy -i ${ runtimeDir }/cloud-init.img
+ ${ dataDir }/cloud-init/meta-data ${ dataDir }/cloud-init/user-data ::
+
"qemu":
# Candidate paths for the executable
"executable": [ "/usr/bin/qemu-system-x86_64" ]
@@ -183,6 +196,16 @@
<#break>
#switch>
#list>
+ # Cloud-init image
+ <#if cloudInit??>
+ - [ "-blockdev", "node-name=drive-${ drvCounter }-host-resource,\
+ driver=file,filename=${ runtimeDir }/cloud-init.img" ]
+ # - how to use the file (as sequence of literal blocks)
+ - [ "-blockdev", "node-name=drive-${ drvCounter }-backend,driver=raw,\
+ file=drive-${ drvCounter }-host-resource" ]
+ # - the driver (what the guest sees)
+ - [ "-device", "virtio-blk-pci,drive=drive-${ drvCounter }-backend" ]
+ #if>
<#if vm.display??>
<#if vm.display.spice??>
From 599f64da4cac76e0316a80260bb904551219378d Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Sat, 17 Feb 2024 16:47:10 +0100
Subject: [PATCH 026/379] Provide fallback for instance-id.
---
.../config-sample.yaml | 5 +++-
.../vmoperator/runner/qemu/Configuration.java | 22 ++++++++++++++++
.../vmoperator/runner/qemu/Runner.java | 26 +++++++++----------
3 files changed, 39 insertions(+), 14 deletions(-)
diff --git a/org.jdrupes.vmoperator.runner.qemu/config-sample.yaml b/org.jdrupes.vmoperator.runner.qemu/config-sample.yaml
index 34c1511..2211fed 100644
--- a/org.jdrupes.vmoperator.runner.qemu/config-sample.yaml
+++ b/org.jdrupes.vmoperator.runner.qemu/config-sample.yaml
@@ -33,7 +33,10 @@
# "metaData":
# ...
# "userData":
- # ...
+ # ...
+ #
+ # If .metaData.instance-id is missing, an id is generated from the
+ # config file's modification timestamp.
# Define the VM (required)
"vm":
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java
index ff60176..ba36317 100644
--- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java
@@ -24,6 +24,8 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
+import java.time.Instant;
+import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
@@ -39,9 +41,14 @@ import org.jdrupes.vmoperator.util.FsdUtils;
*/
@SuppressWarnings("PMD.ExcessivePublicCount")
public class Configuration implements Dto {
+ private static final String CI_INSTANCE_ID = "instance-id";
+
@SuppressWarnings("PMD.FieldNamingConventions")
protected final Logger logger = Logger.getLogger(getClass().getName());
+ /** Configuration timestamp */
+ public Instant asOf;
+
/** The data dir. */
public Path dataDir;
@@ -259,6 +266,7 @@ public class Configuration implements Dto {
}
checkDrives();
+ checkCloudInit();
return true;
}
@@ -372,4 +380,18 @@ public class Configuration implements Dto {
return true;
}
+
+ private void checkCloudInit() {
+ if (cloudInit == null) {
+ return;
+ }
+
+ // Provide default for instance-id
+ if (cloudInit.metaData == null) {
+ cloudInit.metaData = new HashMap<>();
+ }
+ if (!cloudInit.metaData.containsKey(CI_INSTANCE_ID)) {
+ cloudInit.metaData.put(CI_INSTANCE_ID, "v" + asOf.getEpochSecond());
+ }
+ }
}
\ No newline at end of file
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java
index fe3784f..fccdf89 100644
--- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java
@@ -39,10 +39,10 @@ import java.lang.reflect.UndeclaredThrowableException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.time.Instant;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
-import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.logging.Level;
@@ -200,6 +200,7 @@ public class Runner extends Component {
.build());
private final JsonNode defaults;
@SuppressWarnings("PMD.UseConcurrentHashMap")
+ private final File configFile;
private Configuration config = new Configuration();
private final freemarker.template.Configuration fmConfig;
private CommandDefinition swtpmDefinition;
@@ -252,16 +253,16 @@ public class Runner extends Component {
attach(qemuMonitor = new QemuMonitor(channel()));
attach(new StatusUpdater(channel()));
- // Configuration store with file in /etc/opt (default)
- File config = new File(cmdLine.getOptionValue('c',
+ configFile = new File(cmdLine.getOptionValue('c',
"/etc/opt/" + APP_NAME.replace("-", "") + "/config.yaml"));
// Don't rely on night config to produce a good exception
// for this simple case
- if (!Files.isReadable(config.toPath())) {
- throw new IOException("Cannot read configuration file " + config);
+ if (!Files.isReadable(configFile.toPath())) {
+ throw new IOException(
+ "Cannot read configuration file " + configFile);
}
- attach(new YamlConfigurationStore(channel(), config, false));
- fire(new WatchFile(config.toPath()));
+ attach(new YamlConfigurationStore(channel(), configFile, false));
+ fire(new WatchFile(configFile.toPath()));
}
/**
@@ -286,21 +287,20 @@ public class Runner extends Component {
@Handler
public void onConfigurationUpdate(ConfigurationUpdate event) {
event.structured(componentPath()).ifPresent(c -> {
+ var newConf = yamlMapper.convertValue(c, Configuration.class);
+ newConf.asOf = Instant.ofEpochSecond(configFile.lastModified());
if (event instanceof InitialConfiguration) {
- processInitialConfiguration(c);
+ processInitialConfiguration(newConf);
return;
}
logger.fine(() -> "Updating configuration");
- var newConf = yamlMapper.convertValue(c, Configuration.class);
rep.fire(new RunnerConfigurationUpdate(newConf, state));
});
}
- private void processInitialConfiguration(
- Map runnerConfiguration) {
+ private void processInitialConfiguration(Configuration newConfig) {
try {
- config = yamlMapper.convertValue(runnerConfiguration,
- Configuration.class);
+ config = newConfig;
if (!config.check()) {
// Invalid configuration, not used, problems already logged.
config = null;
From 542c4eb61f3a2e1907e6dfc79b4350c723f17973 Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Sat, 17 Feb 2024 17:50:03 +0100
Subject: [PATCH 027/379] Update diagram.
---
.../vmoperator/runner/qemu/Runner.java | 24 ++++++++++++-------
1 file changed, 15 insertions(+), 9 deletions(-)
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java
index fccdf89..5df2d4b 100644
--- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java
@@ -112,21 +112,27 @@ import org.jgrapes.util.events.WatchFile;
*
* state "Starting (Processes)" as StartingProcess {
*
- * state which <>
- * state "Start swtpm" as swtpm
* state "Start qemu" as qemu
* state "Open monitor" as monitor
* state "Configure QMP" as waitForConfigured
* state "Configure QEMU" as configure
* state success <>
* state error <>
- *
- * which --> swtpm: [use swtpm]
- * which --> qemu: [else]
- *
+ *
+ * state prepFork <>
+ * state prepJoin <>
+ * state "Generate cloud-init image" as cloudInit
+ * prepFork --> cloudInit: [cloud-init data provided]
+ * swtpm --> prepJoin: FileChanged[swtpm socket created]
+ * state "Start swtpm" as swtpm
+ * prepFork --> swtpm: [use swtpm]
* swtpm: entry/start swtpm
- * swtpm -> qemu: FileChanged[swtpm socket created]
- *
+ * cloudInit --> prepJoin: ProcessExited
+ * cloudInit: entry/generate cloud-init image
+ * prepFork --> prepJoin: [else]
+ *
+ * prepJoin --> qemu
+ *
* qemu: entry/start qemu
* qemu --> monitor : FileChanged[monitor socket created]
*
@@ -141,7 +147,7 @@ import org.jgrapes.util.events.WatchFile;
* configure --> success: RunnerConfigurationUpdate (last handler)/fire cont command
* }
*
- * Initializing --> which: Started
+ * Initializing --> prepFork: Started
*
* success --> Running
*
From 499c1822fd003009f0f911d0fa49760982f88a11 Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Sat, 17 Feb 2024 19:42:44 +0100
Subject: [PATCH 028/379] Do push when testing.
---
org.jdrupes.vmoperator.manager/build.gradle | 6 ++++++
org.jdrupes.vmoperator.runner.qemu/build.gradle | 12 ++++++++++++
2 files changed, 18 insertions(+)
diff --git a/org.jdrupes.vmoperator.manager/build.gradle b/org.jdrupes.vmoperator.manager/build.gradle
index 63f54ec..38e43f1 100644
--- a/org.jdrupes.vmoperator.manager/build.gradle
+++ b/org.jdrupes.vmoperator.manager/build.gradle
@@ -60,6 +60,9 @@ task tagLatestImage(type: Exec) {
enabled = !project.version.contains("SNAPSHOT")
&& !project.version.contains("alpha") \
&& !project.version.contains("beta") \
+ || project.rootProject.properties['docker.testRegistry'] \
+ && project.rootProject.properties['docker.registry'] \
+ == project.rootProject.properties['docker.testRegistry']
commandLine 'podman', 'tag', "${project.name}:${project.version}",\
"${project.name}:latest"
@@ -85,6 +88,9 @@ task pushLatestImage(type: Exec) {
enabled = !project.version.contains("SNAPSHOT")
&& !project.version.contains("alpha") \
&& !project.version.contains("beta") \
+ || project.rootProject.properties['docker.testRegistry'] \
+ && project.rootProject.properties['docker.registry'] \
+ == project.rootProject.properties['docker.testRegistry']
commandLine 'podman', 'push', '--tls-verify=false', \
"localhost/${project.name}:${project.version}", \
diff --git a/org.jdrupes.vmoperator.runner.qemu/build.gradle b/org.jdrupes.vmoperator.runner.qemu/build.gradle
index ec7de7f..3a5a4b7 100644
--- a/org.jdrupes.vmoperator.runner.qemu/build.gradle
+++ b/org.jdrupes.vmoperator.runner.qemu/build.gradle
@@ -45,6 +45,9 @@ task tagLatestArchImage(type: Exec) {
enabled = !project.version.contains("SNAPSHOT")
&& !project.version.contains("alpha") \
&& !project.version.contains("beta") \
+ || project.rootProject.properties['docker.testRegistry'] \
+ && project.rootProject.properties['docker.registry'] \
+ == project.rootProject.properties['docker.testRegistry']
commandLine 'podman', 'tag', "${project.name}-arch:${project.version}",\
"${project.name}-arch:latest"
@@ -70,6 +73,9 @@ task pushArchLatestImage(type: Exec) {
enabled = !project.version.contains("SNAPSHOT")
&& !project.version.contains("alpha") \
&& !project.version.contains("beta") \
+ || project.rootProject.properties['docker.testRegistry'] \
+ && project.rootProject.properties['docker.registry'] \
+ == project.rootProject.properties['docker.testRegistry']
commandLine 'podman', 'push', '--tls-verify=false', \
"localhost/${project.name}-arch:${project.version}", \
@@ -91,6 +97,9 @@ task tagLatestAlpineImage(type: Exec) {
enabled = !project.version.contains("SNAPSHOT")
&& !project.version.contains("alpha") \
&& !project.version.contains("beta") \
+ || project.rootProject.properties['docker.testRegistry'] \
+ && project.rootProject.properties['docker.registry'] \
+ == project.rootProject.properties['docker.testRegistry']
commandLine 'podman', 'tag', "${project.name}-alpine:${project.version}",\
"${project.name}-alpine:latest"
@@ -116,6 +125,9 @@ task pushAlpineLatestImage(type: Exec) {
enabled = !project.version.contains("SNAPSHOT")
&& !project.version.contains("alpha") \
&& !project.version.contains("beta") \
+ || project.rootProject.properties['docker.testRegistry'] \
+ && project.rootProject.properties['docker.registry'] \
+ == project.rootProject.properties['docker.testRegistry']
commandLine 'podman', 'push', '--tls-verify=false', \
"localhost/${project.name}-alpine:${project.version}", \
From b255f0e9468f1744c239db799bfb5d2f193e8866 Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Sun, 18 Feb 2024 11:46:09 +0100
Subject: [PATCH 029/379] Support cloudInit in CRD.
---
deploy/crds/vms-crd.yaml | 15 ++++++++++
dev-example/.gitignore | 1 +
dev-example/test-vm-shell.yaml | 30 +++++++++++++++++++
.../vmoperator/manager/runnerConfig.ftl.yaml | 17 ++++++++++-
4 files changed, 62 insertions(+), 1 deletion(-)
create mode 100644 dev-example/.gitignore
create mode 100644 dev-example/test-vm-shell.yaml
diff --git a/deploy/crds/vms-crd.yaml b/deploy/crds/vms-crd.yaml
index 7e321da..9de714b 100644
--- a/deploy/crds/vms-crd.yaml
+++ b/deploy/crds/vms-crd.yaml
@@ -965,6 +965,21 @@ spec:
additionalProperties:
type: string
nullable: true
+ cloudInit:
+ type: object
+ description: >-
+ Provides data for generating a cloud-init ISO
+ image that is attached to the VM.
+ properties:
+ metaData:
+ description: Copied to cloud-init's meta-data file.
+ type: object
+ additionalProperties:
+ type: string
+ userData:
+ description: Copied to cloud-init's user-data file.
+ type: object
+ x-kubernetes-preserve-unknown-fields: true
vm:
type: object
description: Defines the VM.
diff --git a/dev-example/.gitignore b/dev-example/.gitignore
new file mode 100644
index 0000000..925478d
--- /dev/null
+++ b/dev-example/.gitignore
@@ -0,0 +1 @@
+/test-vm-ci.yaml
diff --git a/dev-example/test-vm-shell.yaml b/dev-example/test-vm-shell.yaml
new file mode 100644
index 0000000..8137694
--- /dev/null
+++ b/dev-example/test-vm-shell.yaml
@@ -0,0 +1,30 @@
+kind: Pod
+apiVersion: v1
+metadata:
+ name: test-vm-shell
+ namespace: vmop-dev
+spec:
+ volumes:
+ - name: test-vm-system-disk
+ persistentVolumeClaim:
+ claimName: system-disk-test-vm-0
+ - name: vmop-image-repository
+ persistentVolumeClaim:
+ claimName: vmop-image-repository
+ containers:
+ - name: test-vm-shell
+ image: archlinux/archlinux
+ args:
+ - bash
+ imagePullPolicy: Always
+ stdin: true
+ stdinOnce: true
+ tty: true
+ volumeDevices:
+ - name: test-vm-system-disk
+ devicePath: /dev/test-vm-system-disk
+ volumeMounts:
+ - name: vmop-image-repository
+ mountPath: /var/local/vmop-image-repository
+ securityContext:
+ privileged: true
diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml
index d49e705..64c5cbf 100644
--- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml
+++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml
@@ -44,7 +44,22 @@ data:
<#if cr.spec.runnerTemplate?? && cr.spec.runnerTemplate.update?? >
updateTemplate: ${ cr.spec.runnerTemplate.update.asBoolean?c }
#if>
-
+
+ # Forward the cloud-init data if provided
+ <#if cr.spec.cloudInit??>
+ cloudInit:
+ <#if cr.spec.cloudInit.metaData??>
+ metaData: ${ cr.spec.cloudInit.metaData.toString() }
+ <#else>
+ metaData: {}
+ #if>
+ <#if cr.spec.cloudInit.userData??>
+ userData: ${ cr.spec.cloudInit.userData.toString() }
+ <#else>
+ userData: {}
+ #if>
+ #if>
+
# Define the VM (required)
vm:
# The VM's name (required)
From 7835686304aa504611e1a491f5df9c2406221a26 Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Sun, 18 Feb 2024 13:17:51 +0100
Subject: [PATCH 030/379] Javadoc fix.
---
.../src/org/jdrupes/vmoperator/vmconlet/TimeSeries.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/TimeSeries.java b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/TimeSeries.java
index cc17295..c7f634b 100644
--- a/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/TimeSeries.java
+++ b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/TimeSeries.java
@@ -36,7 +36,7 @@ public class TimeSeries {
/**
* Instantiates a new time series.
*
- * @param series the number of series
+ * @param period the period
*/
public TimeSeries(Duration period) {
this.period = period;
From ee96f869daebbbf95ec5fcfb26ea8cb4553944ed Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Sun, 18 Feb 2024 13:35:31 +0100
Subject: [PATCH 031/379] Add generation of fallback properties.
---
dev-example/test-vm.yaml | 2 ++
.../vmoperator/manager/Reconciler.java | 28 +++++++++++++++++--
2 files changed, 28 insertions(+), 2 deletions(-)
diff --git a/dev-example/test-vm.yaml b/dev-example/test-vm.yaml
index 0cd820b..5913020 100644
--- a/dev-example/test-vm.yaml
+++ b/dev-example/test-vm.yaml
@@ -14,6 +14,8 @@ spec:
cpu: 1
memory: 2Gi
+ cloudInit: {}
+
vm:
# state: Running
bootMenu: yes
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 c3fee7d..2adb843 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
@@ -209,6 +209,15 @@ public class Reconciler extends Component {
private DynamicKubernetesObject patchCr(DynamicKubernetesObject vmDef) {
var json = vmDef.getRaw().deepCopy();
// Adjust cdromImage path
+ adjustCdRomPaths(json);
+
+ // Adjust cloud-init data
+ adjustCloudInitData(json);
+
+ return new DynamicKubernetesObject(json);
+ }
+
+ private void adjustCdRomPaths(JsonObject json) {
var disks
= GsonPtr.to(json).to("spec", "vm", "disks").get(JsonArray.class);
for (var disk : disks) {
@@ -233,7 +242,23 @@ public class Reconciler extends Component {
logger.warning(() -> "Invalid CDROM image: " + image);
}
}
- return new DynamicKubernetesObject(json);
+ }
+
+ private void adjustCloudInitData(JsonObject json) {
+ var spec = GsonPtr.to(json).to("spec").get(JsonObject.class);
+ if (!spec.has("cloudInit")) {
+ return;
+ }
+ var metaData = GsonPtr.to(spec).to("cloudInit", "metaData");
+ if (metaData.getAsString("instance-id").isEmpty()) {
+ metaData.set("instance-id",
+ GsonPtr.to(json).getAsString("metadata", "resourceVersion")
+ .map(s -> "v" + s).orElse("v1"));
+ }
+ if (metaData.getAsString("local-hostname").isEmpty()) {
+ metaData.set("local-hostname",
+ GsonPtr.to(json).getAsString("metadata", "name").get());
+ }
}
@SuppressWarnings("PMD.CognitiveComplexity")
@@ -300,5 +325,4 @@ public class Reconciler extends Component {
});
return model;
}
-
}
From eccc35d0bfc56da43cb10b6117214d5afc8ab5fe Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Tue, 20 Feb 2024 21:51:48 +0100
Subject: [PATCH 032/379] Make sure to get latest base image versions.
---
org.jdrupes.vmoperator.manager/build.gradle | 3 ++-
org.jdrupes.vmoperator.runner.qemu/build.gradle | 6 ++++--
2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/org.jdrupes.vmoperator.manager/build.gradle b/org.jdrupes.vmoperator.manager/build.gradle
index 38e43f1..d403be8 100644
--- a/org.jdrupes.vmoperator.manager/build.gradle
+++ b/org.jdrupes.vmoperator.manager/build.gradle
@@ -50,7 +50,8 @@ task buildImage(type: Exec) {
dependsOn installDist
inputs.files 'src/org/jdrupes/vmoperator/manager/Containerfile'
- commandLine 'podman', 'build', '-t', "${project.name}:${project.version}",\
+ commandLine 'podman', 'build', '--pull',
+ '-t', "${project.name}:${project.version}",\
'-f', 'src/org/jdrupes/vmoperator/manager/Containerfile', '.'
}
diff --git a/org.jdrupes.vmoperator.runner.qemu/build.gradle b/org.jdrupes.vmoperator.runner.qemu/build.gradle
index 3a5a4b7..7179b8f 100644
--- a/org.jdrupes.vmoperator.runner.qemu/build.gradle
+++ b/org.jdrupes.vmoperator.runner.qemu/build.gradle
@@ -35,7 +35,8 @@ task buildArchImage(type: Exec) {
dependsOn installDist
inputs.files 'src/org/jdrupes/vmoperator/runner/qemu/Containerfile.arch'
- commandLine 'podman', 'build', '-t', "${project.name}-arch:${project.version}",\
+ commandLine 'podman', 'build', '--pull',
+ '-t', "${project.name}-arch:${project.version}",\
'-f', 'src/org/jdrupes/vmoperator/runner/qemu/Containerfile.arch', '.'
}
@@ -87,7 +88,8 @@ task buildAlpineImage(type: Exec) {
dependsOn installDist
inputs.files 'src/org/jdrupes/vmoperator/runner/qemu/Containerfile.alpine'
- commandLine 'podman', 'build', '-t', "${project.name}-alpine:${project.version}",\
+ commandLine 'podman', 'build', '--pull',
+ '-t', "${project.name}-alpine:${project.version}",\
'-f', 'src/org/jdrupes/vmoperator/runner/qemu/Containerfile.alpine', '.'
}
From 413f8f76dc34f2eaaa641db93df92afd97948d73 Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Wed, 21 Feb 2024 16:14:37 +0100
Subject: [PATCH 033/379] Support cloud-init's network-config.
---
org.jdrupes.vmoperator.runner.qemu/config-sample.yaml | 5 ++++-
.../org/jdrupes/vmoperator/runner/qemu/Configuration.java | 2 ++
.../src/org/jdrupes/vmoperator/runner/qemu/Runner.java | 7 +++++++
.../templates/Standard-VM-latest.ftl.yaml | 3 +++
4 files changed, 16 insertions(+), 1 deletion(-)
diff --git a/org.jdrupes.vmoperator.runner.qemu/config-sample.yaml b/org.jdrupes.vmoperator.runner.qemu/config-sample.yaml
index 2211fed..4dc87a2 100644
--- a/org.jdrupes.vmoperator.runner.qemu/config-sample.yaml
+++ b/org.jdrupes.vmoperator.runner.qemu/config-sample.yaml
@@ -34,9 +34,12 @@
# ...
# "userData":
# ...
+ # "networkConfig":
+ # ...
#
# If .metaData.instance-id is missing, an id is generated from the
- # config file's modification timestamp.
+ # config file's modification timestamp. .userData and .networkConfig
+ # are optional.
# Define the VM (required)
"vm":
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java
index ba36317..192b44c 100644
--- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java
@@ -88,6 +88,8 @@ public class Configuration implements Dto {
public Map metaData;
@SuppressWarnings("PMD.UseConcurrentHashMap")
public Map userData;
+ @SuppressWarnings("PMD.UseConcurrentHashMap")
+ public Map networkConfig;
}
/**
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java
index 5df2d4b..d615ad6 100644
--- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java
@@ -503,6 +503,13 @@ public class Runner extends Component {
config.cloudInit.userData);
}
userOut.close();
+ if (config.cloudInit.networkConfig != null) {
+ var networkConfig = Files.newBufferedWriter(
+ cloudInitDir.resolve("network-config"));
+ yamlMapper.writer().writeValue(networkConfig,
+ config.cloudInit.networkConfig);
+ networkConfig.close();
+ }
startProcess(cloudInitImgDefinition);
} catch (IOException e) {
logger.log(Level.SEVERE, e,
diff --git a/org.jdrupes.vmoperator.runner.qemu/templates/Standard-VM-latest.ftl.yaml b/org.jdrupes.vmoperator.runner.qemu/templates/Standard-VM-latest.ftl.yaml
index dff656a..aa7f49e 100644
--- a/org.jdrupes.vmoperator.runner.qemu/templates/Standard-VM-latest.ftl.yaml
+++ b/org.jdrupes.vmoperator.runner.qemu/templates/Standard-VM-latest.ftl.yaml
@@ -23,6 +23,9 @@
mformat -C -f 1440 -v CIDATA -i ${ runtimeDir }/cloud-init.img
&& mcopy -i ${ runtimeDir }/cloud-init.img
${ dataDir }/cloud-init/meta-data ${ dataDir }/cloud-init/user-data ::
+ && if [ -r ${ dataDir }/cloud-init/network-config ]; then
+ mcopy -i ${ runtimeDir }/cloud-init.img
+ ${ dataDir }/cloud-init/network-config :: ; fi
"qemu":
# Candidate paths for the executable
From 639e3157698c4dae42293c7b2fb7eafa7b69ac6e Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Wed, 21 Feb 2024 22:21:32 +0100
Subject: [PATCH 034/379] Add network-config to CRD.
---
deploy/crds/vms-crd.yaml | 4 ++++
.../org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml | 3 +++
2 files changed, 7 insertions(+)
diff --git a/deploy/crds/vms-crd.yaml b/deploy/crds/vms-crd.yaml
index 9de714b..017050f 100644
--- a/deploy/crds/vms-crd.yaml
+++ b/deploy/crds/vms-crd.yaml
@@ -980,6 +980,10 @@ spec:
description: Copied to cloud-init's user-data file.
type: object
x-kubernetes-preserve-unknown-fields: true
+ networkConfig:
+ description: Copied to cloud-init's network-config file.
+ type: object
+ x-kubernetes-preserve-unknown-fields: true
vm:
type: object
description: Defines the VM.
diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml
index 64c5cbf..75371cc 100644
--- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml
+++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml
@@ -58,6 +58,9 @@ data:
<#else>
userData: {}
#if>
+ <#if cr.spec.cloudInit.networkConfig??>
+ networkConfig: ${ cr.spec.cloudInit.networkConfig.toString() }
+ #if>
#if>
# Define the VM (required)
From 48f86a6ef2c11f7f4d5fab0ab75e9c123ddc2493 Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Thu, 22 Feb 2024 09:59:51 +0100
Subject: [PATCH 035/379] Use commonly used prefix.
---
dev-example/test-vm.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/dev-example/test-vm.yaml b/dev-example/test-vm.yaml
index 5913020..1959e4e 100644
--- a/dev-example/test-vm.yaml
+++ b/dev-example/test-vm.yaml
@@ -27,7 +27,7 @@ spec:
networks:
- tap:
- mac: "00:16:3e:33:58:10"
+ mac: "00:50:56:33:58:10"
disks:
- volumeClaimTemplate:
metadata:
From 1a608df411cad973fe0c9f289bb79165e6fbab91 Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Thu, 22 Feb 2024 10:13:14 +0100
Subject: [PATCH 036/379] Use locally administered address in example.
---
dev-example/test-vm.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/dev-example/test-vm.yaml b/dev-example/test-vm.yaml
index 1959e4e..dcb3454 100644
--- a/dev-example/test-vm.yaml
+++ b/dev-example/test-vm.yaml
@@ -27,7 +27,7 @@ spec:
networks:
- tap:
- mac: "00:50:56:33:58:10"
+ mac: "02:16:3e:33:58:10"
disks:
- volumeClaimTemplate:
metadata:
From aa7fdbee080d7ffb626b18f68057c74d4082c2b2 Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Sun, 25 Feb 2024 13:48:44 +0100
Subject: [PATCH 037/379] Allow guest to (finally) shutdown the VM.
---
deploy/vmrunner-role.yaml | 1 +
.../vmoperator/runner/qemu/Runner.java | 26 +++++-----
.../vmoperator/runner/qemu/StatusUpdater.java | 31 ++++++++++++
.../runner/qemu/events/MonitorEvent.java | 17 ++++---
.../runner/qemu/events/RunnerStateChange.java | 32 +++++++++++++
.../runner/qemu/events/ShutdownEvent.java | 47 +++++++++++++++++++
6 files changed, 136 insertions(+), 18 deletions(-)
create mode 100644 org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/ShutdownEvent.java
diff --git a/deploy/vmrunner-role.yaml b/deploy/vmrunner-role.yaml
index 8aea4e2..c6df666 100644
--- a/deploy/vmrunner-role.yaml
+++ b/deploy/vmrunner-role.yaml
@@ -12,6 +12,7 @@ rules:
verbs:
- list
- get
+ - patch
- apiGroups:
- vmoperator.jdrupes.org
resources:
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java
index d615ad6..922f2af 100644
--- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java
@@ -640,13 +640,25 @@ public class Runner extends Component {
return;
}
if (procDef.equals(qemuDefinition) && state == State.RUNNING) {
- rep.fire(new Stop());
+ rep.fire(new Exit(event.exitValue()));
}
logger.info(() -> "Process " + procDef.name
+ " has exited with value " + event.exitValue());
});
}
+ /**
+ * On exit.
+ *
+ * @param event the event
+ */
+ @Handler(priority = 10_001)
+ public void onExit(Exit event) {
+ if (exitStatus == 0) {
+ exitStatus = event.exitStatus();
+ }
+ }
+
/**
* On stop.
*
@@ -656,7 +668,7 @@ public class Runner extends Component {
public void onStopFirst(Stop event) {
state = State.TERMINATING;
rep.fire(new RunnerStateChange(state, "VmTerminating",
- "The VM is being shut down"));
+ "The VM is being shut down", exitStatus != 0));
}
/**
@@ -671,16 +683,6 @@ public class Runner extends Component {
"The VM has been shut down"));
}
- /**
- * On exit.
- *
- * @param event the event
- */
- @Handler
- public void onExit(Exit event) {
- exitStatus = event.exitStatus();
- }
-
private void shutdown() {
if (!Set.of(State.TERMINATING, State.STOPPED).contains(state)) {
fire(new Stop());
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java
index 542fa06..5f8cf13 100644
--- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java
@@ -21,6 +21,7 @@ package org.jdrupes.vmoperator.runner.qemu;
import com.google.gson.JsonObject;
import io.kubernetes.client.custom.Quantity;
import io.kubernetes.client.custom.Quantity.Format;
+import io.kubernetes.client.custom.V1Patch;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.apis.ApisApi;
import io.kubernetes.client.openapi.apis.CustomObjectsApi;
@@ -32,6 +33,7 @@ import io.kubernetes.client.openapi.models.V1ObjectMeta;
import io.kubernetes.client.util.Config;
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.io.IOException;
import java.math.BigDecimal;
import java.nio.file.Files;
@@ -52,6 +54,7 @@ import org.jdrupes.vmoperator.runner.qemu.events.HotpluggableCpuStatus;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerConfigurationUpdate;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State;
+import org.jdrupes.vmoperator.runner.qemu.events.ShutdownEvent;
import org.jdrupes.vmoperator.util.GsonPtr;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Component;
@@ -75,6 +78,7 @@ public class StatusUpdater extends Component {
private DynamicKubernetesApi vmCrApi;
private EventsV1Api evtsApi;
private long observedGeneration;
+ private boolean shutdownByGuest;
/**
* Instantiates a new status updater.
@@ -268,6 +272,22 @@ public class StatusUpdater extends Component {
return status;
}).throwsApiException();
+ // Maybe stop VM
+ if (event.state() == State.TERMINATING && !event.failed()
+ && shutdownByGuest) {
+ PatchOptions patchOpts = new PatchOptions();
+ patchOpts.setFieldManager("kubernetes-java-kubectl-apply");
+ var res = vmCrApi.patch(namespace, vmName,
+ V1Patch.PATCH_FORMAT_JSON_PATCH,
+ new V1Patch("[{\"op\": \"replace\", \"path\": \"/spec/vm/state"
+ + "\", \"value\": \"Stopped\"}]"),
+ patchOpts);
+ if (!res.isSuccess()) {
+ logger.warning(
+ () -> "Cannot patch pod annotations: " + res.getStatus());
+ }
+ }
+
// Log event
var evt = new EventsV1Event().kind("Event")
.metadata(new V1ObjectMeta().namespace(namespace)
@@ -344,4 +364,15 @@ public class StatusUpdater extends Component {
return status;
}).throwsApiException();
}
+
+ /**
+ * On shutdown.
+ *
+ * @param event the event
+ * @throws ApiException the api exception
+ */
+ @Handler
+ public void onShutdown(ShutdownEvent event) throws ApiException {
+ shutdownByGuest = event.byGuest();
+ }
}
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorEvent.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorEvent.java
index 72647a1..ba04a26 100644
--- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorEvent.java
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorEvent.java
@@ -28,11 +28,13 @@ import org.jgrapes.core.Event;
*/
public class MonitorEvent extends Event {
+ private static final String EVENT_DATA = "data";
+
/**
* The kind of monitor event.
*/
public enum Kind {
- READY, POWERDOWN, DEVICE_TRAY_MOVED, BALLOON_CHANGE
+ READY, POWERDOWN, DEVICE_TRAY_MOVED, BALLOON_CHANGE, SHUTDOWN
}
private final Kind kind;
@@ -47,20 +49,23 @@ public class MonitorEvent extends Event {
@SuppressWarnings("PMD.TooFewBranchesForASwitchStatement")
public static Optional from(JsonNode response) {
try {
- var kind
- = MonitorEvent.Kind.valueOf(response.get("event").asText());
+ var kind = MonitorEvent.Kind
+ .valueOf(response.get("event").asText());
switch (kind) {
case POWERDOWN:
return Optional.of(new PowerdownEvent(kind, null));
case DEVICE_TRAY_MOVED:
return Optional
- .of(new TrayMovedEvent(kind, response.get("data")));
+ .of(new TrayMovedEvent(kind, response.get(EVENT_DATA)));
case BALLOON_CHANGE:
+ return Optional.of(
+ new BalloonChangeEvent(kind, response.get(EVENT_DATA)));
+ case SHUTDOWN:
return Optional
- .of(new BalloonChangeEvent(kind, response.get("data")));
+ .of(new ShutdownEvent(kind, response.get(EVENT_DATA)));
default:
return Optional
- .of(new MonitorEvent(kind, response.get("data")));
+ .of(new MonitorEvent(kind, response.get(EVENT_DATA)));
}
} catch (IllegalArgumentException e) {
return Optional.empty();
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/RunnerStateChange.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/RunnerStateChange.java
index 46fa1f8..5d5bffd 100644
--- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/RunnerStateChange.java
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/RunnerStateChange.java
@@ -25,6 +25,7 @@ import org.jgrapes.core.Event;
/**
* The Class RunnerStateChange.
*/
+@SuppressWarnings("PMD.DataClass")
public class RunnerStateChange extends Event {
/**
@@ -37,17 +38,36 @@ public class RunnerStateChange extends Event {
private final State state;
private final String reason;
private final String message;
+ private final boolean failed;
/**
* Instantiates a new runner state change.
*
+ * @param state the state
+ * @param reason the reason
+ * @param message the message
* @param channels the channels
*/
public RunnerStateChange(State state, String reason, String message,
Channel... channels) {
+ this(state, reason, message, false, channels);
+ }
+
+ /**
+ * Instantiates a new runner state change.
+ *
+ * @param state the state
+ * @param reason the reason
+ * @param message the message
+ * @param failed the failed
+ * @param channels the channels
+ */
+ public RunnerStateChange(State state, String reason, String message,
+ boolean failed, Channel... channels) {
super(channels);
this.state = state;
this.reason = reason;
+ this.failed = failed;
this.message = message;
}
@@ -78,11 +98,23 @@ public class RunnerStateChange extends Event {
return message;
}
+ /**
+ * Checks if is failed.
+ *
+ * @return the failed
+ */
+ public boolean failed() {
+ return failed;
+ }
+
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append(Components.objectName(this))
.append(" [").append(state).append(": ").append(reason);
+ if (failed) {
+ builder.append(" (failed)");
+ }
if (channels() != null) {
builder.append(", channels=");
builder.append(Channel.toString(channels()));
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/ShutdownEvent.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/ShutdownEvent.java
new file mode 100644
index 0000000..e46bbd3
--- /dev/null
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/ShutdownEvent.java
@@ -0,0 +1,47 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2024 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.runner.qemu.events;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+/**
+ * Signals the processing of the {@link QmpShutdown} event.
+ */
+public class ShutdownEvent extends MonitorEvent {
+
+ /**
+ * Instantiates a new shutdown event.
+ *
+ * @param kind the kind
+ * @param data the data
+ */
+ public ShutdownEvent(Kind kind, JsonNode data) {
+ super(kind, data);
+ }
+
+ /**
+ * returns if this is initiated by the guest.
+ *
+ * @return the value
+ */
+ public boolean byGuest() {
+ return data().get("guest").asBoolean();
+ }
+
+}
From bbe2d6efbc1bbfe3b7814c72571bb87f0439b606 Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Sun, 25 Feb 2024 14:27:54 +0100
Subject: [PATCH 038/379] Make this explicit (implied by replicas = 1).
---
.../resources/org/jdrupes/vmoperator/manager/runnerSts.ftl.yaml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerSts.ftl.yaml b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerSts.ftl.yaml
index ac1178a..2e5712b 100644
--- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerSts.ftl.yaml
+++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerSts.ftl.yaml
@@ -23,6 +23,8 @@ spec:
app.kubernetes.io/name: ${ constants.APP_NAME }
app.kubernetes.io/instance: ${ cr.metadata.name.asString }
replicas: ${ (cr.spec.vm.state.asString == "Running")?then(1, 0) }
+ updateStrategy:
+ type: OnDelete
template:
metadata:
namespace: ${ cr.metadata.namespace.asString }
From fb4a0206f162a7436b081c1b65d5d00f2f6cacb4 Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Sun, 25 Feb 2024 15:49:56 +0100
Subject: [PATCH 039/379] Make result of guest shutdown configurable.
---
deploy/crds/vms-crd.yaml | 6 ++++++
.../org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml | 2 ++
org.jdrupes.vmoperator.runner.qemu/config-sample.yaml | 5 +++++
.../org/jdrupes/vmoperator/runner/qemu/Configuration.java | 3 +++
.../org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java | 7 ++++++-
5 files changed, 22 insertions(+), 1 deletion(-)
diff --git a/deploy/crds/vms-crd.yaml b/deploy/crds/vms-crd.yaml
index 017050f..0292b8d 100644
--- a/deploy/crds/vms-crd.yaml
+++ b/deploy/crds/vms-crd.yaml
@@ -999,6 +999,12 @@ spec:
type: string
enum: [ "Stopped", "Running" ]
default: "Stopped"
+ guestShutdownStops:
+ description: >-
+ If true, sets the state to "Stopped" when
+ the VM terminates due to a shutdown by the guest.
+ type: boolean
+ default: false
machineUuid:
description: >-
The machine's uuid. If none is specified, a uuid
diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml
index 75371cc..d96a66b 100644
--- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml
+++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml
@@ -63,6 +63,8 @@ data:
#if>
#if>
+ guestShutdownStops: ${ cr.spec.guestShutdownStops!false?string('true', 'false') }
+
# Define the VM (required)
vm:
# The VM's name (required)
diff --git a/org.jdrupes.vmoperator.runner.qemu/config-sample.yaml b/org.jdrupes.vmoperator.runner.qemu/config-sample.yaml
index 4dc87a2..c365a12 100644
--- a/org.jdrupes.vmoperator.runner.qemu/config-sample.yaml
+++ b/org.jdrupes.vmoperator.runner.qemu/config-sample.yaml
@@ -41,6 +41,11 @@
# config file's modification timestamp. .userData and .networkConfig
# are optional.
+ # Whether a guest initiated shutdown event patches the state
+ # property in the CRD.
+ # "guestShutdownStops":
+ # false
+
# Define the VM (required)
"vm":
# The VM's name (required)
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java
index 192b44c..7fc3f95 100644
--- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java
@@ -76,6 +76,9 @@ public class Configuration implements Dto {
/** Optional cloud-init data. */
public CloudInit cloudInit;
+ /** If guest shutdown changes CRD .vm.state to "Stopped". */
+ public boolean guestShutdownStops;
+
/** The vm. */
@SuppressWarnings("PMD.ShortVariable")
public Vm vm;
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java
index 5f8cf13..1cb5e74 100644
--- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java
@@ -78,6 +78,7 @@ public class StatusUpdater extends Component {
private DynamicKubernetesApi vmCrApi;
private EventsV1Api evtsApi;
private long observedGeneration;
+ private boolean guestShutdownStops;
private boolean shutdownByGuest;
/**
@@ -217,6 +218,9 @@ public class StatusUpdater extends Component {
@Handler
public void onRunnerConfigurationUpdate(RunnerConfigurationUpdate event)
throws ApiException {
+ guestShutdownStops = event.configuration().guestShutdownStops;
+
+ // Remainder applies only if we have a connection to k8s.
if (vmCrApi == null) {
return;
}
@@ -274,7 +278,8 @@ public class StatusUpdater extends Component {
// Maybe stop VM
if (event.state() == State.TERMINATING && !event.failed()
- && shutdownByGuest) {
+ && guestShutdownStops && shutdownByGuest) {
+ logger.info(() -> "Stopping VM because of shutdown by guest.");
PatchOptions patchOpts = new PatchOptions();
patchOpts.setFieldManager("kubernetes-java-kubectl-apply");
var res = vmCrApi.patch(namespace, vmName,
From b207e0226fa7b49b27e1fb370cca1c7b6eab0298 Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Sun, 25 Feb 2024 17:22:06 +0100
Subject: [PATCH 040/379] Fix template error.
---
.../org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml
index d96a66b..64822a5 100644
--- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml
+++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml
@@ -63,7 +63,7 @@ data:
#if>
#if>
- guestShutdownStops: ${ cr.spec.guestShutdownStops!false?string('true', 'false') }
+ guestShutdownStops: ${ cr.spec.vm.guestShutdownStops!false?c }
# Define the VM (required)
vm:
From e25358085fac0fb44c4d67651380138711c401b4 Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Mon, 26 Feb 2024 15:15:55 +0100
Subject: [PATCH 041/379] Move guestShutdownStops up one level.
---
deploy/crds/vms-crd.yaml | 12 ++++++------
dev-example/test-vm.yaml | 4 +++-
.../jdrupes/vmoperator/manager/runnerConfig.ftl.yaml | 5 +++--
3 files changed, 12 insertions(+), 9 deletions(-)
diff --git a/deploy/crds/vms-crd.yaml b/deploy/crds/vms-crd.yaml
index 0292b8d..1863afe 100644
--- a/deploy/crds/vms-crd.yaml
+++ b/deploy/crds/vms-crd.yaml
@@ -933,6 +933,12 @@ spec:
update:
type: boolean
default: true
+ guestShutdownStops:
+ description: >-
+ If true, sets the VM's state to "Stopped" when
+ the VM terminates due to a shutdown by the guest.
+ type: boolean
+ default: false
loadBalancerService:
description: >-
Data to be merged with the loadBalancerService
@@ -999,12 +1005,6 @@ spec:
type: string
enum: [ "Stopped", "Running" ]
default: "Stopped"
- guestShutdownStops:
- description: >-
- If true, sets the state to "Stopped" when
- the VM terminates due to a shutdown by the guest.
- type: boolean
- default: false
machineUuid:
description: >-
The machine's uuid. If none is specified, a uuid
diff --git a/dev-example/test-vm.yaml b/dev-example/test-vm.yaml
index dcb3454..0a8a098 100644
--- a/dev-example/test-vm.yaml
+++ b/dev-example/test-vm.yaml
@@ -13,7 +13,9 @@ spec:
requests:
cpu: 1
memory: 2Gi
-
+
+ guestShutdownStops: true
+
cloudInit: {}
vm:
diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml
index 64822a5..451a465 100644
--- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml
+++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml
@@ -45,6 +45,9 @@ data:
updateTemplate: ${ cr.spec.runnerTemplate.update.asBoolean?c }
#if>
+ # Whether a shutdown initiated by the guest stops the pod deployment
+ guestShutdownStops: ${ cr.spec.guestShutdownStops!false?c }
+
# Forward the cloud-init data if provided
<#if cr.spec.cloudInit??>
cloudInit:
@@ -63,8 +66,6 @@ data:
#if>
#if>
- guestShutdownStops: ${ cr.spec.vm.guestShutdownStops!false?c }
-
# Define the VM (required)
vm:
# The VM's name (required)
From ee2de96c568ab7b2635f831e2a3513532c4475d4 Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp"
Date: Mon, 26 Feb 2024 15:19:29 +0100
Subject: [PATCH 042/379] Fix javadoc.
---
.../jdrupes/vmoperator/runner/qemu/events/ShutdownEvent.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/ShutdownEvent.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/ShutdownEvent.java
index e46bbd3..1804232 100644
--- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/ShutdownEvent.java
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/ShutdownEvent.java
@@ -21,7 +21,7 @@ package org.jdrupes.vmoperator.runner.qemu.events;
import com.fasterxml.jackson.databind.JsonNode;
/**
- * Signals the processing of the {@link QmpShutdown} event.
+ * Signals the reception of a SHUTDOWN event.
*/
public class ShutdownEvent extends MonitorEvent {
From a2641da7f5a7212aa6ea44c3128fa512ed94067c Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp" <1446020+mnlipp@users.noreply.github.com>
Date: Thu, 14 Mar 2024 20:12:37 +0100
Subject: [PATCH 043/379] Refactor internal Kubernetes API and upgrade to
official v19 (#19)
---
checkstyle.xml | 3 +
dev-example/test-vm.yaml | 4 +-
.../.settings/net.sf.jautodoc.prefs | 7 +
org.jdrupes.vmoperator.common/build.gradle | 3 +-
.../org/jdrupes/vmoperator/common/K8s.java | 234 +++---
.../jdrupes/vmoperator/common/K8sClient.java | 759 ++++++++++++++++++
.../vmoperator/common/K8sDynamicModel.java | 114 +++
.../K8sDynamicModelTypeAdapterFactory.java | 130 +++
.../vmoperator/common/K8sDynamicModels.java | 163 ++++
.../vmoperator/common/K8sDynamicStub.java | 109 +++
.../vmoperator/common/K8sGenericStub.java | 418 ++++++++++
.../vmoperator/common/K8sV1ConfigMapStub.java | 60 ++
.../common/K8sV1DeploymentStub.java | 77 ++
.../vmoperator/common/K8sV1PodStub.java | 78 ++
.../common/K8sV1StatefulSetStub.java | 60 ++
.../vmoperator/manager/events/VmChannel.java | 16 +-
.../manager/events/VmDefChanged.java | 8 +-
.../.settings/net.sf.jautodoc.prefs | 2 +-
org.jdrupes.vmoperator.manager/build.gradle | 2 -
.../vmoperator/manager/Controller.java | 38 +-
.../manager/LoadBalancerReconciler.java | 38 +-
.../vmoperator/manager/Reconciler.java | 5 +-
.../manager/StatefulSetReconciler.java | 25 +-
.../jdrupes/vmoperator/manager/VmWatcher.java | 74 +-
.../vmoperator/manager/BasicTests.java | 97 +--
.../build.gradle | 2 +-
.../vmoperator/runner/qemu/StatusUpdater.java | 157 ++--
.../jdrupes/vmoperator/vmconlet/VmConlet.java | 55 +-
28 files changed, 2343 insertions(+), 395 deletions(-)
create mode 100644 org.jdrupes.vmoperator.common/.settings/net.sf.jautodoc.prefs
create mode 100644 org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sClient.java
create mode 100644 org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModel.java
create mode 100644 org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModelTypeAdapterFactory.java
create mode 100644 org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModels.java
create mode 100644 org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicStub.java
create mode 100644 org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sGenericStub.java
create mode 100644 org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1ConfigMapStub.java
create mode 100644 org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1DeploymentStub.java
create mode 100644 org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1PodStub.java
create mode 100644 org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1StatefulSetStub.java
diff --git a/checkstyle.xml b/checkstyle.xml
index 015ef09..088e543 100644
--- a/checkstyle.xml
+++ b/checkstyle.xml
@@ -30,8 +30,11 @@
+
+
+
diff --git a/dev-example/test-vm.yaml b/dev-example/test-vm.yaml
index 0a8a098..19144d5 100644
--- a/dev-example/test-vm.yaml
+++ b/dev-example/test-vm.yaml
@@ -13,11 +13,11 @@ spec:
requests:
cpu: 1
memory: 2Gi
-
+
guestShutdownStops: true
cloudInit: {}
-
+
vm:
# state: Running
bootMenu: yes
diff --git a/org.jdrupes.vmoperator.common/.settings/net.sf.jautodoc.prefs b/org.jdrupes.vmoperator.common/.settings/net.sf.jautodoc.prefs
new file mode 100644
index 0000000..8b8b906
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/.settings/net.sf.jautodoc.prefs
@@ -0,0 +1,7 @@
+add_header=true
+eclipse.preferences.version=1
+header_text=/*\n * VM-Operator\n * Copyright (C) 2024 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 .\n */
+project_specific_settings=true
+visibility_package=false
+visibility_private=false
+visibility_protected=false
diff --git a/org.jdrupes.vmoperator.common/build.gradle b/org.jdrupes.vmoperator.common/build.gradle
index ed082a1..42c05ae 100644
--- a/org.jdrupes.vmoperator.common/build.gradle
+++ b/org.jdrupes.vmoperator.common/build.gradle
@@ -10,5 +10,6 @@ plugins {
dependencies {
api project(':org.jdrupes.vmoperator.util')
- api 'io.kubernetes:client-java:[18.0.0,19)'
+ api 'io.kubernetes:client-java:[19.0.0,20.0.0)'
+ api 'org.yaml:snakeyaml'
}
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8s.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8s.java
index f61b431..e350cf1 100644
--- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8s.java
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8s.java
@@ -1,6 +1,6 @@
/*
* VM-Operator
- * Copyright (C) 2023 Michael N. Lipp
+ * Copyright (C) 2023,2024 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
@@ -18,29 +18,31 @@
package org.jdrupes.vmoperator.common;
+import com.google.gson.JsonObject;
+import io.kubernetes.client.Discovery;
+import io.kubernetes.client.Discovery.APIResource;
import io.kubernetes.client.common.KubernetesListObject;
import io.kubernetes.client.common.KubernetesObject;
+import io.kubernetes.client.common.KubernetesType;
import io.kubernetes.client.custom.V1Patch;
import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.ApiException;
-import io.kubernetes.client.openapi.apis.ApisApi;
-import io.kubernetes.client.openapi.apis.CustomObjectsApi;
-import io.kubernetes.client.openapi.models.V1APIGroup;
-import io.kubernetes.client.openapi.models.V1ConfigMap;
-import io.kubernetes.client.openapi.models.V1ConfigMapList;
-import io.kubernetes.client.openapi.models.V1GroupVersionForDiscovery;
+import io.kubernetes.client.openapi.apis.EventsV1Api;
+import io.kubernetes.client.openapi.models.EventsV1Event;
import io.kubernetes.client.openapi.models.V1ObjectMeta;
import io.kubernetes.client.openapi.models.V1ObjectReference;
-import io.kubernetes.client.openapi.models.V1PersistentVolumeClaim;
-import io.kubernetes.client.openapi.models.V1PersistentVolumeClaimList;
-import io.kubernetes.client.openapi.models.V1Pod;
-import io.kubernetes.client.openapi.models.V1PodList;
+import io.kubernetes.client.util.Strings;
import io.kubernetes.client.util.generic.GenericKubernetesApi;
-import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi;
-import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
-import io.kubernetes.client.util.generic.options.DeleteOptions;
+import io.kubernetes.client.util.generic.KubernetesApiResponse;
import io.kubernetes.client.util.generic.options.PatchOptions;
+import java.io.Reader;
+import java.net.HttpURLConnection;
+import java.time.OffsetDateTime;
+import java.util.Map;
import java.util.Optional;
+import org.yaml.snakeyaml.LoaderOptions;
+import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.constructor.SafeConstructor;
/**
* Helpers for K8s API.
@@ -50,89 +52,80 @@ import java.util.Optional;
public class K8s {
/**
- * Given a groupVersion, returns only the version.
+ * Returns the result from an API call as {@link Optional} if the
+ * call was successful. Returns an empty `Optional` if the status
+ * code is 404 (not found). Else throws an exception.
*
- * @param groupVersion the group version
- * @return the string
+ * @param the generic type
+ * @param response the response
+ * @return the optional
+ * @throws ApiException the API exception
*/
- public static String version(String groupVersion) {
- return groupVersion.substring(groupVersion.lastIndexOf('/') + 1);
+ public static Optional
+ optional(KubernetesApiResponse response) throws ApiException {
+ if (response.isSuccess()) {
+ return Optional.of(response.getObject());
+ }
+ if (response.getHttpStatusCode() == HttpURLConnection.HTTP_NOT_FOUND) {
+ return Optional.empty();
+ }
+ response.throwsApiException();
+ // Never reached
+ return Optional.empty();
}
/**
- * Get PVC API.
+ * Convert Yaml to Json.
*
* @param client the client
- * @return the generic kubernetes api
+ * @param yaml the yaml
+ * @return the json element
*/
- public static GenericKubernetesApi pvcApi(ApiClient client) {
- return new GenericKubernetesApi<>(V1PersistentVolumeClaim.class,
- V1PersistentVolumeClaimList.class, "", "v1",
- "persistentvolumeclaims", client);
+ public static JsonObject yamlToJson(ApiClient client, Reader yaml) {
+ // Avoid Yaml.load due to
+ // https://github.com/kubernetes-client/java/issues/2741
+ @SuppressWarnings("PMD.UseConcurrentHashMap")
+ Map yamlData
+ = new Yaml(new SafeConstructor(new LoaderOptions())).load(yaml);
+
+ // There's no short-cut from Java (collections) to Gson
+ var gson = client.getJSON().getGson();
+ var jsonText = gson.toJson(yamlData);
+ return gson.fromJson(jsonText, JsonObject.class);
}
/**
- * Get config map API.
- *
- * @param client the client
- * @return the generic kubernetes api
- */
- public static GenericKubernetesApi cmApi(ApiClient client) {
- return new GenericKubernetesApi<>(V1ConfigMap.class,
- V1ConfigMapList.class, "", "v1", "configmaps", client);
- }
-
- /**
- * Get pod API.
- *
- * @param client the client
- * @return the generic kubernetes api
- */
- public static GenericKubernetesApi
- podApi(ApiClient client) {
- return new GenericKubernetesApi<>(V1Pod.class, V1PodList.class, "",
- "v1", "pods", client);
- }
-
- /**
- * Get the API for a custom resource.
+ * Lookup the specified API resource. If the version is `null` or
+ * empty, the preferred version in the result is the default
+ * returned from the server.
*
* @param client the client
* @param group the group
+ * @param version the version
* @param kind the kind
- * @param namespace the namespace
- * @param name the name
- * @return the dynamic kubernetes api
+ * @return the optional
* @throws ApiException the api exception
*/
- @SuppressWarnings("PMD.UseObjectForClearerAPI")
- public static Optional crApi(ApiClient client,
- String group, String kind, String namespace, String name)
- throws ApiException {
- var apis = new ApisApi(client).getAPIVersions();
- var crdVersions = apis.getGroups().stream()
- .filter(g -> g.getName().equals(group)).findFirst()
- .map(V1APIGroup::getVersions).stream().flatMap(l -> l.stream())
- .map(V1GroupVersionForDiscovery::getVersion).toList();
- var coa = new CustomObjectsApi(client);
- for (var crdVersion : crdVersions) {
- var crdApiRes = coa.getAPIResources(group, crdVersion)
- .getResources().stream().filter(r -> kind.equals(r.getKind()))
- .findFirst();
- if (crdApiRes.isEmpty()) {
- continue;
- }
- @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
- var crApi = new DynamicKubernetesApi(group,
- crdVersion, crdApiRes.get().getName(), client);
- var customResource = crApi.get(namespace, name);
- if (customResource.isSuccess()) {
- return Optional.of(crApi);
- }
+ public static Optional context(ApiClient client,
+ String group, String version, String kind) throws ApiException {
+ var apiMatch = new Discovery(client).findAll().stream()
+ .filter(r -> r.getGroup().equals(group) && r.getKind().equals(kind)
+ && (Strings.isNullOrEmpty(version)
+ || r.getVersions().contains(version)))
+ .findFirst();
+ if (apiMatch.isEmpty()) {
+ return Optional.empty();
}
- return Optional.empty();
+ var apiRes = apiMatch.get();
+ if (!Strings.isNullOrEmpty(version)) {
+ if (!apiRes.getVersions().contains(version)) {
+ return Optional.empty();
+ }
+ apiRes = new APIResource(apiRes.getGroup(), apiRes.getVersions(),
+ version, apiRes.getKind(), apiRes.getNamespaced(),
+ apiRes.getResourcePlural(), apiRes.getResourceSingular());
+ }
+ return Optional.of(apiRes);
}
/**
@@ -144,6 +137,7 @@ public class K8s {
* @param meta the meta
* @return the object
*/
+ @Deprecated
public static
Optional
get(GenericKubernetesApi api, V1ObjectMeta meta) {
@@ -154,36 +148,6 @@ public class K8s {
return Optional.empty();
}
- /**
- * Delete an object.
- *
- * @param the generic type
- * @param the generic type
- * @param api the api
- * @param object the object
- */
- public static
- void delete(GenericKubernetesApi api, T object)
- throws ApiException {
- api.delete(object.getMetadata().getNamespace(),
- object.getMetadata().getName()).throwsApiException();
- }
-
- /**
- * Delete an object.
- *
- * @param the generic type
- * @param the generic type
- * @param api the api
- * @param object the object
- */
- public static
- void delete(GenericKubernetesApi api, T object,
- DeleteOptions options) throws ApiException {
- api.delete(object.getMetadata().getNamespace(),
- object.getMetadata().getName(), options).throwsApiException();
- }
-
/**
* Apply the given patch data.
*
@@ -213,7 +177,7 @@ public class K8s {
* @return the v 1 object reference
*/
public static V1ObjectReference
- objectReference(DynamicKubernetesObject object) {
+ objectReference(KubernetesObject object) {
return new V1ObjectReference().apiVersion(object.getApiVersion())
.kind(object.getKind())
.namespace(object.getMetadata().getNamespace())
@@ -221,4 +185,54 @@ public class K8s {
.resourceVersion(object.getMetadata().getResourceVersion())
.uid(object.getMetadata().getUid());
}
+
+ /**
+ * Creates an event related to the object, adding reasonable defaults.
+ *
+ * * If `kind` is not set, it is set to "Event".
+ * * If `metadata.namespace` is not set, it is set
+ * to the object's namespace.
+ * * If neither `metadata.name` nor `matadata.generateName` are set,
+ * set `generateName` to the object's name with a dash appended.
+ * * If `reportingInstance` is not set, set it to the object's name.
+ * * If `eventTime` is not set, set it to now.
+ * * If `type` is not set, set it to "Normal"
+ * * If `regarding` is not set, set it to the given object.
+ *
+ * @param event the event
+ * @throws ApiException
+ */
+ @SuppressWarnings("PMD.NPathComplexity")
+ public static void createEvent(ApiClient client,
+ KubernetesObject object, EventsV1Event event)
+ throws ApiException {
+ if (Strings.isNullOrEmpty(event.getKind())) {
+ event.kind("Event");
+ }
+ if (event.getMetadata() == null) {
+ event.metadata(new V1ObjectMeta());
+ }
+ if (Strings.isNullOrEmpty(event.getMetadata().getNamespace())) {
+ event.getMetadata().namespace(object.getMetadata().getNamespace());
+ }
+ if (Strings.isNullOrEmpty(event.getMetadata().getName())
+ && Strings.isNullOrEmpty(event.getMetadata().getGenerateName())) {
+ event.getMetadata()
+ .generateName(object.getMetadata().getName() + "-");
+ }
+ if (Strings.isNullOrEmpty(event.getReportingInstance())) {
+ event.reportingInstance(object.getMetadata().getName());
+ }
+ if (event.getEventTime() == null) {
+ event.eventTime(OffsetDateTime.now());
+ }
+ if (Strings.isNullOrEmpty(event.getType())) {
+ event.type("Normal");
+ }
+ if (event.getRegarding() == null) {
+ event.regarding(objectReference(object));
+ }
+ new EventsV1Api(client).createNamespacedEvent(
+ object.getMetadata().getNamespace(), event, null, null, null, null);
+ }
}
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sClient.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sClient.java
new file mode 100644
index 0000000..b7106fb
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sClient.java
@@ -0,0 +1,759 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2024 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.common;
+
+import io.kubernetes.client.openapi.ApiCallback;
+import io.kubernetes.client.openapi.ApiClient;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.ApiResponse;
+import io.kubernetes.client.openapi.JSON;
+import io.kubernetes.client.openapi.Pair;
+import io.kubernetes.client.openapi.auth.Authentication;
+import io.kubernetes.client.util.ClientBuilder;
+import io.kubernetes.client.util.generic.options.PatchOptions;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Type;
+import java.text.DateFormat;
+import java.time.format.DateTimeFormatter;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import javax.net.ssl.KeyManager;
+import okhttp3.Call;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Request.Builder;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+
+/**
+ * A client with some additional properties.
+ */
+@SuppressWarnings({ "PMD.ExcessivePublicCount", "PMD.TooManyMethods",
+ "PMD.LinguisticNaming", "checkstyle:LineLength" })
+public class K8sClient extends ApiClient {
+
+ private ApiClient apiClient;
+ private PatchOptions defaultPatchOptions;
+
+ /**
+ * Instantiates a new client.
+ *
+ * @throws IOException Signals that an I/O exception has occurred.
+ */
+ public K8sClient() throws IOException {
+ defaultPatchOptions = new PatchOptions();
+ defaultPatchOptions.setFieldManager("kubernetes-java-kubectl-apply");
+ }
+
+ private ApiClient apiClient() {
+ if (apiClient == null) {
+ try {
+ apiClient = ClientBuilder.standard().build();
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ return apiClient;
+ }
+
+ /**
+ * Gets the default patch options.
+ *
+ * @return the defaultPatchOptions
+ */
+ public PatchOptions defaultPatchOptions() {
+ return defaultPatchOptions;
+ }
+
+ /**
+ * Changes the default patch options.
+ *
+ * @param patchOptions the patch options
+ * @return the client
+ */
+ public K8sClient with(PatchOptions patchOptions) {
+ defaultPatchOptions = patchOptions;
+ return this;
+ }
+
+ /**
+ * @return
+ * @see ApiClient#getBasePath()
+ */
+ public String getBasePath() {
+ return apiClient().getBasePath();
+ }
+
+ /**
+ * @param basePath
+ * @return
+ * @see ApiClient#setBasePath(java.lang.String)
+ */
+ public ApiClient setBasePath(String basePath) {
+ return apiClient().setBasePath(basePath);
+ }
+
+ /**
+ * @return
+ * @see ApiClient#getHttpClient()
+ */
+ public OkHttpClient getHttpClient() {
+ return apiClient().getHttpClient();
+ }
+
+ /**
+ * @param newHttpClient
+ * @return
+ * @see ApiClient#setHttpClient(okhttp3.OkHttpClient)
+ */
+ public ApiClient setHttpClient(OkHttpClient newHttpClient) {
+ return apiClient().setHttpClient(newHttpClient);
+ }
+
+ /**
+ * @return
+ * @see ApiClient#getJSON()
+ */
+ @SuppressWarnings("abbreviationAsWordInName")
+ public JSON getJSON() {
+ return apiClient().getJSON();
+ }
+
+ /**
+ * @param json
+ * @return
+ * @see ApiClient#setJSON(io.kubernetes.client.openapi.JSON)
+ */
+ @SuppressWarnings("abbreviationAsWordInName")
+ public ApiClient setJSON(JSON json) {
+ return apiClient().setJSON(json);
+ }
+
+ /**
+ * @return
+ * @see ApiClient#isVerifyingSsl()
+ */
+ public boolean isVerifyingSsl() {
+ return apiClient().isVerifyingSsl();
+ }
+
+ /**
+ * @param verifyingSsl
+ * @return
+ * @see ApiClient#setVerifyingSsl(boolean)
+ */
+ public ApiClient setVerifyingSsl(boolean verifyingSsl) {
+ return apiClient().setVerifyingSsl(verifyingSsl);
+ }
+
+ /**
+ * @return
+ * @see ApiClient#getSslCaCert()
+ */
+ public InputStream getSslCaCert() {
+ return apiClient().getSslCaCert();
+ }
+
+ /**
+ * @param sslCaCert
+ * @return
+ * @see ApiClient#setSslCaCert(java.io.InputStream)
+ */
+ public ApiClient setSslCaCert(InputStream sslCaCert) {
+ return apiClient().setSslCaCert(sslCaCert);
+ }
+
+ /**
+ * @return
+ * @see ApiClient#getKeyManagers()
+ */
+ public KeyManager[] getKeyManagers() {
+ return apiClient().getKeyManagers();
+ }
+
+ /**
+ * @param managers
+ * @return
+ * @see ApiClient#setKeyManagers(javax.net.ssl.KeyManager[])
+ */
+ @SuppressWarnings("PMD.UseVarargs")
+ public ApiClient setKeyManagers(KeyManager[] managers) {
+ return apiClient().setKeyManagers(managers);
+ }
+
+ /**
+ * @return
+ * @see ApiClient#getDateFormat()
+ */
+ public DateFormat getDateFormat() {
+ return apiClient().getDateFormat();
+ }
+
+ /**
+ * @param dateFormat
+ * @return
+ * @see ApiClient#setDateFormat(java.text.DateFormat)
+ */
+ public ApiClient setDateFormat(DateFormat dateFormat) {
+ return apiClient().setDateFormat(dateFormat);
+ }
+
+ /**
+ * @param dateFormat
+ * @return
+ * @see ApiClient#setSqlDateFormat(java.text.DateFormat)
+ */
+ public ApiClient setSqlDateFormat(DateFormat dateFormat) {
+ return apiClient().setSqlDateFormat(dateFormat);
+ }
+
+ /**
+ * @param dateFormat
+ * @return
+ * @see ApiClient#setOffsetDateTimeFormat(java.time.format.DateTimeFormatter)
+ */
+ public ApiClient setOffsetDateTimeFormat(DateTimeFormatter dateFormat) {
+ return apiClient().setOffsetDateTimeFormat(dateFormat);
+ }
+
+ /**
+ * @param dateFormat
+ * @return
+ * @see ApiClient#setLocalDateFormat(java.time.format.DateTimeFormatter)
+ */
+ public ApiClient setLocalDateFormat(DateTimeFormatter dateFormat) {
+ return apiClient().setLocalDateFormat(dateFormat);
+ }
+
+ /**
+ * @param lenientOnJson
+ * @return
+ * @see ApiClient#setLenientOnJson(boolean)
+ */
+ public ApiClient setLenientOnJson(boolean lenientOnJson) {
+ return apiClient().setLenientOnJson(lenientOnJson);
+ }
+
+ /**
+ * @return
+ * @see ApiClient#getAuthentications()
+ */
+ public Map getAuthentications() {
+ return apiClient().getAuthentications();
+ }
+
+ /**
+ * @param authName
+ * @return
+ * @see ApiClient#getAuthentication(java.lang.String)
+ */
+ public Authentication getAuthentication(String authName) {
+ return apiClient().getAuthentication(authName);
+ }
+
+ /**
+ * @param username
+ * @see ApiClient#setUsername(java.lang.String)
+ */
+ public void setUsername(String username) {
+ apiClient().setUsername(username);
+ }
+
+ /**
+ * @param password
+ * @see ApiClient#setPassword(java.lang.String)
+ */
+ public void setPassword(String password) {
+ apiClient().setPassword(password);
+ }
+
+ /**
+ * @param apiKey
+ * @see ApiClient#setApiKey(java.lang.String)
+ */
+ public void setApiKey(String apiKey) {
+ apiClient().setApiKey(apiKey);
+ }
+
+ /**
+ * @param apiKeyPrefix
+ * @see ApiClient#setApiKeyPrefix(java.lang.String)
+ */
+ public void setApiKeyPrefix(String apiKeyPrefix) {
+ apiClient().setApiKeyPrefix(apiKeyPrefix);
+ }
+
+ /**
+ * @param accessToken
+ * @see ApiClient#setAccessToken(java.lang.String)
+ */
+ public void setAccessToken(String accessToken) {
+ apiClient().setAccessToken(accessToken);
+ }
+
+ /**
+ * @param userAgent
+ * @return
+ * @see ApiClient#setUserAgent(java.lang.String)
+ */
+ public ApiClient setUserAgent(String userAgent) {
+ return apiClient().setUserAgent(userAgent);
+ }
+
+ /**
+ * @return
+ * @see java.lang.Object#toString()
+ */
+ public String toString() {
+ return apiClient().toString();
+ }
+
+ /**
+ * @param key
+ * @param value
+ * @return
+ * @see ApiClient#addDefaultHeader(java.lang.String, java.lang.String)
+ */
+ public ApiClient addDefaultHeader(String key, String value) {
+ return apiClient().addDefaultHeader(key, value);
+ }
+
+ /**
+ * @param key
+ * @param value
+ * @return
+ * @see ApiClient#addDefaultCookie(java.lang.String, java.lang.String)
+ */
+ public ApiClient addDefaultCookie(String key, String value) {
+ return apiClient().addDefaultCookie(key, value);
+ }
+
+ /**
+ * @return
+ * @see ApiClient#isDebugging()
+ */
+ public boolean isDebugging() {
+ return apiClient().isDebugging();
+ }
+
+ /**
+ * @param debugging
+ * @return
+ * @see ApiClient#setDebugging(boolean)
+ */
+ public ApiClient setDebugging(boolean debugging) {
+ return apiClient().setDebugging(debugging);
+ }
+
+ /**
+ * @return
+ * @see ApiClient#getTempFolderPath()
+ */
+ public String getTempFolderPath() {
+ return apiClient().getTempFolderPath();
+ }
+
+ /**
+ * @param tempFolderPath
+ * @return
+ * @see ApiClient#setTempFolderPath(java.lang.String)
+ */
+ public ApiClient setTempFolderPath(String tempFolderPath) {
+ return apiClient().setTempFolderPath(tempFolderPath);
+ }
+
+ /**
+ * @return
+ * @see ApiClient#getConnectTimeout()
+ */
+ public int getConnectTimeout() {
+ return apiClient().getConnectTimeout();
+ }
+
+ /**
+ * @param connectionTimeout
+ * @return
+ * @see ApiClient#setConnectTimeout(int)
+ */
+ public ApiClient setConnectTimeout(int connectionTimeout) {
+ return apiClient().setConnectTimeout(connectionTimeout);
+ }
+
+ /**
+ * @return
+ * @see ApiClient#getReadTimeout()
+ */
+ public int getReadTimeout() {
+ return apiClient().getReadTimeout();
+ }
+
+ /**
+ * @param readTimeout
+ * @return
+ * @see ApiClient#setReadTimeout(int)
+ */
+ public ApiClient setReadTimeout(int readTimeout) {
+ return apiClient().setReadTimeout(readTimeout);
+ }
+
+ /**
+ * @return
+ * @see ApiClient#getWriteTimeout()
+ */
+ public int getWriteTimeout() {
+ return apiClient().getWriteTimeout();
+ }
+
+ /**
+ * @param writeTimeout
+ * @return
+ * @see ApiClient#setWriteTimeout(int)
+ */
+ public ApiClient setWriteTimeout(int writeTimeout) {
+ return apiClient().setWriteTimeout(writeTimeout);
+ }
+
+ /**
+ * @param param
+ * @return
+ * @see ApiClient#parameterToString(java.lang.Object)
+ */
+ public String parameterToString(Object param) {
+ return apiClient().parameterToString(param);
+ }
+
+ /**
+ * @param name
+ * @param value
+ * @return
+ * @see ApiClient#parameterToPair(java.lang.String, java.lang.Object)
+ */
+ public List parameterToPair(String name, Object value) {
+ return apiClient().parameterToPair(name, value);
+ }
+
+ /**
+ * @param collectionFormat
+ * @param name
+ * @param value
+ * @return
+ * @see ApiClient#parameterToPairs(java.lang.String, java.lang.String, java.util.Collection)
+ */
+ @SuppressWarnings({ "rawtypes", "PMD.AvoidDuplicateLiterals" })
+ public List parameterToPairs(String collectionFormat, String name,
+ Collection value) {
+ return apiClient().parameterToPairs(collectionFormat, name, value);
+ }
+
+ /**
+ * @param collectionFormat
+ * @param value
+ * @return
+ * @see ApiClient#collectionPathParameterToString(java.lang.String, java.util.Collection)
+ */
+ @SuppressWarnings("rawtypes")
+ public String collectionPathParameterToString(String collectionFormat,
+ Collection value) {
+ return apiClient().collectionPathParameterToString(collectionFormat,
+ value);
+ }
+
+ /**
+ * @param filename
+ * @return
+ * @see ApiClient#sanitizeFilename(java.lang.String)
+ */
+ public String sanitizeFilename(String filename) {
+ return apiClient().sanitizeFilename(filename);
+ }
+
+ /**
+ * @param mime
+ * @return
+ * @see ApiClient#isJsonMime(java.lang.String)
+ */
+ public boolean isJsonMime(String mime) {
+ return apiClient().isJsonMime(mime);
+ }
+
+ /**
+ * @param accepts
+ * @return
+ * @see ApiClient#selectHeaderAccept(java.lang.String[])
+ */
+ @SuppressWarnings("PMD.UseVarargs")
+ public String selectHeaderAccept(String[] accepts) {
+ return apiClient().selectHeaderAccept(accepts);
+ }
+
+ /**
+ * @param contentTypes
+ * @return
+ * @see ApiClient#selectHeaderContentType(java.lang.String[])
+ */
+ @SuppressWarnings("PMD.UseVarargs")
+ public String selectHeaderContentType(String[] contentTypes) {
+ return apiClient().selectHeaderContentType(contentTypes);
+ }
+
+ /**
+ * @param str
+ * @return
+ * @see ApiClient#escapeString(java.lang.String)
+ */
+ public String escapeString(String str) {
+ return apiClient().escapeString(str);
+ }
+
+ /**
+ * @param
+ * @param response
+ * @param returnType
+ * @return
+ * @throws ApiException
+ * @see ApiClient#deserialize(okhttp3.Response, java.lang.reflect.Type)
+ */
+ public T deserialize(Response response, Type returnType)
+ throws ApiException {
+ return apiClient().deserialize(response, returnType);
+ }
+
+ /**
+ * @param obj
+ * @param contentType
+ * @return
+ * @throws ApiException
+ * @see ApiClient#serialize(java.lang.Object, java.lang.String)
+ */
+ public RequestBody serialize(Object obj, String contentType)
+ throws ApiException {
+ return apiClient().serialize(obj, contentType);
+ }
+
+ /**
+ * @param response
+ * @return
+ * @throws ApiException
+ * @see ApiClient#downloadFileFromResponse(okhttp3.Response)
+ */
+ public File downloadFileFromResponse(Response response)
+ throws ApiException {
+ return apiClient().downloadFileFromResponse(response);
+ }
+
+ /**
+ * @param response
+ * @return
+ * @throws IOException
+ * @see ApiClient#prepareDownloadFile(okhttp3.Response)
+ */
+ public File prepareDownloadFile(Response response) throws IOException {
+ return apiClient().prepareDownloadFile(response);
+ }
+
+ /**
+ * @param
+ * @param call
+ * @return
+ * @throws ApiException
+ * @see ApiClient#execute(okhttp3.Call)
+ */
+ public ApiResponse execute(Call call) throws ApiException {
+ return apiClient().execute(call);
+ }
+
+ /**
+ * @param
+ * @param call
+ * @param returnType
+ * @return
+ * @throws ApiException
+ * @see ApiClient#execute(okhttp3.Call, java.lang.reflect.Type)
+ */
+ public ApiResponse execute(Call call, Type returnType)
+ throws ApiException {
+ return apiClient().execute(call, returnType);
+ }
+
+ /**
+ * @param
+ * @param call
+ * @param callback
+ * @see ApiClient#executeAsync(okhttp3.Call, io.kubernetes.client.openapi.ApiCallback)
+ */
+ public void executeAsync(Call call, ApiCallback callback) {
+ apiClient().executeAsync(call, callback);
+ }
+
+ /**
+ * @param
+ * @param call
+ * @param returnType
+ * @param callback
+ * @see ApiClient#executeAsync(okhttp3.Call, java.lang.reflect.Type, io.kubernetes.client.openapi.ApiCallback)
+ */
+ public void executeAsync(Call call, Type returnType,
+ ApiCallback callback) {
+ apiClient().executeAsync(call, returnType, callback);
+ }
+
+ /**
+ * @param
+ * @param response
+ * @param returnType
+ * @return
+ * @throws ApiException
+ * @see ApiClient#handleResponse(okhttp3.Response, java.lang.reflect.Type)
+ */
+ public T handleResponse(Response response, Type returnType)
+ throws ApiException {
+ return apiClient().handleResponse(response, returnType);
+ }
+
+ /**
+ * @param path
+ * @param method
+ * @param queryParams
+ * @param collectionQueryParams
+ * @param body
+ * @param headerParams
+ * @param cookieParams
+ * @param formParams
+ * @param authNames
+ * @param callback
+ * @return
+ * @throws ApiException
+ * @see ApiClient#buildCall(java.lang.String, java.lang.String, java.util.List, java.util.List, java.lang.Object, java.util.Map, java.util.Map, java.util.Map, java.lang.String[], io.kubernetes.client.openapi.ApiCallback)
+ */
+ @SuppressWarnings({ "rawtypes", "PMD.ExcessiveParameterList" })
+ public Call buildCall(String path, String method, List queryParams,
+ List collectionQueryParams, Object body,
+ Map headerParams, Map cookieParams,
+ Map formParams, String[] authNames,
+ ApiCallback callback) throws ApiException {
+ return apiClient().buildCall(path, method, queryParams,
+ collectionQueryParams, body, headerParams, cookieParams, formParams,
+ authNames, callback);
+ }
+
+ /**
+ * @param path
+ * @param method
+ * @param queryParams
+ * @param collectionQueryParams
+ * @param body
+ * @param headerParams
+ * @param cookieParams
+ * @param formParams
+ * @param authNames
+ * @param callback
+ * @return
+ * @throws ApiException
+ * @see ApiClient#buildRequest(java.lang.String, java.lang.String, java.util.List, java.util.List, java.lang.Object, java.util.Map, java.util.Map, java.util.Map, java.lang.String[], io.kubernetes.client.openapi.ApiCallback)
+ */
+ @SuppressWarnings({ "rawtypes", "PMD.ExcessiveParameterList" })
+ public Request buildRequest(String path, String method,
+ List queryParams, List collectionQueryParams,
+ Object body, Map headerParams,
+ Map cookieParams, Map formParams,
+ String[] authNames, ApiCallback callback) throws ApiException {
+ return apiClient().buildRequest(path, method, queryParams,
+ collectionQueryParams, body, headerParams, cookieParams, formParams,
+ authNames, callback);
+ }
+
+ /**
+ * @param path
+ * @param queryParams
+ * @param collectionQueryParams
+ * @return
+ * @see ApiClient#buildUrl(java.lang.String, java.util.List, java.util.List)
+ */
+ public String buildUrl(String path, List queryParams,
+ List collectionQueryParams) {
+ return apiClient().buildUrl(path, queryParams, collectionQueryParams);
+ }
+
+ /**
+ * @param headerParams
+ * @param reqBuilder
+ * @see ApiClient#processHeaderParams(java.util.Map, okhttp3.Request.Builder)
+ */
+ public void processHeaderParams(Map headerParams,
+ Builder reqBuilder) {
+ apiClient().processHeaderParams(headerParams, reqBuilder);
+ }
+
+ /**
+ * @param cookieParams
+ * @param reqBuilder
+ * @see ApiClient#processCookieParams(java.util.Map, okhttp3.Request.Builder)
+ */
+ public void processCookieParams(Map cookieParams,
+ Builder reqBuilder) {
+ apiClient().processCookieParams(cookieParams, reqBuilder);
+ }
+
+ /**
+ * @param authNames
+ * @param queryParams
+ * @param headerParams
+ * @param cookieParams
+ * @see ApiClient#updateParamsForAuth(java.lang.String[], java.util.List, java.util.Map, java.util.Map)
+ */
+ public void updateParamsForAuth(String[] authNames, List queryParams,
+ Map headerParams,
+ Map cookieParams) {
+ apiClient().updateParamsForAuth(authNames, queryParams, headerParams,
+ cookieParams);
+ }
+
+ /**
+ * @param formParams
+ * @return
+ * @see ApiClient#buildRequestBodyFormEncoding(java.util.Map)
+ */
+ public RequestBody
+ buildRequestBodyFormEncoding(Map formParams) {
+ return apiClient().buildRequestBodyFormEncoding(formParams);
+ }
+
+ /**
+ * @param formParams
+ * @return
+ * @see ApiClient#buildRequestBodyMultipart(java.util.Map)
+ */
+ public RequestBody
+ buildRequestBodyMultipart(Map formParams) {
+ return apiClient().buildRequestBodyMultipart(formParams);
+ }
+
+ /**
+ * @param file
+ * @return
+ * @see ApiClient#guessContentTypeFromFile(java.io.File)
+ */
+ public String guessContentTypeFromFile(File file) {
+ return apiClient().guessContentTypeFromFile(file);
+ }
+
+}
\ No newline at end of file
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModel.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModel.java
new file mode 100644
index 0000000..6a4410f
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModel.java
@@ -0,0 +1,114 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2024 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.common;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import io.kubernetes.client.common.KubernetesObject;
+import io.kubernetes.client.openapi.models.V1ObjectMeta;
+
+/**
+ * Represents a Kubernetes object using a JSON data structure.
+ * Some information that is common to all Kubernetes objects,
+ * notably the metadata, is made available through the methods
+ * defined by {@link KubernetesObject}.
+ */
+@SuppressWarnings("PMD.DataClass")
+public class K8sDynamicModel implements KubernetesObject {
+
+ private final V1ObjectMeta metadata;
+ private final JsonObject data;
+
+ /**
+ * Instantiates a new model from the JSON representation.
+ *
+ * @param delegate the gson instance to use for extracting structured data
+ * @param json the JSON
+ */
+ public K8sDynamicModel(Gson delegate, JsonObject json) {
+ this.data = json;
+ metadata = delegate.fromJson(data.get("metadata"), V1ObjectMeta.class);
+ }
+
+ @Override
+ public String getApiVersion() {
+ return apiVersion();
+ }
+
+ /**
+ * Gets the API version. (Abbreviated method name for convenience.)
+ *
+ * @return the API version
+ */
+ public String apiVersion() {
+ return data.get("apiVersion").getAsString();
+ }
+
+ @Override
+ public String getKind() {
+ return kind();
+ }
+
+ /**
+ * Gets the kind. (Abbreviated method name for convenience.)
+ *
+ * @return the kind
+ */
+ public String kind() {
+ return data.get("kind").getAsString();
+ }
+
+ @Override
+ public V1ObjectMeta getMetadata() {
+ return metadata;
+ }
+
+ /**
+ * Gets the metadata. (Abbreviated method name for convenience.)
+ *
+ * @return the metadata
+ */
+ public V1ObjectMeta metadata() {
+ return metadata;
+ }
+
+ /**
+ * Gets the data.
+ *
+ * @return the data
+ */
+ public JsonObject data() {
+ return data;
+ }
+
+ /**
+ * Convenience method for getting the status.
+ *
+ * @return the JSON object describing the status
+ */
+ public JsonObject status() {
+ return data.getAsJsonObject("status");
+ }
+
+ @Override
+ public String toString() {
+ return data.toString();
+ }
+
+}
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModelTypeAdapterFactory.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModelTypeAdapterFactory.java
new file mode 100644
index 0000000..9018744
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModelTypeAdapterFactory.java
@@ -0,0 +1,130 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2024 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.common;
+
+import com.google.gson.Gson;
+import com.google.gson.InstanceCreator;
+import com.google.gson.JsonObject;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.lang.reflect.Type;
+
+/**
+ * A factory for creating K8sDynamicModel(s) objects.
+ */
+public class K8sDynamicModelTypeAdapterFactory implements TypeAdapterFactory {
+
+ /**
+ * Creates a type adapter for the given type.
+ *
+ * @param the generic type
+ * @param gson the gson
+ * @param typeToken the type token
+ * @return the type adapter or null if the type is not handles by
+ * this factory
+ */
+ @SuppressWarnings("unchecked")
+ public TypeAdapter create(Gson gson, TypeToken typeToken) {
+ if (TypeToken.get(K8sDynamicModel.class).equals(typeToken)) {
+ return (TypeAdapter) (new K8sDynamicModelCreator(gson));
+ }
+ if (TypeToken.get(K8sDynamicModels.class).equals(typeToken)) {
+ return (TypeAdapter) (new K8sDynamicModelsCreator(gson));
+ }
+ return null;
+ }
+
+ /**
+ * The Class K8sDynamicModelCreator.
+ */
+ /* default */ class K8sDynamicModelCreator
+ extends TypeAdapter
+ implements InstanceCreator {
+ private final Gson delegate;
+
+ /**
+ * Instantiates a new object state creator.
+ *
+ * @param delegate the delegate
+ */
+ public K8sDynamicModelCreator(Gson delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public K8sDynamicModel createInstance(Type type) {
+ return new K8sDynamicModel(delegate, null);
+ }
+
+ @Override
+ public void write(JsonWriter jsonWriter, K8sDynamicModel state)
+ throws IOException {
+ jsonWriter.jsonValue(delegate.toJson(state.data()));
+ }
+
+ @Override
+ public K8sDynamicModel read(JsonReader jsonReader)
+ throws IOException {
+ return new K8sDynamicModel(delegate,
+ delegate.fromJson(jsonReader, JsonObject.class));
+ }
+ }
+
+ /**
+ * The Class K8sDynamicModelsCreator.
+ */
+ /* default */class K8sDynamicModelsCreator
+ extends TypeAdapter
+ implements InstanceCreator {
+
+ private final Gson delegate;
+
+ /**
+ * Instantiates a new object states creator.
+ *
+ * @param delegate the delegate
+ */
+ public K8sDynamicModelsCreator(Gson delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public K8sDynamicModels createInstance(Type type) {
+ return new K8sDynamicModels(delegate, null);
+ }
+
+ @Override
+ public void write(JsonWriter jsonWriter, K8sDynamicModels states)
+ throws IOException {
+ jsonWriter.jsonValue(delegate.toJson(states.data()));
+ }
+
+ @Override
+ public K8sDynamicModels read(JsonReader jsonReader)
+ throws IOException {
+ return new K8sDynamicModels(delegate,
+ delegate.fromJson(jsonReader, JsonObject.class));
+ }
+ }
+
+}
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModels.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModels.java
new file mode 100644
index 0000000..165b10e
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModels.java
@@ -0,0 +1,163 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2024 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.common;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import io.kubernetes.client.common.KubernetesListObject;
+import io.kubernetes.client.openapi.Configuration;
+import io.kubernetes.client.openapi.models.V1ListMeta;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents a list of Kubernetes objects each of which is
+ * represented using a JSON data structure.
+ * Some information that is common to all Kubernetes objects,
+ * notably the metadata, is made available through the methods
+ * defined by {@link KubernetesListObject}.
+ */
+public class K8sDynamicModels implements KubernetesListObject {
+
+ private final JsonObject data;
+ private final V1ListMeta metadata;
+ private final List items;
+
+ /**
+ * Initialize the object list using the given JSON data.
+ *
+ * @param delegate the gson instance to use for extracting structured data
+ * @param data the data
+ */
+ public K8sDynamicModels(Gson delegate, JsonObject data) {
+ this.data = data;
+ metadata = delegate.fromJson(data.get("metadata"), V1ListMeta.class);
+ items = new ArrayList<>();
+ for (JsonElement e : data.get("items").getAsJsonArray()) {
+ items.add(new K8sDynamicModel(delegate, e.getAsJsonObject()));
+ }
+ }
+
+ @Override
+ public String getApiVersion() {
+ return apiVersion();
+ }
+
+ /**
+ * Gets the API version. (Abbreviated method name for convenience.)
+ *
+ * @return the API version
+ */
+ public String apiVersion() {
+ return data.get("apiVersion").getAsString();
+ }
+
+ @Override
+ public String getKind() {
+ return kind();
+ }
+
+ /**
+ * Gets the kind. (Abbreviated method name for convenience.)
+ *
+ * @return the kind
+ */
+ public String kind() {
+ return data.get("kind").getAsString();
+ }
+
+ @Override
+ public V1ListMeta getMetadata() {
+ return metadata;
+ }
+
+ /**
+ * Gets the metadata. (Abbreviated method name for convenience.)
+ *
+ * @return the metadata
+ */
+ public V1ListMeta metadata() {
+ return metadata;
+ }
+
+ /**
+ * Returns the JSON representation of this object.
+ *
+ * @return the JOSN representation
+ */
+ public JsonObject data() {
+ return data;
+ }
+
+ @Override
+ public List getItems() {
+ return items;
+ }
+
+ /**
+ * Sets the api version.
+ *
+ * @param apiVersion the new api version
+ */
+ public void setApiVersion(String apiVersion) {
+ data.addProperty("apiVersion", apiVersion);
+ }
+
+ /**
+ * Sets the kind.
+ *
+ * @param kind the new kind
+ */
+ public void setKind(String kind) {
+ data.addProperty("kind", kind);
+ }
+
+ /**
+ * Sets the metadata.
+ *
+ * @param objectMeta the new metadata
+ */
+ public void setMetadata(V1ListMeta objectMeta) {
+ data.add("metadata",
+ Configuration.getDefaultApiClient().getJSON().getGson()
+ .toJsonTree(objectMeta));
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(data);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ K8sDynamicModels other = (K8sDynamicModels) obj;
+ return Objects.equals(data, other.data);
+ }
+}
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicStub.java
new file mode 100644
index 0000000..1ab33ca
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicStub.java
@@ -0,0 +1,109 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2024 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.common;
+
+import io.kubernetes.client.Discovery.APIResource;
+import io.kubernetes.client.apimachinery.GroupVersionKind;
+import io.kubernetes.client.openapi.ApiException;
+import java.io.Reader;
+
+/**
+ * A stub for namespaced custom objects. It uses a dynamic model
+ * (see {@link K8sDynamicModel}) for representing the object's
+ * state and can therefore be used for any kind of object, especially
+ * custom objects.
+ */
+@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
+public class K8sDynamicStub
+ extends K8sGenericStub {
+
+ /**
+ * Instantiates a new dynamic stub.
+ *
+ * @param objectClass the object class
+ * @param objectListClass the object list class
+ * @param client the client
+ * @param context the context
+ * @param namespace the namespace
+ * @param name the name
+ */
+ public K8sDynamicStub(Class objectClass,
+ Class objectListClass, K8sClient client,
+ APIResource context, String namespace, String name) {
+ super(objectClass, objectListClass, client, context, namespace, name);
+ }
+
+ /**
+ * Get a dynamic object stub. If the version in parameter
+ * `gvk` is an empty string, the stub refers to the first object with
+ * matching group and kind.
+ *
+ * @param client the client
+ * @param gvk the group, version and kind
+ * @param namespace the namespace
+ * @param name the name
+ * @return the stub if the object exists
+ * @throws ApiException the api exception
+ */
+ @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
+ "PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
+ public static K8sDynamicStub get(K8sClient client,
+ GroupVersionKind gvk, String namespace, String name)
+ throws ApiException {
+ return K8sGenericStub.get(K8sDynamicModel.class, K8sDynamicModels.class,
+ client, gvk, namespace, name, K8sDynamicStub::new);
+ }
+
+ /**
+ * Get a dynamic object stub.
+ *
+ * @param client the client
+ * @param context the context
+ * @param namespace the namespace
+ * @param name the name
+ * @return the stub if the object exists
+ * @throws ApiException the api exception
+ */
+ @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
+ "PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
+ public static K8sDynamicStub get(K8sClient client,
+ APIResource context, String namespace, String name)
+ throws ApiException {
+ return K8sGenericStub.get(K8sDynamicModel.class, K8sDynamicModels.class,
+ client, context, namespace, name, K8sDynamicStub::new);
+ }
+
+ /**
+ * Creates a stub from yaml.
+ *
+ * @param client the client
+ * @param context the context
+ * @param yaml the yaml
+ * @return the k 8 s dynamic stub
+ * @throws ApiException the api exception
+ */
+ public static K8sDynamicStub createFromYaml(K8sClient client,
+ APIResource context, Reader yaml) throws ApiException {
+ var model = new K8sDynamicModel(client.getJSON().getGson(),
+ K8s.yamlToJson(client, yaml));
+ return K8sGenericStub.create(K8sDynamicModel.class,
+ K8sDynamicModels.class, client, context, model,
+ K8sDynamicStub::new);
+ }
+}
\ No newline at end of file
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sGenericStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sGenericStub.java
new file mode 100644
index 0000000..30c6699
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sGenericStub.java
@@ -0,0 +1,418 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2024 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.common;
+
+import com.google.gson.Gson;
+import io.kubernetes.client.Discovery.APIResource;
+import io.kubernetes.client.apimachinery.GroupVersionKind;
+import io.kubernetes.client.common.KubernetesListObject;
+import io.kubernetes.client.common.KubernetesObject;
+import io.kubernetes.client.custom.V1Patch;
+import io.kubernetes.client.openapi.ApiClient;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.util.Strings;
+import io.kubernetes.client.util.generic.GenericKubernetesApi;
+import io.kubernetes.client.util.generic.options.ListOptions;
+import io.kubernetes.client.util.generic.options.PatchOptions;
+import java.net.HttpURLConnection;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Optional;
+import java.util.function.Function;
+
+/**
+ * A stub for namespaced custom objects. This stub provides the
+ * functions common to all Kubernetes objects, but uses variables
+ * for all types. This class should be used as base class only.
+ *
+ * @param the generic type
+ * @param the generic type
+ */
+@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
+public class K8sGenericStub {
+ protected final K8sClient client;
+ private final GenericKubernetesApi api;
+ protected final String group;
+ protected final String version;
+ protected final String kind;
+ protected final String plural;
+ protected final String namespace;
+ protected final String name;
+
+ /**
+ * Get a namespaced object stub. If the version in parameter
+ * `gvk` is an empty string, the stub refers to the first object
+ * found with matching group and kind.
+ *
+ * @param the object type
+ * @param the object list type
+ * @param the stub type
+ * @param objectClass the object class
+ * @param objectListClass the object list class
+ * @param client the client
+ * @param gvk the group, version and kind
+ * @param namespace the namespace
+ * @param name the name
+ * @param provider the provider
+ * @return the stub if the object exists
+ * @throws ApiException the api exception
+ */
+ @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
+ "PMD.AvoidInstantiatingObjectsInLoops" })
+ public static >
+ R get(Class objectClass, Class objectListClass,
+ K8sClient client, GroupVersionKind gvk, String namespace,
+ String name, GenericSupplier provider)
+ throws ApiException {
+ var context = K8s.context(client, gvk.getGroup(), gvk.getVersion(),
+ gvk.getKind());
+ if (context.isEmpty()) {
+ throw new ApiException("No known API for " + gvk.getGroup()
+ + "/" + gvk.getVersion() + " " + gvk.getKind());
+ }
+ return provider.get(objectClass, objectListClass, client, context.get(),
+ namespace, name);
+ }
+
+ /**
+ * Get a namespaced object stub.
+ *
+ * @param the object type
+ * @param the object list type
+ * @param the stub type
+ * @param objectClass the object class
+ * @param objectListClass the object list class
+ * @param client the client
+ * @param context the context
+ * @param namespace the namespace
+ * @param name the name
+ * @param provider the provider
+ * @return the stub if the object exists
+ * @throws ApiException the api exception
+ */
+ @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
+ "PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
+ public static >
+ R get(Class objectClass, Class objectListClass,
+ K8sClient client, APIResource context, String namespace,
+ String name, GenericSupplier provider)
+ throws ApiException {
+ return provider.get(objectClass, objectListClass, client,
+ context, namespace, name);
+ }
+
+ /**
+ * Get a namespaced object stub for a newly created object.
+ *
+ * @param the object type
+ * @param the object list type
+ * @param the stub type
+ * @param objectClass the object class
+ * @param objectListClass the object list class
+ * @param client the client
+ * @param context the context
+ * @param model the model
+ * @param provider the provider
+ * @return the stub if the object exists
+ * @throws ApiException the api exception
+ */
+ @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
+ "PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
+ public static >
+ R create(Class objectClass, Class objectListClass,
+ K8sClient client, APIResource context, O model,
+ GenericSupplier provider) throws ApiException {
+ var api = new GenericKubernetesApi<>(objectClass, objectListClass,
+ context.getGroup(), context.getPreferredVersion(),
+ context.getResourcePlural(), client);
+ api.create(model).throwsApiException();
+ return provider.get(objectClass, objectListClass, client,
+ context, model.getMetadata().getNamespace(),
+ model.getMetadata().getName());
+ }
+
+ /**
+ * Get the stubs for the objects in the given namespace that match
+ * the criteria from the given options.
+ *
+ * @param the object type
+ * @param the object list type
+ * @param the stub type
+ * @param objectClass the object class
+ * @param objectListClass the object list class
+ * @param client the client
+ * @param context the context
+ * @param namespace the namespace
+ * @param options the options
+ * @param provider the provider
+ * @return the collection
+ * @throws ApiException the api exception
+ */
+ public static >
+ Collection list(Class objectClass, Class objectListClass,
+ K8sClient client, APIResource context, String namespace,
+ ListOptions options, SpecificSupplier provider)
+ throws ApiException {
+ var api = new GenericKubernetesApi<>(objectClass, objectListClass,
+ context.getGroup(), context.getPreferredVersion(),
+ context.getResourcePlural(), client);
+ var objs = api.list(namespace, options).throwsApiException();
+ var result = new ArrayList();
+ for (var item : objs.getObject().getItems()) {
+ result.add(
+ provider.get(client, namespace, item.getMetadata().getName()));
+ }
+ return result;
+ }
+
+ /**
+ * Instantiates a new namespaced custom object stub.
+ *
+ * @param objectClass the object class
+ * @param objectListClass the object list class
+ * @param client the client
+ * @param context the context
+ * @param namespace the namespace
+ * @param name the name
+ */
+ protected K8sGenericStub(Class objectClass, Class objectListClass,
+ K8sClient client, APIResource context, String namespace,
+ String name) {
+ this.client = client;
+ group = context.getGroup();
+ version = context.getPreferredVersion();
+ kind = context.getKind();
+ plural = context.getResourcePlural();
+ this.namespace = namespace;
+ this.name = name;
+
+ Gson gson = client.getJSON().getGson();
+ if (!checkAdapters(client)) {
+ client.getJSON().setGson(gson.newBuilder()
+ .registerTypeAdapterFactory(
+ new K8sDynamicModelTypeAdapterFactory())
+ .create());
+ }
+ api = new GenericKubernetesApi<>(objectClass,
+ objectListClass, group, version, plural, client);
+ }
+
+ private boolean checkAdapters(ApiClient client) {
+ return K8sDynamicModelTypeAdapterFactory.K8sDynamicModelCreator.class
+ .equals(client.getJSON().getGson().getAdapter(K8sDynamicModel.class)
+ .getClass())
+ && K8sDynamicModelTypeAdapterFactory.K8sDynamicModelsCreator.class
+ .equals(client.getJSON().getGson()
+ .getAdapter(K8sDynamicModels.class).getClass());
+ }
+
+ /**
+ * Gets the group.
+ *
+ * @return the group
+ */
+ public String group() {
+ return group;
+ }
+
+ /**
+ * Gets the version.
+ *
+ * @return the version
+ */
+ public String version() {
+ return version;
+ }
+
+ /**
+ * Gets the kind.
+ *
+ * @return the kind
+ */
+ public String kind() {
+ return kind;
+ }
+
+ /**
+ * Gets the plural.
+ *
+ * @return the plural
+ */
+ public String plural() {
+ return plural;
+ }
+
+ /**
+ * Gets the namespace.
+ *
+ * @return the namespace
+ */
+ public String namespace() {
+ return namespace;
+ }
+
+ /**
+ * Gets the name.
+ *
+ * @return the name
+ */
+ public String name() {
+ return name;
+ }
+
+ /**
+ * Delete the Kubernetes object.
+ *
+ * @throws ApiException the API exception
+ */
+ public void delete() throws ApiException {
+ var result = api.delete(namespace, name);
+ if (result.isSuccess()
+ || result.getHttpStatusCode() == HttpURLConnection.HTTP_NOT_FOUND) {
+ return;
+ }
+ result.throwsApiException();
+ }
+
+ /**
+ * Retrieves and returns the current state of the object.
+ *
+ * @return the object's state
+ * @throws ApiException the api exception
+ */
+ public Optional model() throws ApiException {
+ return K8s.optional(api.get(namespace, name));
+ }
+
+ /**
+ * Updates the object's status.
+ *
+ * @param object the current state of the object (passed to `status`)
+ * @param status function that returns the new status
+ * @return the updated model or empty if not successful
+ * @throws ApiException the api exception
+ */
+ public Optional updateStatus(O object,
+ Function status) throws ApiException {
+ return K8s.optional(api.updateStatus(object, status));
+ }
+
+ /**
+ * Updates the status.
+ *
+ * @param status the status
+ * @return the kubernetes api response
+ * the updated model or empty if not successful
+ * @throws ApiException the api exception
+ */
+ public Optional updateStatus(Function status)
+ throws ApiException {
+ return updateStatus(
+ api.get(namespace, name).throwsApiException().getObject(), status);
+ }
+
+ /**
+ * Patch the object.
+ *
+ * @param patchType the patch type
+ * @param patch the patch
+ * @param options the options
+ * @return the kubernetes api response
+ * @throws ApiException the api exception
+ */
+ public Optional patch(String patchType, V1Patch patch,
+ PatchOptions options) throws ApiException {
+ return K8s
+ .optional(api.patch(namespace, name, patchType, patch, options));
+ }
+
+ /**
+ * Patch the object using default options.
+ *
+ * @param patchType the patch type
+ * @param patch the patch
+ * @return the kubernetes api response
+ * @throws ApiException the api exception
+ */
+ public Optional
+ patch(String patchType, V1Patch patch) throws ApiException {
+ PatchOptions opts = new PatchOptions();
+ return patch(patchType, patch, opts);
+ }
+
+ /**
+ * A supplier for generic stubs.
+ *
+ * @param the object type
+ * @param the object list type
+ * @param the result type
+ */
+ public interface GenericSupplier> {
+
+ /**
+ * Gets a new stub.
+ *
+ * @param objectClass the object class
+ * @param objectListClass the object list class
+ * @param client the client
+ * @param context the API resource
+ * @param namespace the namespace
+ * @param name the name
+ * @return the result
+ */
+ @SuppressWarnings("PMD.UseObjectForClearerAPI")
+ R get(Class objectClass, Class objectListClass, K8sClient client,
+ APIResource context, String namespace, String name);
+ }
+
+ /**
+ * A supplier for specific stubs.
+ *
+ * @param the object type
+ * @param the object list type
+ * @param the result type
+ */
+ public interface SpecificSupplier> {
+
+ /**
+ * Gets a new stub.
+ *
+ * @param client the client
+ * @param namespace the namespace
+ * @param name the name
+ * @return the result
+ */
+ R get(K8sClient client, String namespace, String name);
+ }
+
+ @Override
+ @SuppressWarnings("PMD.UseLocaleWithCaseConversions")
+ public String toString() {
+ return (Strings.isNullOrEmpty(group) ? "" : group + "/")
+ + version.toUpperCase() + kind + " " + namespace + ":" + name;
+ }
+
+}
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1ConfigMapStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1ConfigMapStub.java
new file mode 100644
index 0000000..58a9516
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1ConfigMapStub.java
@@ -0,0 +1,60 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2024 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.common;
+
+import io.kubernetes.client.Discovery.APIResource;
+import io.kubernetes.client.openapi.models.V1ConfigMap;
+import io.kubernetes.client.openapi.models.V1ConfigMapList;
+import java.util.List;
+
+/**
+ * A stub for config maps (v1).
+ */
+@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
+public class K8sV1ConfigMapStub
+ extends K8sGenericStub {
+
+ /**
+ * Instantiates a new stub.
+ *
+ * @param client the client
+ * @param namespace the namespace
+ * @param name the name
+ */
+ protected K8sV1ConfigMapStub(K8sClient client, String namespace,
+ String name) {
+ super(V1ConfigMap.class, V1ConfigMapList.class, client,
+ new APIResource("", List.of("v1"), "v1", "ConfigMap", true,
+ "configmaps", "configmap"),
+ namespace, name);
+ }
+
+ /**
+ * Gets the stub for the given namespace and name.
+ *
+ * @param client the client
+ * @param namespace the namespace
+ * @param name the name
+ * @return the config map stub
+ */
+ public static K8sV1ConfigMapStub get(K8sClient client, String namespace,
+ String name) {
+ return new K8sV1ConfigMapStub(client, namespace, name);
+ }
+}
\ No newline at end of file
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1DeploymentStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1DeploymentStub.java
new file mode 100644
index 0000000..049363d
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1DeploymentStub.java
@@ -0,0 +1,77 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2024 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.common;
+
+import io.kubernetes.client.Discovery.APIResource;
+import io.kubernetes.client.custom.V1Patch;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.models.V1Deployment;
+import io.kubernetes.client.openapi.models.V1DeploymentList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * A stub for pods (v1).
+ */
+@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
+public class K8sV1DeploymentStub
+ extends K8sGenericStub {
+
+ /**
+ * Instantiates a new stub.
+ *
+ * @param client the client
+ * @param namespace the namespace
+ * @param name the name
+ */
+ protected K8sV1DeploymentStub(K8sClient client, String namespace,
+ String name) {
+ super(V1Deployment.class, V1DeploymentList.class, client,
+ new APIResource("apps", List.of("v1"), "v1", "Pod", true,
+ "deployments", "deployment"),
+ namespace, name);
+ }
+
+ /**
+ * Gets the stub for the given namespace and name.
+ *
+ * @param client the client
+ * @param namespace the namespace
+ * @param name the name
+ * @return the deployment stub
+ */
+ public static K8sV1DeploymentStub get(K8sClient client, String namespace,
+ String name) {
+ return new K8sV1DeploymentStub(client, namespace, name);
+ }
+
+ /**
+ * Scales the deployment.
+ *
+ * @param replicas the replicas
+ * @return the new model or empty if not successful
+ * @throws ApiException the API exception
+ */
+ public Optional scale(int replicas) throws ApiException {
+ return patch(V1Patch.PATCH_FORMAT_JSON_PATCH,
+ new V1Patch("[{\"op\": \"replace\", \"path\": \"/spec/replicas"
+ + "\", \"value\": " + replicas + "}]"),
+ client.defaultPatchOptions());
+ }
+}
\ No newline at end of file
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1PodStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1PodStub.java
new file mode 100644
index 0000000..fe47a0f
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1PodStub.java
@@ -0,0 +1,78 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2024 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.common;
+
+import io.kubernetes.client.Discovery.APIResource;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.models.V1Pod;
+import io.kubernetes.client.openapi.models.V1PodList;
+import io.kubernetes.client.util.generic.options.ListOptions;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * A stub for pods (v1).
+ */
+@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
+public class K8sV1PodStub extends K8sGenericStub {
+
+ public static final APIResource CONTEXT
+ = new APIResource("", List.of("v1"), "v1", "Pod", true, "pods", "pod");
+
+ /**
+ * Instantiates a new stub.
+ *
+ * @param client the client
+ * @param namespace the namespace
+ * @param name the name
+ */
+ protected K8sV1PodStub(K8sClient client, String namespace, String name) {
+ super(V1Pod.class, V1PodList.class, client, CONTEXT, namespace, name);
+ }
+
+ /**
+ * Gets the stub for the given namespace and name.
+ *
+ * @param client the client
+ * @param namespace the namespace
+ * @param name the name
+ * @return the kpod stub
+ */
+ public static K8sV1PodStub get(K8sClient client, String namespace,
+ String name) {
+ return new K8sV1PodStub(client, namespace, name);
+ }
+
+ /**
+ * Get the stubs for the objects in the given namespace that match
+ * the criteria from the given options.
+ *
+ * @param client the client
+ * @param namespace the namespace
+ * @param options the options
+ * @return the collection
+ * @throws ApiException the api exception
+ */
+ public static Collection list(K8sClient client,
+ String namespace, ListOptions options) throws ApiException {
+ return K8sGenericStub.list(V1Pod.class, V1PodList.class, client,
+ CONTEXT, namespace, options, K8sV1PodStub::new);
+ }
+
+}
\ No newline at end of file
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1StatefulSetStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1StatefulSetStub.java
new file mode 100644
index 0000000..13462b9
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1StatefulSetStub.java
@@ -0,0 +1,60 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2024 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.common;
+
+import io.kubernetes.client.Discovery.APIResource;
+import io.kubernetes.client.openapi.models.V1StatefulSet;
+import io.kubernetes.client.openapi.models.V1StatefulSetList;
+import java.util.List;
+
+/**
+ * A stub for stateful sets (v1).
+ */
+@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
+public class K8sV1StatefulSetStub
+ extends K8sGenericStub {
+
+ /**
+ * Instantiates a new stub.
+ *
+ * @param client the client
+ * @param namespace the namespace
+ * @param name the name
+ */
+ protected K8sV1StatefulSetStub(K8sClient client, String namespace,
+ String name) {
+ super(V1StatefulSet.class, V1StatefulSetList.class, client,
+ new APIResource("apps", List.of("v1"), "v1", "StatefulSet", true,
+ "statefulsets", "statefulset"),
+ namespace, name);
+ }
+
+ /**
+ * Gets the stub for the given namespace and name.
+ *
+ * @param client the client
+ * @param namespace the namespace
+ * @param name the name
+ * @return the stateful set stub
+ */
+ public static K8sV1StatefulSetStub get(K8sClient client, String namespace,
+ String name) {
+ return new K8sV1StatefulSetStub(client, namespace, name);
+ }
+}
\ No newline at end of file
diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmChannel.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmChannel.java
index bc06e68..972693a 100644
--- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmChannel.java
+++ b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmChannel.java
@@ -18,8 +18,8 @@
package org.jdrupes.vmoperator.manager.events;
-import io.kubernetes.client.openapi.ApiClient;
-import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
+import org.jdrupes.vmoperator.common.K8sClient;
+import org.jdrupes.vmoperator.common.K8sDynamicModel;
import org.jgrapes.core.Channel;
import org.jgrapes.core.EventPipeline;
import org.jgrapes.core.Subchannel.DefaultSubchannel;
@@ -31,8 +31,8 @@ import org.jgrapes.core.Subchannel.DefaultSubchannel;
public class VmChannel extends DefaultSubchannel {
private final EventPipeline pipeline;
- private final ApiClient client;
- private DynamicKubernetesObject vmDefinition;
+ private final K8sClient client;
+ private K8sDynamicModel vmDefinition;
private long generation = -1;
/**
@@ -43,7 +43,7 @@ public class VmChannel extends DefaultSubchannel {
* @param client the client
*/
public VmChannel(Channel mainChannel, EventPipeline pipeline,
- ApiClient client) {
+ K8sClient client) {
super(mainChannel);
this.pipeline = pipeline;
this.client = client;
@@ -56,7 +56,7 @@ public class VmChannel extends DefaultSubchannel {
* @return the watch channel
*/
@SuppressWarnings("PMD.LinguisticNaming")
- public VmChannel setVmDefinition(DynamicKubernetesObject definition) {
+ public VmChannel setVmDefinition(K8sDynamicModel definition) {
this.vmDefinition = definition;
return this;
}
@@ -66,7 +66,7 @@ public class VmChannel extends DefaultSubchannel {
*
* @return the json object
*/
- public DynamicKubernetesObject vmDefinition() {
+ public K8sDynamicModel vmDefinition() {
return vmDefinition;
}
@@ -109,7 +109,7 @@ public class VmChannel extends DefaultSubchannel {
*
* @return the API client
*/
- public ApiClient client() {
+ public K8sClient client() {
return client;
}
}
diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmDefChanged.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmDefChanged.java
index fd5d43c..e9c9ca1 100644
--- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmDefChanged.java
+++ b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmDefChanged.java
@@ -19,7 +19,7 @@
package org.jdrupes.vmoperator.manager.events;
import io.kubernetes.client.openapi.models.V1APIResource;
-import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
+import org.jdrupes.vmoperator.common.K8sDynamicModel;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Components;
import org.jgrapes.core.Event;
@@ -44,7 +44,7 @@ public class VmDefChanged extends Event {
private final Type type;
private final boolean specChanged;
private final V1APIResource crd;
- private final DynamicKubernetesObject vmDef;
+ private final K8sDynamicModel vmDef;
/**
* Instantiates a new VM changed event.
@@ -55,7 +55,7 @@ public class VmDefChanged extends Event {
* @param vmDefinition the VM definition
*/
public VmDefChanged(Type type, boolean specChanged, V1APIResource crd,
- DynamicKubernetesObject vmDefinition) {
+ K8sDynamicModel vmDefinition) {
this.type = type;
this.specChanged = specChanged;
this.crd = crd;
@@ -92,7 +92,7 @@ public class VmDefChanged extends Event {
*
* @return the object.
*/
- public DynamicKubernetesObject vmDefinition() {
+ public K8sDynamicModel vmDefinition() {
return vmDef;
}
diff --git a/org.jdrupes.vmoperator.manager/.settings/net.sf.jautodoc.prefs b/org.jdrupes.vmoperator.manager/.settings/net.sf.jautodoc.prefs
index 6f3b6d4..03e8200 100644
--- a/org.jdrupes.vmoperator.manager/.settings/net.sf.jautodoc.prefs
+++ b/org.jdrupes.vmoperator.manager/.settings/net.sf.jautodoc.prefs
@@ -1,6 +1,6 @@
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 .\n */
+header_text=/*\n * VM-Operator\n * Copyright (C) 2024 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 .\n */
project_specific_settings=true
replacements=\n\n\nReturns the\nSets the\nAdds the\nEdits the\nRemoves the\nInits the\nParses the\nCreates the\nBuilds the\nChecks if is\nPrints the\nChecks for\n\n\n
visibility_package=false
diff --git a/org.jdrupes.vmoperator.manager/build.gradle b/org.jdrupes.vmoperator.manager/build.gradle
index d403be8..a4990d5 100644
--- a/org.jdrupes.vmoperator.manager/build.gradle
+++ b/org.jdrupes.vmoperator.manager/build.gradle
@@ -33,8 +33,6 @@ dependencies {
runtimeOnly 'org.apache.logging.log4j:log4j-to-jul:2.20.0'
runtimeOnly project(':org.jdrupes.vmoperator.vmconlet')
-
- testImplementation 'io.fabric8:kubernetes-client:[6.8.1,6.9)'
}
application {
diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java
index 589affc..ee693c2 100644
--- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java
+++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java
@@ -18,20 +18,21 @@
package org.jdrupes.vmoperator.manager;
+import io.kubernetes.client.apimachinery.GroupVersionKind;
import io.kubernetes.client.custom.V1Patch;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.Configuration;
-import io.kubernetes.client.util.Config;
-import io.kubernetes.client.util.generic.options.PatchOptions;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.logging.Level;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM;
-import org.jdrupes.vmoperator.common.K8s;
+import org.jdrupes.vmoperator.common.K8sClient;
+import org.jdrupes.vmoperator.common.K8sDynamicStub;
import org.jdrupes.vmoperator.manager.events.Exit;
import org.jdrupes.vmoperator.manager.events.ModifyVm;
+import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Component;
@@ -160,35 +161,30 @@ public class Controller extends Component {
* @throws IOException Signals that an I/O exception has occurred.
*/
@Handler
- public void onModigyVm(ModifyVm event) throws ApiException, IOException {
- patchVmSpec(event.name(), event.path(), event.value());
+ public void onModifyVm(ModifyVm event, VmChannel channel)
+ throws ApiException, IOException {
+ patchVmSpec(channel.client(), event.name(), event.path(),
+ event.value());
}
- private void patchVmSpec(String name, String path, Object value)
+ private void patchVmSpec(K8sClient client, String name, String path,
+ Object value)
throws ApiException, IOException {
- var crApi = K8s.crApi(Config.defaultClient(), VM_OP_GROUP,
- VM_OP_KIND_VM, namespace, name);
- if (crApi.isEmpty()) {
- logger.warning(() -> "Trying to patch " + namespace + "/" + name
- + " which does not exist.");
- return;
- }
+ var vmStub = K8sDynamicStub.get(client,
+ new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM), namespace,
+ name);
// Patch running
- PatchOptions patchOpts = new PatchOptions();
- patchOpts.setFieldManager("kubernetes-java-kubectl-apply");
String valueAsText = value instanceof String
? "\"" + value + "\""
: value.toString();
- var res = crApi.get().patch(namespace, name,
- V1Patch.PATCH_FORMAT_JSON_PATCH,
+ var res = vmStub.patch(V1Patch.PATCH_FORMAT_JSON_PATCH,
new V1Patch("[{\"op\": \"replace\", \"path\": \"/spec/vm/"
+ path + "\", \"value\": " + valueAsText + "}]"),
- patchOpts);
- if (!res.isSuccess()) {
+ client.defaultPatchOptions());
+ if (!res.isPresent()) {
logger.warning(
- () -> "Cannot patch pod annotations: " + res.getStatus());
+ () -> "Cannot patch pod annotations for " + vmStub.name());
}
-
}
}
diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/LoadBalancerReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/LoadBalancerReconciler.java
index efa95f4..85158c7 100644
--- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/LoadBalancerReconciler.java
+++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/LoadBalancerReconciler.java
@@ -29,11 +29,11 @@ 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.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Logger;
import org.jdrupes.vmoperator.common.K8s;
+import org.jdrupes.vmoperator.common.K8sDynamicModel;
import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jdrupes.vmoperator.util.GsonPtr;
@@ -79,19 +79,25 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
Map model, VmChannel channel)
throws IOException, TemplateException, ApiException {
// Check if to be generated
- @SuppressWarnings({ "unchecked", "PMD.AvoidDuplicateLiterals" })
- var lbs = Optional.of(model)
+ @SuppressWarnings({ "PMD.AvoidDuplicateLiterals", "unchecked" })
+ var lbsDef = Optional.of(model)
.map(m -> (Map) m.get("reconciler"))
.map(c -> c.get(LOAD_BALANCER_SERVICE)).orElse(Boolean.FALSE);
- if (lbs instanceof Boolean isOn && !isOn) {
- return;
- }
- if (!(lbs instanceof Map)) {
+ if (!(lbsDef instanceof Map) && !(lbsDef instanceof Boolean)) {
logger.warning(() -> "\"" + LOAD_BALANCER_SERVICE
+ "\" in configuration must be boolean or mapping but is "
- + lbs.getClass() + ".");
+ + lbsDef.getClass() + ".");
return;
}
+ if (lbsDef instanceof Boolean isOn && !isOn) {
+ return;
+ }
+ JsonObject cfgMeta = new JsonObject();
+ if (lbsDef instanceof Map) {
+ var json = channel.client().getJSON();
+ cfgMeta
+ = json.deserialize(json.serialize(lbsDef), JsonObject.class);
+ }
// Combine template and data and parse result
var fmTemplate = fmConfig.getTemplate("runnerLoadBalancer.ftl.yaml");
@@ -101,7 +107,7 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
// https://github.com/kubernetes-client/java/issues/2741
var svcDef = Dynamics.newFromYaml(
new Yaml(new SafeConstructor(new LoaderOptions())), out.toString());
- mergeMetadata(svcDef, lbs, channel);
+ mergeMetadata(svcDef, cfgMeta, event.vmDefinition());
// Apply
DynamicKubernetesApi svcApi = new DynamicKubernetesApi("", "v1",
@@ -109,20 +115,10 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
K8s.apply(svcApi, svcDef, svcDef.getRaw().toString());
}
- @SuppressWarnings("unchecked")
private void mergeMetadata(DynamicKubernetesObject svcDef,
- Object lbsConfig, VmChannel channel) {
- // Get metadata from config
- Map asmData = Collections.emptyMap();
- if (lbsConfig instanceof Map config) {
- asmData = (Map) config;
- }
- var json = channel.client().getJSON();
- JsonObject cfgMeta
- = json.deserialize(json.serialize(asmData), JsonObject.class);
-
+ JsonObject cfgMeta, K8sDynamicModel vmDefinition) {
// Get metadata from VM definition
- var vmMeta = GsonPtr.to(channel.vmDefinition().getRaw()).to("spec")
+ var vmMeta = GsonPtr.to(vmDefinition.data()).to("spec")
.get(JsonObject.class, LOAD_BALANCER_SERVICE)
.map(JsonObject::deepCopy).orElseGet(() -> new JsonObject());
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 2adb843..0683c76 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
@@ -44,6 +44,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.jdrupes.vmoperator.common.Convertions;
+import org.jdrupes.vmoperator.common.K8sDynamicModel;
import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jdrupes.vmoperator.manager.events.VmDefChanged.Type;
@@ -206,8 +207,8 @@ public class Reconciler extends Component {
lbReconciler.reconcile(event, model, channel);
}
- private DynamicKubernetesObject patchCr(DynamicKubernetesObject vmDef) {
- var json = vmDef.getRaw().deepCopy();
+ private DynamicKubernetesObject patchCr(K8sDynamicModel vmDef) {
+ var json = vmDef.data().deepCopy();
// Adjust cdromImage path
adjustCdRomPaths(json);
diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/StatefulSetReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/StatefulSetReconciler.java
index 3cd6a39..8812a93 100644
--- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/StatefulSetReconciler.java
+++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/StatefulSetReconciler.java
@@ -22,14 +22,13 @@ 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 io.kubernetes.client.util.generic.options.PatchOptions;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Map;
import java.util.logging.Logger;
-import org.jdrupes.vmoperator.common.K8s;
+import org.jdrupes.vmoperator.common.K8sV1StatefulSetStub;
import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jdrupes.vmoperator.util.GsonPtr;
@@ -68,8 +67,6 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
public void reconcile(VmDefChanged event, Map model,
VmChannel channel)
throws IOException, TemplateException, ApiException {
- DynamicKubernetesApi stsApi = new DynamicKubernetesApi("apps", "v1",
- "statefulsets", channel.client());
var metadata = event.vmDefinition().getMetadata();
// Combine template and data and parse result
@@ -83,25 +80,27 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
// If exists apply changes only when transitioning state
// or not running.
- var existing = K8s.get(stsApi, metadata);
- if (existing.isPresent()) {
- var current = GsonPtr.to(existing.get().getRaw())
- .to("spec").getAsInt("replicas").orElse(1);
+ var stsStub = K8sV1StatefulSetStub.get(channel.client(),
+ metadata.getNamespace(), metadata.getName());
+ stsStub.model().ifPresent(sts -> {
+ var current = sts.getSpec().getReplicas();
var desired = GsonPtr.to(stsDef.getRaw())
.to("spec").getAsInt("replicas").orElse(1);
if (current == 1 && desired == 1) {
return;
}
- }
+ });
// Do apply changes
PatchOptions opts = new PatchOptions();
opts.setForce(true);
opts.setFieldManager("kubernetes-java-kubectl-apply");
- stsApi.patch(stsDef.getMetadata().getNamespace(),
- stsDef.getMetadata().getName(), V1Patch.PATCH_FORMAT_APPLY_YAML,
- new V1Patch(channel.client().getJSON().serialize(stsDef)),
- opts).throwsApiException();
+ if (stsStub.patch(V1Patch.PATCH_FORMAT_APPLY_YAML,
+ new V1Patch(channel.client().getJSON().serialize(stsDef)), opts)
+ .isEmpty()) {
+ logger.warning(
+ () -> "Could not patch stateful set for " + stsStub.name());
+ }
}
}
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 2d99727..c074ac2 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
@@ -1,6 +1,6 @@
/*
* VM-Operator
- * Copyright (C) 2023 Michael N. Lipp
+ * Copyright (C) 2023,2024 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
@@ -21,6 +21,8 @@ package org.jdrupes.vmoperator.manager;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
+import io.kubernetes.client.apimachinery.GroupVersion;
+import io.kubernetes.client.apimachinery.GroupVersionKind;
import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.apis.ApisApi;
@@ -33,7 +35,6 @@ import io.kubernetes.client.openapi.models.V1ObjectMeta;
import io.kubernetes.client.util.Config;
import io.kubernetes.client.util.Watch;
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.io.IOException;
import java.nio.file.Files;
@@ -48,7 +49,10 @@ import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP;
-import org.jdrupes.vmoperator.common.K8s;
+import org.jdrupes.vmoperator.common.K8sClient;
+import org.jdrupes.vmoperator.common.K8sDynamicModel;
+import org.jdrupes.vmoperator.common.K8sDynamicStub;
+import org.jdrupes.vmoperator.common.K8sV1PodStub;
import static org.jdrupes.vmoperator.manager.Constants.APP_NAME;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_KIND_VM;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME;
@@ -68,7 +72,7 @@ import org.jgrapes.util.events.ConfigurationUpdate;
/**
* Watches for changes of VM definitions.
*/
-@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
+@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports" })
public class VmWatcher extends Component {
private String namespaceToWatch;
@@ -269,13 +273,13 @@ public class VmWatcher extends Component {
}
private void handleVmDefinitionChange(V1APIResource vmsCrd,
- Watch.Response vmDefStub) {
- V1ObjectMeta metadata = vmDefStub.object.getMetadata();
+ Watch.Response vmDefRef) throws ApiException {
+ V1ObjectMeta metadata = vmDefRef.object.getMetadata();
VmChannel channel = channels.computeIfAbsent(metadata.getName(),
k -> {
try {
return new VmChannel(channel(), newEventPipeline(),
- Config.defaultClient());
+ new K8sClient());
} catch (IOException e) {
logger.log(Level.SEVERE, e, () -> "Failed to create client"
+ " for handling changes: " + e.getMessage());
@@ -287,30 +291,27 @@ public class VmWatcher extends Component {
}
// Get full definition and associate with channel as backup
- var apiVersion = K8s.version(vmDefStub.object.getApiVersion());
- DynamicKubernetesApi vmCrApi = new DynamicKubernetesApi(VM_OP_GROUP,
- apiVersion, vmsCrd.getName(), channel.client());
- var curVmDef = K8s.get(vmCrApi, metadata);
- curVmDef.ifPresent(def -> {
- // Augment with "dynamic" data and associate with channel
- addDynamicData(channel.client(), def);
- channel.setVmDefinition(def);
+ @SuppressWarnings("PMD.ShortVariable")
+ var gv = GroupVersion.parse(vmDefRef.object.getApiVersion());
+ var vmStub = K8sDynamicStub.get(channel.client(),
+ new GroupVersionKind(gv.getGroup(), gv.getVersion(), VM_OP_KIND_VM),
+ metadata.getNamespace(), metadata.getName());
+ vmStub.model().ifPresent(vmDef -> {
+ addDynamicData(channel.client(), vmDef);
+ channel.setVmDefinition(vmDef);
+
+ // Create and fire event
+ channel.pipeline().fire(new VmDefChanged(VmDefChanged.Type
+ .valueOf(vmDefRef.type),
+ channel
+ .setGeneration(
+ vmDefRef.object.getMetadata().getGeneration()),
+ vmsCrd, vmDef), channel);
});
-
- // Get eventual definition to use
- var vmDef = curVmDef.orElse(channel.vmDefinition());
-
- // Create and fire event
- channel.pipeline().fire(new VmDefChanged(VmDefChanged.Type
- .valueOf(vmDefStub.type),
- channel
- .setGeneration(vmDefStub.object.getMetadata().getGeneration()),
- vmsCrd, vmDef), channel);
}
- private void addDynamicData(ApiClient client,
- DynamicKubernetesObject vmDef) {
- var rootNode = GsonPtr.to(vmDef.getRaw()).get(JsonObject.class);
+ private void addDynamicData(K8sClient client, K8sDynamicModel vmState) {
+ var rootNode = GsonPtr.to(vmState.data()).get(JsonObject.class);
rootNode.addProperty("nodeName", "");
// VM definition status changes before the pod terminates.
@@ -329,11 +330,18 @@ public class VmWatcher extends Component {
var podSearch = new ListOptions();
podSearch.setLabelSelector("app.kubernetes.io/name=" + APP_NAME
+ ",app.kubernetes.io/component=" + APP_NAME
- + ",app.kubernetes.io/instance=" + vmDef.getMetadata().getName());
- var podList = K8s.podApi(client).list(namespaceToWatch, podSearch);
- podList.getObject().getItems().stream().forEach(pod -> {
- rootNode.addProperty("nodeName", pod.getSpec().getNodeName());
- });
+ + ",app.kubernetes.io/instance=" + vmState.getMetadata().getName());
+ try {
+ var podList
+ = K8sV1PodStub.list(client, namespaceToWatch, podSearch);
+ for (var podStub : podList) {
+ rootNode.addProperty("nodeName",
+ podStub.model().get().getSpec().getNodeName());
+ }
+ } catch (ApiException e) {
+ logger.log(Level.WARNING, e,
+ () -> "Cannot access node information: " + e.getMessage());
+ }
}
/**
diff --git a/org.jdrupes.vmoperator.manager/test/org/jdrupes/vmoperator/manager/BasicTests.java b/org.jdrupes.vmoperator.manager/test/org/jdrupes/vmoperator/manager/BasicTests.java
index 26eb387..13a93e1 100644
--- a/org.jdrupes.vmoperator.manager/test/org/jdrupes/vmoperator/manager/BasicTests.java
+++ b/org.jdrupes.vmoperator.manager/test/org/jdrupes/vmoperator/manager/BasicTests.java
@@ -1,13 +1,18 @@
package org.jdrupes.vmoperator.manager;
-import io.fabric8.kubernetes.client.Config;
-import io.fabric8.kubernetes.client.KubernetesClient;
-import io.fabric8.kubernetes.client.KubernetesClientBuilder;
-import io.fabric8.kubernetes.client.dsl.base.ResourceDefinitionContext;
+import io.kubernetes.client.Discovery.APIResource;
+import io.kubernetes.client.openapi.ApiException;
+import java.io.FileReader;
import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
import java.util.Map;
+import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP;
+import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM;
+import org.jdrupes.vmoperator.common.K8s;
+import org.jdrupes.vmoperator.common.K8sClient;
+import org.jdrupes.vmoperator.common.K8sDynamicStub;
+import org.jdrupes.vmoperator.common.K8sV1ConfigMapStub;
+import org.jdrupes.vmoperator.common.K8sV1DeploymentStub;
+import org.jdrupes.vmoperator.common.K8sV1StatefulSetStub;
import org.junit.jupiter.api.AfterAll;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeAll;
@@ -18,8 +23,9 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
class BasicTests {
- private static KubernetesClient client;
- private static ResourceDefinitionContext vmsContext;
+ private static K8sClient client;
+ private static APIResource vmsContext;
+ private static K8sV1DeploymentStub mgrDeployment;
@BeforeAll
static void setUpBeforeClass() throws Exception {
@@ -27,29 +33,27 @@ class BasicTests {
assertNotNull(testCluster);
// Get client
- client = new KubernetesClientBuilder()
- .withConfig(Config.autoConfigure(testCluster)).build();
+ client = new K8sClient();
// Context for working with our CR
- vmsContext = new ResourceDefinitionContext.Builder()
- .withGroup("vmoperator.jdrupes.org").withKind("VirtualMachine")
- .withPlural("vms").withNamespaced(true).withVersion("v1").build();
+ var apiRes = K8s.context(client, VM_OP_GROUP, null, VM_OP_KIND_VM);
+ assertTrue(apiRes.isPresent());
+ vmsContext = apiRes.get();
- // Cleanup
- var resourcesInNamespace = client.genericKubernetesResources(vmsContext)
- .inNamespace("vmop-dev");
- resourcesInNamespace.withName("unittest-vm").delete();
+ // Cleanup existing VM
+ K8sDynamicStub.get(client, vmsContext, "vmop-dev", "unittest-vm")
+ .delete();
// Update manager pod by scaling deployment
- client.apps().deployments().inNamespace("vmop-dev")
- .withName("vm-operator").scale(0);
- client.apps().deployments().inNamespace("vmop-dev")
- .withName("vm-operator").scale(1);
+ mgrDeployment
+ = K8sV1DeploymentStub.get(client, "vmop-dev", "vm-operator");
+ mgrDeployment.scale(0);
+ mgrDeployment.scale(1);
// Wait until available
+
for (int i = 0; i < 10; i++) {
- if (client.apps().deployments().inNamespace("vmop-dev")
- .withName("vm-operator").get().getStatus().getConditions()
+ if (mgrDeployment.model().get().getStatus().getConditions()
.stream().filter(c -> "Available".equals(c.getType())).findAny()
.isPresent()) {
return;
@@ -62,44 +66,40 @@ class BasicTests {
@AfterAll
static void tearDownAfterClass() throws Exception {
// Bring down manager
- client.apps().deployments().inNamespace("vmop-dev")
- .withName("vm-operator").scale(0);
- client.close();
+ mgrDeployment.scale(0);
}
@Test
- void test() throws IOException, InterruptedException {
+ void test() throws IOException, InterruptedException, ApiException {
// Load from Yaml
- var vm = client.genericKubernetesResources(vmsContext)
- .load(Files
- .newInputStream(Path.of("test-resources/unittest-vm.yaml")));
- // Create Custom Resource
- vm.create();
+ var rdr = new FileReader("test-resources/unittest-vm.yaml");
+ var vmStub = K8sDynamicStub.createFromYaml(client, vmsContext, rdr);
+ assertTrue(vmStub.model().isPresent());
// Wait for created resources
- assertTrue(waitForConfigMap());
- assertTrue(waitForStatefulSet());
+ assertTrue(waitForConfigMap(client));
+ assertTrue(waitForStatefulSet(client));
// Check config map
- var config = client.configMaps().inNamespace("vmop-dev")
- .withName("unittest-vm").get();
+ var config = K8sV1ConfigMapStub.get(client, "vmop-dev", "unittest-vm")
+ .model().get();
var yaml = new Yaml(new SafeConstructor(new LoaderOptions()))
- .load((String) config.getData().get("config.yaml"));
+ .load(config.getData().get("config.yaml"));
@SuppressWarnings("unchecked")
- var currentRam = ((Map>>) yaml)
+ var maximumRam = ((Map>>) yaml)
.get("/Runner").get("vm").get("maximumRam");
- assertEquals("4 GiB", currentRam);
+ assertEquals("4 GiB", maximumRam);
// Cleanup
- var resourcesInNamespace = client.genericKubernetesResources(vmsContext)
- .inNamespace("vmop-dev");
- resourcesInNamespace.withName("unittest-vm").delete();
+ K8sDynamicStub.get(client, vmsContext, "vmop-dev", "unittest-vm")
+ .delete();
}
- private boolean waitForConfigMap() throws InterruptedException {
+ private boolean waitForConfigMap(K8sClient client)
+ throws InterruptedException, ApiException {
+ var stub = K8sV1ConfigMapStub.get(client, "vmop-dev", "unittest-vm");
for (int i = 0; i < 10; i++) {
- if (client.configMaps().inNamespace("vmop-dev")
- .withName("unittest-vm").get() != null) {
+ if (stub.model().isPresent()) {
return true;
}
Thread.sleep(1000);
@@ -107,10 +107,11 @@ class BasicTests {
return false;
}
- private boolean waitForStatefulSet() throws InterruptedException {
+ private boolean waitForStatefulSet(K8sClient client)
+ throws InterruptedException, ApiException {
+ var stub = K8sV1StatefulSetStub.get(client, "vmop-dev", "unittest-vm");
for (int i = 0; i < 10; i++) {
- if (client.apps().statefulSets().inNamespace("vmop-dev")
- .withName("unittest-vm").get() != null) {
+ if (stub.model().isPresent()) {
return true;
}
Thread.sleep(1000);
diff --git a/org.jdrupes.vmoperator.runner.qemu/build.gradle b/org.jdrupes.vmoperator.runner.qemu/build.gradle
index 7179b8f..82525c6 100644
--- a/org.jdrupes.vmoperator.runner.qemu/build.gradle
+++ b/org.jdrupes.vmoperator.runner.qemu/build.gradle
@@ -16,7 +16,7 @@ dependencies {
implementation project(':org.jdrupes.vmoperator.common')
implementation 'commons-cli:commons-cli:1.5.0'
- implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:[2.15.1,3]'
+ implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:[2.16.1]'
runtimeOnly 'org.slf4j:slf4j-jdk14:[2.0.7,3)'
}
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java
index 1cb5e74..d55e027 100644
--- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java
@@ -1,6 +1,6 @@
/*
* VM-Operator
- * Copyright (C) 2023 Michael N. Lipp
+ * Copyright (C) 2023,2024 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
@@ -19,27 +19,17 @@
package org.jdrupes.vmoperator.runner.qemu;
import com.google.gson.JsonObject;
+import io.kubernetes.client.apimachinery.GroupVersionKind;
import io.kubernetes.client.custom.Quantity;
import io.kubernetes.client.custom.Quantity.Format;
import io.kubernetes.client.custom.V1Patch;
import io.kubernetes.client.openapi.ApiException;
-import io.kubernetes.client.openapi.apis.ApisApi;
-import io.kubernetes.client.openapi.apis.CustomObjectsApi;
-import io.kubernetes.client.openapi.apis.EventsV1Api;
import io.kubernetes.client.openapi.models.EventsV1Event;
-import io.kubernetes.client.openapi.models.V1APIGroup;
-import io.kubernetes.client.openapi.models.V1GroupVersionForDiscovery;
-import io.kubernetes.client.openapi.models.V1ObjectMeta;
-import io.kubernetes.client.util.Config;
-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.io.IOException;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
-import java.time.OffsetDateTime;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@@ -48,6 +38,9 @@ import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM;
import org.jdrupes.vmoperator.common.K8s;
+import org.jdrupes.vmoperator.common.K8sClient;
+import org.jdrupes.vmoperator.common.K8sDynamicModel;
+import org.jdrupes.vmoperator.common.K8sDynamicStub;
import org.jdrupes.vmoperator.runner.qemu.events.BalloonChangeEvent;
import org.jdrupes.vmoperator.runner.qemu.events.Exit;
import org.jdrupes.vmoperator.runner.qemu.events.HotpluggableCpuStatus;
@@ -75,11 +68,11 @@ public class StatusUpdater extends Component {
private String namespace;
private String vmName;
- private DynamicKubernetesApi vmCrApi;
- private EventsV1Api evtsApi;
+ private K8sClient apiClient;
private long observedGeneration;
private boolean guestShutdownStops;
private boolean shutdownByGuest;
+ private K8sDynamicStub vmStub;
/**
* Instantiates a new status updater.
@@ -88,6 +81,16 @@ public class StatusUpdater extends Component {
*/
public StatusUpdater(Channel componentChannel) {
super(componentChannel);
+ try {
+ apiClient = new K8sClient();
+ io.kubernetes.client.openapi.Configuration
+ .setDefaultApiClient(apiClient);
+ } catch (IOException e) {
+ logger.log(Level.SEVERE, e,
+ () -> "Cannot access events API, terminating.");
+ fire(new Exit(1));
+ }
+
}
/**
@@ -154,59 +157,18 @@ public class StatusUpdater extends Component {
return;
}
try {
- initVmCrApi(event);
- } catch (IOException | ApiException e) {
+ vmStub = K8sDynamicStub.get(apiClient,
+ new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM),
+ namespace, vmName);
+ vmStub.model().ifPresent(model -> {
+ observedGeneration = model.getMetadata().getGeneration();
+ });
+ } catch (ApiException e) {
logger.log(Level.SEVERE, e,
- () -> "Cannot access VM's CR, terminating.");
+ () -> "Cannot access VM object, terminating.");
event.cancel(true);
fire(new Exit(1));
}
- try {
- evtsApi = new EventsV1Api(Config.defaultClient());
- } catch (IOException e) {
- logger.log(Level.SEVERE, e,
- () -> "Cannot access events API, terminating.");
- event.cancel(true);
- fire(new Exit(1));
- }
- }
-
- private void initVmCrApi(Start event) throws IOException, ApiException {
- var client = Config.defaultClient();
- var apis = new ApisApi(client).getAPIVersions();
- var crdVersions = apis.getGroups().stream()
- .filter(g -> g.getName().equals(VM_OP_GROUP)).findFirst()
- .map(V1APIGroup::getVersions).stream().flatMap(l -> l.stream())
- .map(V1GroupVersionForDiscovery::getVersion).toList();
- var coa = new CustomObjectsApi(client);
- for (var crdVersion : crdVersions) {
- var crdApiRes = coa.getAPIResources(VM_OP_GROUP,
- crdVersion).getResources().stream()
- .filter(r -> VM_OP_KIND_VM.equals(r.getKind())).findFirst();
- if (crdApiRes.isEmpty()) {
- continue;
- }
- @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
- var crApi = new DynamicKubernetesApi(VM_OP_GROUP,
- crdVersion, crdApiRes.get().getName(), client);
- var vmCr = crApi.get(namespace, vmName);
- if (vmCr.isSuccess()) {
- vmCrApi = crApi;
- observedGeneration
- = vmCr.getObject().getMetadata().getGeneration();
- break;
- }
- }
- if (vmCrApi == null) {
- logger.severe(() -> "Cannot find VM's CR, terminating.");
- event.cancel(true);
- fire(new Exit(1));
- }
- }
-
- @SuppressWarnings("PMD.AvoidDuplicateLiterals")
- private JsonObject currentStatus(DynamicKubernetesObject vmCr) {
- return vmCr.getRaw().getAsJsonObject("status").deepCopy();
}
/**
@@ -221,18 +183,19 @@ public class StatusUpdater extends Component {
guestShutdownStops = event.configuration().guestShutdownStops;
// Remainder applies only if we have a connection to k8s.
- if (vmCrApi == null) {
+ if (vmStub == null) {
return;
}
+
// A change of the runner configuration is typically caused
// by a new version of the CR. So we observe the new CR.
- var vmCr = vmCrApi.get(namespace, vmName).throwsApiException()
- .getObject();
- if (vmCr.getMetadata().getGeneration() == observedGeneration) {
+ var vmDef = vmStub.model();
+ if (vmDef.isPresent()
+ && vmDef.get().metadata().getGeneration() == observedGeneration) {
return;
}
- vmCrApi.updateStatus(vmCr, from -> {
- JsonObject status = currentStatus(from);
+ vmStub.updateStatus(vmDef.get(), from -> {
+ JsonObject status = from.status();
status.getAsJsonArray("conditions").asList().stream()
.map(cond -> (JsonObject) cond).filter(cond -> "Running"
.equals(cond.get("type").getAsString()))
@@ -249,15 +212,15 @@ public class StatusUpdater extends Component {
* @throws ApiException
*/
@Handler
+ @SuppressWarnings("PMD.AssignmentInOperand")
public void onRunnerStateChanged(RunnerStateChange event)
throws ApiException {
- if (vmCrApi == null) {
+ K8sDynamicModel vmDef;
+ if (vmStub == null || (vmDef = vmStub.model().orElse(null)) == null) {
return;
}
- var vmCr = vmCrApi.get(namespace, vmName).throwsApiException()
- .getObject();
- vmCrApi.updateStatus(vmCr, from -> {
- JsonObject status = currentStatus(from);
+ vmStub.updateStatus(vmDef, from -> {
+ JsonObject status = from.status();
status.getAsJsonArray("conditions").asList().stream()
.map(cond -> (JsonObject) cond)
.forEach(cond -> {
@@ -266,7 +229,7 @@ public class StatusUpdater extends Component {
}
});
if (event.state() == State.STARTING) {
- status.addProperty("ram", GsonPtr.to(from.getRaw())
+ status.addProperty("ram", GsonPtr.to(from.data())
.getAsString("spec", "vm", "maximumRam").orElse("0"));
status.addProperty("cpus", 1);
} else if (event.state() == State.STOPPED) {
@@ -274,40 +237,32 @@ public class StatusUpdater extends Component {
status.addProperty("cpus", 0);
}
return status;
- }).throwsApiException();
+ });
// Maybe stop VM
if (event.state() == State.TERMINATING && !event.failed()
&& guestShutdownStops && shutdownByGuest) {
logger.info(() -> "Stopping VM because of shutdown by guest.");
- PatchOptions patchOpts = new PatchOptions();
- patchOpts.setFieldManager("kubernetes-java-kubectl-apply");
- var res = vmCrApi.patch(namespace, vmName,
- V1Patch.PATCH_FORMAT_JSON_PATCH,
+ var res = vmStub.patch(V1Patch.PATCH_FORMAT_JSON_PATCH,
new V1Patch("[{\"op\": \"replace\", \"path\": \"/spec/vm/state"
+ "\", \"value\": \"Stopped\"}]"),
- patchOpts);
- if (!res.isSuccess()) {
+ apiClient.defaultPatchOptions());
+ if (!res.isPresent()) {
logger.warning(
- () -> "Cannot patch pod annotations: " + res.getStatus());
+ () -> "Cannot patch pod annotations for: " + vmStub.name());
}
}
// Log event
- var evt = new EventsV1Event().kind("Event")
- .metadata(new V1ObjectMeta().namespace(namespace)
- .generateName("vmrunner-"))
+ var evt = new EventsV1Event()
.reportingController(VM_OP_GROUP + "/" + APP_NAME)
- .reportingInstance(vmCr.getMetadata().getName())
- .eventTime(OffsetDateTime.now()).type("Normal")
- .regarding(K8s.objectReference(vmCr))
.action("StatusUpdate").reason(event.reason())
.note(event.message());
- evtsApi.createNamespacedEvent(namespace, evt, null, null, null, null);
+ K8s.createEvent(apiClient, vmDef, evt);
}
private void updateRunningCondition(RunnerStateChange event,
- DynamicKubernetesObject from, JsonObject cond) {
+ K8sDynamicModel from, JsonObject cond) {
boolean reportedRunning
= "True".equals(cond.get("status").getAsString());
if (RUNNING_STATES.contains(event.state())
@@ -336,18 +291,16 @@ public class StatusUpdater extends Component {
*/
@Handler
public void onBallonChange(BalloonChangeEvent event) throws ApiException {
- if (vmCrApi == null) {
+ if (vmStub == null) {
return;
}
- var vmCr = vmCrApi.get(namespace, vmName).throwsApiException()
- .getObject();
- vmCrApi.updateStatus(vmCr, from -> {
- JsonObject status = currentStatus(from);
+ vmStub.updateStatus(from -> {
+ JsonObject status = from.status();
status.addProperty("ram",
new Quantity(new BigDecimal(event.size()), Format.BINARY_SI)
.toSuffixedString());
return status;
- }).throwsApiException();
+ });
}
/**
@@ -358,16 +311,14 @@ public class StatusUpdater extends Component {
*/
@Handler
public void onCpuChange(HotpluggableCpuStatus event) throws ApiException {
- if (vmCrApi == null) {
+ if (vmStub == null) {
return;
}
- var vmCr = vmCrApi.get(namespace, vmName).throwsApiException()
- .getObject();
- vmCrApi.updateStatus(vmCr, from -> {
- JsonObject status = currentStatus(from);
+ vmStub.updateStatus(from -> {
+ JsonObject status = from.status();
status.addProperty("cpus", event.usedCpus().size());
return status;
- }).throwsApiException();
+ });
}
/**
diff --git a/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/VmConlet.java b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/VmConlet.java
index f096485..993a844 100644
--- a/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/VmConlet.java
+++ b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/VmConlet.java
@@ -25,7 +25,6 @@ import freemarker.template.Template;
import freemarker.template.TemplateNotFoundException;
import io.kubernetes.client.custom.Quantity;
import io.kubernetes.client.custom.Quantity.Format;
-import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
@@ -38,6 +37,7 @@ import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.jdrupes.json.JsonBeanDecoder;
import org.jdrupes.json.JsonDecodeException;
+import org.jdrupes.vmoperator.common.K8sDynamicModel;
import org.jdrupes.vmoperator.manager.events.ModifyVm;
import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
@@ -46,7 +46,6 @@ import org.jdrupes.vmoperator.util.GsonPtr;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Event;
import org.jgrapes.core.Manager;
-import org.jgrapes.core.NamedChannel;
import org.jgrapes.core.annotation.Handler;
import org.jgrapes.webconsole.base.Conlet.RenderMode;
import org.jgrapes.webconsole.base.ConletBaseModel;
@@ -70,7 +69,9 @@ public class VmConlet extends FreeMarkerConlet {
private static final Set MODES = RenderMode.asSet(
RenderMode.Preview, RenderMode.View);
- private final Map vmInfos
+ private final Map vmInfos
+ = new ConcurrentHashMap<>();
+ private final Map vmChannels
= new ConcurrentHashMap<>();
private final TimeSeries summarySeries = new TimeSeries(Duration.ofDays(1));
private Summary cachedSummary;
@@ -162,7 +163,7 @@ public class VmConlet extends FreeMarkerConlet {
}
if (sendVmInfos) {
for (var vmInfo : vmInfos.values()) {
- var def = JsonBeanDecoder.create(vmInfo.getRaw().toString())
+ var def = JsonBeanDecoder.create(vmInfo.data().toString())
.readObject();
channel.respond(new NotifyConletView(type(),
conletId, "updateVm", def));
@@ -185,9 +186,10 @@ public class VmConlet extends FreeMarkerConlet {
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.AvoidDuplicateLiterals" })
public void onVmDefChanged(VmDefChanged event, VmChannel channel)
throws JsonDecodeException, IOException {
+ var vmName = event.vmDefinition().getMetadata().getName();
if (event.type() == Type.DELETED) {
- var vmName = event.vmDefinition().getMetadata().getName();
vmInfos.remove(vmName);
+ vmChannels.remove(vmName);
for (var entry : conletIdsByConsoleConnection().entrySet()) {
for (String conletId : entry.getValue()) {
entry.getKey().respond(new NotifyConletView(type(),
@@ -195,8 +197,11 @@ public class VmConlet extends FreeMarkerConlet {
}
}
} else {
- var vmDef = convertQuantities(event);
- var def = JsonBeanDecoder.create(vmDef.getRaw().toString())
+ var vmDef = new K8sDynamicModel(channel.client().getJSON()
+ .getGson(), convertQuantities(event.vmDefinition().data()));
+ vmInfos.put(vmName, vmDef);
+ vmChannels.put(vmName, channel);
+ var def = JsonBeanDecoder.create(vmDef.data().toString())
.readObject();
for (var entry : conletIdsByConsoleConnection().entrySet()) {
for (String conletId : entry.getValue()) {
@@ -217,28 +222,25 @@ public class VmConlet extends FreeMarkerConlet {
}
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
- private DynamicKubernetesObject convertQuantities(VmDefChanged event) {
+ private JsonObject convertQuantities(JsonObject vmDef) {
// Clone and remove managed fields
- var vmDef = new DynamicKubernetesObject(
- event.vmDefinition().getRaw().deepCopy());
- GsonPtr.to(vmDef.getRaw()).to("metadata").get(JsonObject.class)
+ var json = vmDef.deepCopy();
+ GsonPtr.to(json).to("metadata").get(JsonObject.class)
.remove("managedFields");
// Convert RAM sizes to unitless numbers
- var vmSpec = GsonPtr.to(vmDef.getRaw()).to("spec", "vm");
+ var vmSpec = GsonPtr.to(json).to("spec", "vm");
vmSpec.set("maximumRam", Quantity.fromString(
vmSpec.getAsString("maximumRam").orElse("0")).getNumber()
.toBigInteger());
vmSpec.set("currentRam", Quantity.fromString(
vmSpec.getAsString("currentRam").orElse("0")).getNumber()
.toBigInteger());
- var status = GsonPtr.to(vmDef.getRaw()).to("status");
+ var status = GsonPtr.to(json).to("status");
status.set("ram", Quantity.fromString(
status.getAsString("ram").orElse("0")).getNumber()
.toBigInteger());
- String vmName = event.vmDefinition().getMetadata().getName();
- vmInfos.put(vmName, vmDef);
- return vmDef;
+ return json;
}
/**
@@ -323,7 +325,7 @@ public class VmConlet extends FreeMarkerConlet {
Summary summary = new Summary();
for (var vmDef : vmInfos.values()) {
summary.totalVms += 1;
- var status = GsonPtr.to(vmDef.getRaw()).to("status");
+ var status = GsonPtr.to(vmDef.data()).to("status");
summary.usedCpus += status.getAsInt("cpus").orElse(0);
summary.usedRam = summary.usedRam.add(status.getAsString("ram")
.map(BigInteger::new).orElse(BigInteger.ZERO));
@@ -346,25 +348,28 @@ public class VmConlet extends FreeMarkerConlet {
ConsoleConnection channel, VmsModel conletState)
throws Exception {
event.stop();
+ var vmName = event.params().asString(0);
+ var vmChannel = vmChannels.get(vmName);
+ if (vmChannel == null) {
+ return;
+ }
switch (event.method()) {
case "start":
- fire(new ModifyVm(event.params().asString(0), "state", "Running",
- new NamedChannel("manager")));
+ fire(new ModifyVm(vmName, "state", "Running", vmChannel));
break;
case "stop":
- fire(new ModifyVm(event.params().asString(0), "state", "Stopped",
- new NamedChannel("manager")));
+ fire(new ModifyVm(vmName, "state", "Stopped", vmChannel));
break;
case "cpus":
- fire(new ModifyVm(event.params().asString(0), "currentCpus",
+ fire(new ModifyVm(vmName, "currentCpus",
new BigDecimal(event.params().asDouble(1)).toBigInteger(),
- new NamedChannel("manager")));
+ vmChannel));
break;
case "ram":
- fire(new ModifyVm(event.params().asString(0), "currentRam",
+ fire(new ModifyVm(vmName, "currentRam",
new Quantity(new BigDecimal(event.params().asDouble(1)),
Format.BINARY_SI).toSuffixedString(),
- new NamedChannel("manager")));
+ vmChannel));
break;
default:// ignore
break;
From 85b0a160f36f5f16722753f5b3cc8399d0bbb414 Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp" <1446020+mnlipp@users.noreply.github.com>
Date: Mon, 18 Mar 2024 13:15:59 +0100
Subject: [PATCH 044/379] Generate doc on GitLab (#20)
---
.gitlab-ci.yml | 28 ++++++++++++++++++++++++++++
1 file changed, 28 insertions(+)
create mode 100644 .gitlab-ci.yml
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..70654cc
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,28 @@
+default:
+ # Template project: https://gitlab.com/pages/jekyll
+ # Docs: https://docs.gitlab.com/ee/pages/
+ image: ruby:3.2
+ before_script:
+ - git fetch origin gh-pages
+ - git checkout gh-pages
+ - gem install bundler
+ - bundle install
+variables:
+ JEKYLL_ENV: production
+ LC_ALL: C.UTF-8
+test:
+ stage: test
+ script:
+ - bundle exec jekyll build -d test
+ artifacts:
+ paths:
+ - test
+
+pages:
+ stage: deploy
+ script:
+ - bundle exec jekyll build -d public
+ artifacts:
+ paths:
+ - public
+ environment: production
From 3103452170ccddd8ff67bd9a60f50a8caae1366c Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp" <1446020+mnlipp@users.noreply.github.com>
Date: Wed, 20 Mar 2024 11:03:09 +0100
Subject: [PATCH 045/379] Support for display secrets (#21)
---
dev-example/test-vm-display-secret.yaml | 12 +
.../jdrupes/vmoperator/common/Constants.java | 3 +
.../org/jdrupes/vmoperator/common/K8s.java | 35 +-
.../vmoperator/common/K8sDynamicStub.java | 58 ++-
.../vmoperator/common/K8sGenericStub.java | 367 +++++++++---------
.../vmoperator/common/K8sObserver.java | 230 +++++++++++
.../vmoperator/common/K8sV1ConfigMapStub.java | 7 +-
.../common/K8sV1DeploymentStub.java | 34 +-
.../vmoperator/common/K8sV1PodStub.java | 13 +-
.../vmoperator/common/K8sV1SecretStub.java | 60 +++
.../common/K8sV1StatefulSetStub.java | 9 +-
.../manager/events/ChannelCache.java | 207 ++++++++++
.../manager/events/ChannelManager.java | 303 +++++++++++++++
.../manager/events/DisplaySecretChanged.java | 77 ++++
.../manager/events/VmDefChanged.java | 27 +-
.../vmoperator/manager/runnerSts.ftl.yaml | 10 +-
.../vmoperator/manager/AbstractMonitor.java | 287 ++++++++++++++
.../jdrupes/vmoperator/manager/Constants.java | 3 +
.../vmoperator/manager/Controller.java | 18 +-
.../manager/DisplaySecretsMonitor.java | 77 ++++
.../vmoperator/manager/Reconciler.java | 4 +-
.../manager/StatefulSetReconciler.java | 8 +
.../jdrupes/vmoperator/manager/VmMonitor.java | 183 +++++++++
.../jdrupes/vmoperator/manager/VmWatcher.java | 360 -----------------
.../display-password | 1 +
.../runner/qemu/CdMediaController.java | 4 +-
.../vmoperator/runner/qemu/Configuration.java | 13 +-
.../vmoperator/runner/qemu/CpuController.java | 6 +-
.../runner/qemu/DisplayController.java | 117 ++++++
.../vmoperator/runner/qemu/QemuMonitor.java | 14 +-
.../vmoperator/runner/qemu/RamController.java | 4 +-
.../vmoperator/runner/qemu/Runner.java | 49 ++-
.../vmoperator/runner/qemu/StatusUpdater.java | 4 +-
.../runner/qemu/commands/QmpCommand.java | 27 ++
.../qemu/commands/QmpSetDisplayPassword.java | 68 ++++
...gurationUpdate.java => ConfigureQemu.java} | 4 +-
.../templates/Standard-VM-latest.ftl.yaml | 6 +-
.../jdrupes/vmoperator/vmconlet/VmConlet.java | 30 +-
38 files changed, 2081 insertions(+), 658 deletions(-)
create mode 100644 dev-example/test-vm-display-secret.yaml
create mode 100644 org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sObserver.java
create mode 100644 org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1SecretStub.java
create mode 100644 org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ChannelCache.java
create mode 100644 org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ChannelManager.java
create mode 100644 org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/DisplaySecretChanged.java
create mode 100644 org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/AbstractMonitor.java
create mode 100644 org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretsMonitor.java
create mode 100644 org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmMonitor.java
delete mode 100644 org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmWatcher.java
create mode 100644 org.jdrupes.vmoperator.runner.qemu/display-password
create mode 100644 org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/DisplayController.java
create mode 100644 org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpSetDisplayPassword.java
rename org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/{RunnerConfigurationUpdate.java => ConfigureQemu.java} (93%)
diff --git a/dev-example/test-vm-display-secret.yaml b/dev-example/test-vm-display-secret.yaml
new file mode 100644
index 0000000..f3b5ccb
--- /dev/null
+++ b/dev-example/test-vm-display-secret.yaml
@@ -0,0 +1,12 @@
+kind: Secret
+apiVersion: v1
+metadata:
+ name: test-vm-display-secret
+ namespace: vmop-dev
+ labels:
+ app.kubernetes.io/name: vm-runner
+ app.kubernetes.io/instance: test-vm
+ app.kubernetes.io/component: display-secret
+type: Opaque
+data:
+ display-password: dGVzdC12bQ==
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Constants.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Constants.java
index 3ebe29d..d5d457c 100644
--- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Constants.java
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Constants.java
@@ -26,6 +26,9 @@ public class Constants {
/** The Constant APP_NAME. */
public static final String APP_NAME = "vm-runner";
+ /** The Constant COMP_DISPLAY_SECRETS. */
+ public static final String COMP_DISPLAY_SECRET = "display-secret";
+
/** The Constant VM_OP_NAME. */
public static final String VM_OP_NAME = "vm-operator";
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8s.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8s.java
index e350cf1..2c29fab 100644
--- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8s.java
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8s.java
@@ -44,6 +44,7 @@ import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;
+// TODO: Auto-generated Javadoc
/**
* Helpers for K8s API.
*/
@@ -74,6 +75,35 @@ public class K8s {
return Optional.empty();
}
+ /**
+ * Returns a new context with the given version as preferred version.
+ *
+ * @param context the context
+ * @param version the version
+ * @return the API resource
+ */
+ public static APIResource preferred(APIResource context, String version) {
+ assert context.getVersions().contains(version);
+ return new APIResource(context.getGroup(),
+ context.getVersions(), version, context.getKind(),
+ context.getNamespaced(), context.getResourcePlural(),
+ context.getResourceSingular());
+ }
+
+ /**
+ * Return a string representation of the context (API resource).
+ *
+ * @param context the context
+ * @return the string
+ */
+ @SuppressWarnings("PMD.UseLocaleWithCaseConversions")
+ public static String toString(APIResource context) {
+ return (Strings.isNullOrEmpty(context.getGroup()) ? ""
+ : context.getGroup() + "/")
+ + context.getPreferredVersion().toUpperCase()
+ + context.getKind();
+ }
+
/**
* Convert Yaml to Json.
*
@@ -156,6 +186,7 @@ public class K8s {
* @param api the api
* @param existing the existing
* @param update the update
+ * @return the t
* @throws ApiException the api exception
*/
public static
@@ -199,8 +230,10 @@ public class K8s {
* * If `type` is not set, set it to "Normal"
* * If `regarding` is not set, set it to the given object.
*
+ * @param client the client
+ * @param object the object
* @param event the event
- * @throws ApiException
+ * @throws ApiException the api exception
*/
@SuppressWarnings("PMD.NPathComplexity")
public static void createEvent(ApiClient client,
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicStub.java
index 1ab33ca..e6d36c5 100644
--- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicStub.java
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicStub.java
@@ -18,10 +18,14 @@
package org.jdrupes.vmoperator.common;
+import com.google.gson.Gson;
import io.kubernetes.client.Discovery.APIResource;
import io.kubernetes.client.apimachinery.GroupVersionKind;
+import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.util.generic.options.ListOptions;
import java.io.Reader;
+import java.util.Collection;
/**
* A stub for namespaced custom objects. It uses a dynamic model
@@ -47,6 +51,24 @@ public class K8sDynamicStub
Class objectListClass, K8sClient client,
APIResource context, String namespace, String name) {
super(objectClass, objectListClass, client, context, namespace, name);
+
+ // Make sure that we have an adapter for our type
+ Gson gson = client.getJSON().getGson();
+ if (!checkAdapters(client)) {
+ client.getJSON().setGson(gson.newBuilder()
+ .registerTypeAdapterFactory(
+ new K8sDynamicModelTypeAdapterFactory())
+ .create());
+ }
+ }
+
+ private boolean checkAdapters(ApiClient client) {
+ return K8sDynamicModelTypeAdapterFactory.K8sDynamicModelCreator.class
+ .equals(client.getJSON().getGson().getAdapter(K8sDynamicModel.class)
+ .getClass())
+ && K8sDynamicModelTypeAdapterFactory.K8sDynamicModelsCreator.class
+ .equals(client.getJSON().getGson()
+ .getAdapter(K8sDynamicModels.class).getClass());
}
/**
@@ -83,8 +105,7 @@ public class K8sDynamicStub
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
public static K8sDynamicStub get(K8sClient client,
- APIResource context, String namespace, String name)
- throws ApiException {
+ APIResource context, String namespace, String name) {
return K8sGenericStub.get(K8sDynamicModel.class, K8sDynamicModels.class,
client, context, namespace, name, K8sDynamicStub::new);
}
@@ -106,4 +127,37 @@ public class K8sDynamicStub
K8sDynamicModels.class, client, context, model,
K8sDynamicStub::new);
}
+
+ /**
+ * Get the stubs for the objects in the given namespace that match
+ * the criteria from the given options.
+ *
+ * @param client the client
+ * @param namespace the namespace
+ * @param options the options
+ * @return the collection
+ * @throws ApiException the api exception
+ */
+ public static Collection list(K8sClient client,
+ APIResource context, String namespace, ListOptions options)
+ throws ApiException {
+ return K8sGenericStub.list(K8sDynamicModel.class,
+ K8sDynamicModels.class, client, context, namespace, options,
+ K8sDynamicStub::new);
+ }
+
+ /**
+ * Get the stubs for the objects in the given namespace.
+ *
+ * @param client the client
+ * @param namespace the namespace
+ * @return the collection
+ * @throws ApiException the api exception
+ */
+ public static Collection list(K8sClient client,
+ APIResource context, String namespace)
+ throws ApiException {
+ return list(client, context, namespace, new ListOptions());
+ }
+
}
\ No newline at end of file
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sGenericStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sGenericStub.java
index 30c6699..db68a38 100644
--- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sGenericStub.java
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sGenericStub.java
@@ -18,21 +18,22 @@
package org.jdrupes.vmoperator.common;
-import com.google.gson.Gson;
import io.kubernetes.client.Discovery.APIResource;
import io.kubernetes.client.apimachinery.GroupVersionKind;
import io.kubernetes.client.common.KubernetesListObject;
import io.kubernetes.client.common.KubernetesObject;
import io.kubernetes.client.custom.V1Patch;
-import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.util.Strings;
import io.kubernetes.client.util.generic.GenericKubernetesApi;
+import io.kubernetes.client.util.generic.options.GetOptions;
import io.kubernetes.client.util.generic.options.ListOptions;
import io.kubernetes.client.util.generic.options.PatchOptions;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
import java.util.Optional;
import java.util.function.Function;
@@ -49,145 +50,16 @@ public class K8sGenericStub {
protected final K8sClient client;
private final GenericKubernetesApi api;
- protected final String group;
- protected final String version;
- protected final String kind;
- protected final String plural;
+ protected final APIResource context;
protected final String namespace;
protected final String name;
/**
- * Get a namespaced object stub. If the version in parameter
- * `gvk` is an empty string, the stub refers to the first object
- * found with matching group and kind.
- *
- * @param the object type
- * @param the object list type
- * @param the stub type
- * @param objectClass the object class
- * @param objectListClass the object list class
- * @param client the client
- * @param gvk the group, version and kind
- * @param namespace the namespace
- * @param name the name
- * @param provider the provider
- * @return the stub if the object exists
- * @throws ApiException the api exception
- */
- @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
- "PMD.AvoidInstantiatingObjectsInLoops" })
- public static >
- R get(Class objectClass, Class objectListClass,
- K8sClient client, GroupVersionKind gvk, String namespace,
- String name, GenericSupplier provider)
- throws ApiException {
- var context = K8s.context(client, gvk.getGroup(), gvk.getVersion(),
- gvk.getKind());
- if (context.isEmpty()) {
- throw new ApiException("No known API for " + gvk.getGroup()
- + "/" + gvk.getVersion() + " " + gvk.getKind());
- }
- return provider.get(objectClass, objectListClass, client, context.get(),
- namespace, name);
- }
-
- /**
- * Get a namespaced object stub.
- *
- * @param the object type
- * @param the object list type
- * @param the stub type
- * @param objectClass the object class
- * @param objectListClass the object list class
- * @param client the client
- * @param context the context
- * @param namespace the namespace
- * @param name the name
- * @param provider the provider
- * @return the stub if the object exists
- * @throws ApiException the api exception
- */
- @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
- "PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
- public static >
- R get(Class objectClass, Class objectListClass,
- K8sClient client, APIResource context, String namespace,
- String name, GenericSupplier provider)
- throws ApiException {
- return provider.get(objectClass, objectListClass, client,
- context, namespace, name);
- }
-
- /**
- * Get a namespaced object stub for a newly created object.
- *
- * @param the object type
- * @param the object list type
- * @param the stub type
- * @param objectClass the object class
- * @param objectListClass the object list class
- * @param client the client
- * @param context the context
- * @param model the model
- * @param provider the provider
- * @return the stub if the object exists
- * @throws ApiException the api exception
- */
- @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
- "PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
- public static >
- R create(Class objectClass, Class objectListClass,
- K8sClient client, APIResource context, O model,
- GenericSupplier provider) throws ApiException {
- var api = new GenericKubernetesApi<>(objectClass, objectListClass,
- context.getGroup(), context.getPreferredVersion(),
- context.getResourcePlural(), client);
- api.create(model).throwsApiException();
- return provider.get(objectClass, objectListClass, client,
- context, model.getMetadata().getNamespace(),
- model.getMetadata().getName());
- }
-
- /**
- * Get the stubs for the objects in the given namespace that match
- * the criteria from the given options.
- *
- * @param the object type
- * @param the object list type
- * @param the stub type
- * @param objectClass the object class
- * @param objectListClass the object list class
- * @param client the client
- * @param context the context
- * @param namespace the namespace
- * @param options the options
- * @param provider the provider
- * @return the collection
- * @throws ApiException the api exception
- */
- public static >
- Collection list(Class objectClass, Class objectListClass,
- K8sClient client, APIResource context, String namespace,
- ListOptions options, SpecificSupplier provider)
- throws ApiException {
- var api = new GenericKubernetesApi<>(objectClass, objectListClass,
- context.getGroup(), context.getPreferredVersion(),
- context.getResourcePlural(), client);
- var objs = api.list(namespace, options).throwsApiException();
- var result = new ArrayList();
- for (var item : objs.getObject().getItems()) {
- result.add(
- provider.get(client, namespace, item.getMetadata().getName()));
- }
- return result;
- }
-
- /**
- * Instantiates a new namespaced custom object stub.
+ * Instantiates a new stub for the object specified. If the object
+ * exists in the context specified, the version (see
+ * {@link #version()} is bound to the existing object's version.
+ * Else the stub is dangling with the version set to the context's
+ * preferred version.
*
* @param objectClass the object class
* @param objectListClass the object list class
@@ -196,35 +68,47 @@ public class K8sGenericStub objectClass, Class objectListClass,
K8sClient client, APIResource context, String namespace,
String name) {
this.client = client;
- group = context.getGroup();
- version = context.getPreferredVersion();
- kind = context.getKind();
- plural = context.getResourcePlural();
this.namespace = namespace;
this.name = name;
- Gson gson = client.getJSON().getGson();
- if (!checkAdapters(client)) {
- client.getJSON().setGson(gson.newBuilder()
- .registerTypeAdapterFactory(
- new K8sDynamicModelTypeAdapterFactory())
- .create());
+ // Bind version
+ var foundVersion = context.getPreferredVersion();
+ GenericKubernetesApi testApi = null;
+ GetOptions mdOpts
+ = new GetOptions().isPartialObjectMetadataRequest(true);
+ for (var version : candidateVersions(context)) {
+ testApi = new GenericKubernetesApi<>(objectClass, objectListClass,
+ context.getGroup(), version, context.getResourcePlural(),
+ client);
+ if (testApi.get(namespace, name, mdOpts)
+ .isSuccess()) {
+ foundVersion = version;
+ break;
+ }
}
- api = new GenericKubernetesApi<>(objectClass,
- objectListClass, group, version, plural, client);
+ if (foundVersion.equals(context.getPreferredVersion())) {
+ this.context = context;
+ } else {
+ this.context = K8s.preferred(context, foundVersion);
+ }
+
+ api = Optional.ofNullable(testApi)
+ .orElseGet(() -> new GenericKubernetesApi<>(objectClass,
+ objectListClass, group(), version(), plural(), client));
}
- private boolean checkAdapters(ApiClient client) {
- return K8sDynamicModelTypeAdapterFactory.K8sDynamicModelCreator.class
- .equals(client.getJSON().getGson().getAdapter(K8sDynamicModel.class)
- .getClass())
- && K8sDynamicModelTypeAdapterFactory.K8sDynamicModelsCreator.class
- .equals(client.getJSON().getGson()
- .getAdapter(K8sDynamicModels.class).getClass());
+ /**
+ * Gets the context.
+ *
+ * @return the context
+ */
+ public APIResource context() {
+ return context;
}
/**
@@ -233,7 +117,7 @@ public class K8sGenericStub the object type
- * @param the object list type
- * @param the result type
- */
- public interface SpecificSupplier> {
-
- /**
- * Gets a new stub.
- *
- * @param client the client
- * @param namespace the namespace
- * @param name the name
- * @return the result
- */
- R get(K8sClient client, String namespace, String name);
- }
-
@Override
@SuppressWarnings("PMD.UseLocaleWithCaseConversions")
public String toString() {
- return (Strings.isNullOrEmpty(group) ? "" : group + "/")
- + version.toUpperCase() + kind + " " + namespace + ":" + name;
+ return (Strings.isNullOrEmpty(group()) ? "" : group() + "/")
+ + version().toUpperCase() + kind() + " " + namespace + ":" + name;
+ }
+
+ /**
+ * Get a namespaced object stub. If the version in parameter
+ * `gvk` is an empty string, the stub refers to the first object
+ * found with matching group and kind.
+ *
+ * @param the object type
+ * @param the object list type
+ * @param the stub type
+ * @param objectClass the object class
+ * @param objectListClass the object list class
+ * @param client the client
+ * @param gvk the group, version and kind
+ * @param namespace the namespace
+ * @param name the name
+ * @param provider the provider
+ * @return the stub if the object exists
+ * @throws ApiException the api exception
+ */
+ @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop" })
+ public static