From 659463b3b46c5bd41bc2ba56ccdc8fe14087d6c5 Mon Sep 17 00:00:00 2001
From: "Michael N. Lipp" <1446020+mnlipp@users.noreply.github.com>
Date: Sat, 1 Jun 2024 11:12:15 +0200
Subject: [PATCH] Viewer ACL (#26)
Provide ACLs (together with general improvements) for the viewer conlet.
---
deploy/crds/vms-crd.yaml | 27 ++
dev-example/config.yaml | 7 +-
dev-example/kustomization.yaml | 6 +
dev-example/test-vm.yaml | 10 +
.../common/DynamicTypeAdapterFactory.java | 197 ++++++++++++
.../K8sDynamicModelTypeAdapterFactory.java | 131 --------
.../vmoperator/common/K8sDynamicModels.java | 123 +-------
.../common/K8sDynamicModelsBase.java | 174 +++++++++++
.../vmoperator/common/K8sDynamicStub.java | 58 ++--
.../vmoperator/common/K8sDynamicStubBase.java | 51 ++++
.../vmoperator/common/K8sGenericStub.java | 123 ++++----
.../vmoperator/common/K8sV1PodStub.java | 15 +-
.../vmoperator/common/K8sV1SecretStub.java | 29 +-
.../vmoperator/common/K8sV1ServiceStub.java | 16 +-
.../vmoperator/common/VmDefinitionModel.java | 123 ++++++++
.../vmoperator/common/VmDefinitionModels.java | 39 +++
.../vmoperator/common/VmDefinitionStub.java | 157 ++++++++++
.../events/DisplayPasswordChanged.java | 76 -----
.../manager/events/GetDisplayPassword.java | 24 +-
.../vmoperator/manager/events/VmChannel.java | 8 +-
.../manager/events/VmDefChanged.java | 8 +-
org.jdrupes.vmoperator.manager/build.gradle | 1 +
.../manager/ManagerIntro-Preview.md | 5 +
.../manager/ManagerIntro-Preview_de.md | 6 +
.../vmoperator/manager/l10n.properties | 1 +
.../vmoperator/manager/l10n_de.properties | 19 ++
.../vmoperator/manager/AvoidEmptyPolicy.java | 51 +++-
.../manager/ConfigMapReconciler.java | 4 +-
.../jdrupes/vmoperator/manager/Constants.java | 6 +
.../vmoperator/manager/Controller.java | 2 +-
.../manager/DisplayPasswordMonitor.java | 102 -------
.../manager/DisplaySecretMonitor.java | 285 ++++++++++++++++++
.../manager/DisplaySecretReconciler.java | 106 +++++++
.../vmoperator/manager/Reconciler.java | 3 +
.../jdrupes/vmoperator/manager/VmMonitor.java | 18 +-
.../vmoperator/runner/qemu/StatusUpdater.java | 9 +-
.../jdrupes/vmoperator/vmconlet/VmConlet.java | 10 +-
.../vmconlet/browser/VmConlet-style.scss | 2 +-
.../vmoperator/vmviewer/l10n_de.properties | 5 +-
.../jdrupes/vmoperator/vmviewer/VmViewer.java | 264 +++++++++++++---
.../vmviewer/browser/VmViewer-functions.ts | 11 +-
.../vmviewer/browser/VmViewer-style.scss | 31 +-
42 files changed, 1664 insertions(+), 679 deletions(-)
create mode 100644 org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/DynamicTypeAdapterFactory.java
delete 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/K8sDynamicModelsBase.java
create mode 100644 org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicStubBase.java
create mode 100644 org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionModel.java
create mode 100644 org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionModels.java
create mode 100644 org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionStub.java
delete mode 100644 org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/DisplayPasswordChanged.java
create mode 100644 org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/ManagerIntro-Preview.md
create mode 100644 org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/ManagerIntro-Preview_de.md
create mode 100644 org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/l10n_de.properties
delete mode 100644 org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplayPasswordMonitor.java
create mode 100644 org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretMonitor.java
create mode 100644 org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretReconciler.java
diff --git a/deploy/crds/vms-crd.yaml b/deploy/crds/vms-crd.yaml
index de65f8a..f441cbc 100644
--- a/deploy/crds/vms-crd.yaml
+++ b/deploy/crds/vms-crd.yaml
@@ -990,6 +990,30 @@ spec:
description: Copied to cloud-init's network-config file.
type: object
x-kubernetes-preserve-unknown-fields: true
+ permissions:
+ type: array
+ description: >-
+ Defines permissions for accessing and manipulating the VM.
+ items:
+ type: object
+ description: >-
+ Permissions can be granted to a user or to a role.
+ oneOf:
+ - required:
+ - user
+ - required:
+ - role
+ properties:
+ user:
+ type: string
+ role:
+ type: string
+ may:
+ type: array
+ items:
+ type: string
+ enum: ["start", "stop", "accessConsole", "*"]
+ default: []
vm:
type: object
description: Defines the VM.
@@ -1395,6 +1419,9 @@ spec:
to the spice server. Defaults to the address
of the node that the VM is running on.
type: string
+ generateSecret:
+ type: boolean
+ default: true
proxyUrl:
description: >-
If specified, is copied to the generated
diff --git a/dev-example/config.yaml b/dev-example/config.yaml
index 3c94254..579103d 100644
--- a/dev-example/config.yaml
+++ b/dev-example/config.yaml
@@ -28,6 +28,8 @@
# User admin has role admin
admin:
- admin
+ test:
+ - user
# All users have role other
"*":
- other
@@ -37,11 +39,14 @@
# Admins can use all conlets
admin:
- "*"
+ user:
+ - org.jdrupes.vmoperator.vmviewer.VmViewer
# Others cannot use any conlet (except login conlet to log out)
other:
- - --org.jdrupes.vmoperator.vmconlet.VmConlet
- org.jgrapes.webconlet.oidclogin.LoginConlet
"/ComponentCollector":
"/VmViewer":
displayResource:
preferredIpVersion: ipv4
+ syncPreviewsFor:
+ - role: user
diff --git a/dev-example/kustomization.yaml b/dev-example/kustomization.yaml
index 9829bf6..19b6295 100644
--- a/dev-example/kustomization.yaml
+++ b/dev-example/kustomization.yaml
@@ -54,6 +54,8 @@ patches:
# User admin has role admin
admin:
- admin
+ test:
+ - user
# All users have role other
"*":
- other
@@ -63,6 +65,8 @@ patches:
# Admins can use all conlets
admin:
- "*"
+ user:
+ - org.jdrupes.vmoperator.vmviewer.VmViewer
# Others cannot use any conlet (except login conlet to log out)
other:
- org.jgrapes.webconlet.locallogin.LoginConlet
@@ -70,6 +74,8 @@ patches:
"/VmViewer":
displayResource:
preferredIpVersion: ipv4
+ syncPreviewsFor:
+ - role: user
- target:
group: apps
version: v1
diff --git a/dev-example/test-vm.yaml b/dev-example/test-vm.yaml
index 19144d5..e874ef8 100644
--- a/dev-example/test-vm.yaml
+++ b/dev-example/test-vm.yaml
@@ -7,8 +7,17 @@ spec:
image:
repository: docker-registry.lan.mnl.de
path: vmoperator/org.jdrupes.vmoperator.runner.qemu-alpine
+ version: latest
pullPolicy: Always
+ permissions:
+ - user: admin
+ may:
+ - "*"
+ - user: test
+ may:
+ - "accessConsole"
+
resources:
requests:
cpu: 1
@@ -52,3 +61,4 @@ spec:
display:
spice:
port: 5810
+ generateSecret: true
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/DynamicTypeAdapterFactory.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/DynamicTypeAdapterFactory.java
new file mode 100644
index 0000000..d21eed4
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/DynamicTypeAdapterFactory.java
@@ -0,0 +1,197 @@
+/*
+ * 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 io.kubernetes.client.openapi.ApiClient;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Type;
+
+/**
+ * A factory for creating objects.
+ *
+ * @param the generic type
+ * @param the generic type
+ */
+public class DynamicTypeAdapterFactory> implements TypeAdapterFactory {
+
+ private final Class objectClass;
+ private final Class objectListClass;
+
+ /**
+ * Make sure that this adapter is registered.
+ *
+ * @param client the client
+ */
+ public void register(ApiClient client) {
+ if (!ModelCreator.class
+ .equals(client.getJSON().getGson().getAdapter(objectClass)
+ .getClass())
+ || !ModelsCreator.class.equals(client.getJSON().getGson()
+ .getAdapter(objectListClass).getClass())) {
+ Gson gson = client.getJSON().getGson();
+ client.getJSON().setGson(gson.newBuilder()
+ .registerTypeAdapterFactory(this).create());
+ }
+ }
+
+ /**
+ * Instantiates a new generic type adapter factory.
+ *
+ * @param objectClass the object class
+ * @param objectListClass the object list class
+ */
+ public DynamicTypeAdapterFactory(Class objectClass,
+ Class objectListClass) {
+ this.objectClass = objectClass;
+ this.objectListClass = objectListClass;
+ }
+
+ /**
+ * 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")
+ @Override
+ public TypeAdapter create(Gson gson, TypeToken typeToken) {
+ if (TypeToken.get(objectClass).equals(typeToken)) {
+ return (TypeAdapter) new ModelCreator(gson);
+ }
+ if (TypeToken.get(objectListClass).equals(typeToken)) {
+ return (TypeAdapter) new ModelsCreator(gson);
+ }
+ return null;
+ }
+
+ /**
+ * The Class ModelCreator.
+ */
+ private class ModelCreator extends TypeAdapter
+ implements InstanceCreator {
+ private final Gson delegate;
+
+ /**
+ * Instantiates a new object state creator.
+ *
+ * @param delegate the delegate
+ */
+ public ModelCreator(Gson delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public O createInstance(Type type) {
+ try {
+ return objectClass.getConstructor(Gson.class, JsonObject.class)
+ .newInstance(delegate, null);
+ } catch (InstantiationException | IllegalAccessException
+ | IllegalArgumentException | InvocationTargetException
+ | NoSuchMethodException | SecurityException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public void write(JsonWriter jsonWriter, O state)
+ throws IOException {
+ jsonWriter.jsonValue(delegate.toJson(state.data()));
+ }
+
+ @Override
+ public O read(JsonReader jsonReader)
+ throws IOException {
+ try {
+ return objectClass.getConstructor(Gson.class, JsonObject.class)
+ .newInstance(delegate,
+ delegate.fromJson(jsonReader, JsonObject.class));
+ } catch (InstantiationException | IllegalAccessException
+ | IllegalArgumentException | InvocationTargetException
+ | NoSuchMethodException | SecurityException e) {
+ return null;
+ }
+ }
+ }
+
+ /**
+ * The Class ModelsCreator.
+ */
+ private class ModelsCreator extends TypeAdapter
+ implements InstanceCreator {
+
+ private final Gson delegate;
+
+ /**
+ * Instantiates a new object states creator.
+ *
+ * @param delegate the delegate
+ */
+ public ModelsCreator(Gson delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public L createInstance(Type type) {
+ try {
+ return objectListClass
+ .getConstructor(Gson.class, JsonObject.class)
+ .newInstance(delegate, null);
+ } catch (InstantiationException | IllegalAccessException
+ | IllegalArgumentException | InvocationTargetException
+ | NoSuchMethodException | SecurityException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public void write(JsonWriter jsonWriter, L states)
+ throws IOException {
+ jsonWriter.jsonValue(delegate.toJson(states.data()));
+ }
+
+ @Override
+ public L read(JsonReader jsonReader)
+ throws IOException {
+ try {
+ return objectListClass
+ .getConstructor(Gson.class, JsonObject.class)
+ .newInstance(delegate,
+ delegate.fromJson(jsonReader, JsonObject.class));
+ } catch (InstantiationException | IllegalAccessException
+ | IllegalArgumentException | InvocationTargetException
+ | NoSuchMethodException | SecurityException e) {
+ return null;
+ }
+ }
+ }
+
+}
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
deleted file mode 100644
index 33a8e18..0000000
--- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModelTypeAdapterFactory.java
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * 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")
- @Override
- 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
index 165b10e..d165c10 100644
--- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModels.java
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModels.java
@@ -19,14 +19,8 @@
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
@@ -35,11 +29,7 @@ import java.util.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;
+public class K8sDynamicModels extends K8sDynamicModelsBase {
/**
* Initialize the object list using the given JSON data.
@@ -48,116 +38,7 @@ public class K8sDynamicModels implements KubernetesListObject {
* @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()));
- }
+ super(K8sDynamicModel.class, delegate, data);
}
- @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/K8sDynamicModelsBase.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModelsBase.java
new file mode 100644
index 0000000..4e21c0e
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModelsBase.java
@@ -0,0 +1,174 @@
+/*
+ * 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.lang.reflect.InvocationTargetException;
+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 K8sDynamicModelsBase
+ implements KubernetesListObject {
+
+ private final JsonObject data;
+ private final V1ListMeta metadata;
+ private final List items;
+
+ /**
+ * Initialize the object list using the given JSON data.
+ *
+ * @param itemClass the item class
+ * @param delegate the gson instance to use for extracting structured data
+ * @param data the data
+ */
+ public K8sDynamicModelsBase(Class itemClass, 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()) {
+ try {
+ items.add(itemClass.getConstructor(Gson.class, JsonObject.class)
+ .newInstance(delegate, e.getAsJsonObject()));
+ } catch (InstantiationException | IllegalAccessException
+ | IllegalArgumentException | InvocationTargetException
+ | NoSuchMethodException | SecurityException exc) {
+ throw new IllegalArgumentException(exc);
+ }
+ }
+ }
+
+ @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;
+ }
+ K8sDynamicModelsBase> other = (K8sDynamicModelsBase>) 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
index e6d36c5..afed802 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,8 @@
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;
@@ -35,40 +33,23 @@ import java.util.Collection;
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class K8sDynamicStub
- extends K8sGenericStub {
+ extends K8sDynamicStubBase {
+
+ private static DynamicTypeAdapterFactory taf = new K8sDynamicModelTypeAdapterFactory();
/**
* 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,
+ public K8sDynamicStub(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());
+ super(K8sDynamicModel.class, K8sDynamicModels.class, taf, client,
+ context, namespace, name);
}
/**
@@ -88,8 +69,8 @@ public class K8sDynamicStub
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);
+ return new K8sDynamicStub(client, apiResource(client, gvk), namespace,
+ name);
}
/**
@@ -106,8 +87,7 @@ public class K8sDynamicStub
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
public static K8sDynamicStub get(K8sClient client,
APIResource context, String namespace, String name) {
- return K8sGenericStub.get(K8sDynamicModel.class, K8sDynamicModels.class,
- client, context, namespace, name, K8sDynamicStub::new);
+ return new K8sDynamicStub(client, context, namespace, name);
}
/**
@@ -125,7 +105,7 @@ public class K8sDynamicStub
K8s.yamlToJson(client, yaml));
return K8sGenericStub.create(K8sDynamicModel.class,
K8sDynamicModels.class, client, context, model,
- K8sDynamicStub::new);
+ (c, ns, n) -> new K8sDynamicStub(c, context, ns, n));
}
/**
@@ -143,7 +123,7 @@ public class K8sDynamicStub
throws ApiException {
return K8sGenericStub.list(K8sDynamicModel.class,
K8sDynamicModels.class, client, context, namespace, options,
- K8sDynamicStub::new);
+ (c, ns, n) -> new K8sDynamicStub(c, context, ns, n));
}
/**
@@ -160,4 +140,18 @@ public class K8sDynamicStub
return list(client, context, namespace, new ListOptions());
}
+ /**
+ * A factory for creating K8sDynamicModel(s) objects.
+ */
+ public static class K8sDynamicModelTypeAdapterFactory extends
+ DynamicTypeAdapterFactory {
+
+ /**
+ * Instantiates a new dynamic model type adapter factory.
+ */
+ public K8sDynamicModelTypeAdapterFactory() {
+ super(K8sDynamicModel.class, K8sDynamicModels.class);
+ }
+ }
+
}
\ No newline at end of file
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicStubBase.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicStubBase.java
new file mode 100644
index 0000000..44f419c
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicStubBase.java
@@ -0,0 +1,51 @@
+/*
+ * 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;
+
+/**
+ * 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 abstract class K8sDynamicStubBase> 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
+ */
+ @SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
+ public K8sDynamicStubBase(Class objectClass,
+ Class objectListClass, DynamicTypeAdapterFactory taf,
+ K8sClient client, APIResource context, String namespace,
+ String name) {
+ super(objectClass, objectListClass, client, context, namespace, name);
+ taf.register(client);
+ }
+}
\ 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 db68a38..f118a17 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
@@ -26,9 +26,11 @@ import io.kubernetes.client.custom.V1Patch;
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.KubernetesApiResponse;
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 io.kubernetes.client.util.generic.options.UpdateOptions;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.Collection;
@@ -228,7 +230,8 @@ public class K8sGenericStub patch(String patchType, V1Patch patch,
PatchOptions options) throws ApiException {
return K8s
- .optional(api.patch(namespace, name, patchType, patch, options));
+ .optional(api.patch(namespace, name, patchType, patch, options)
+ .throwsApiException());
}
/**
@@ -245,6 +248,30 @@ public class K8sGenericStub update(O object) throws ApiException {
+ return api.update(object).throwsApiException();
+ }
+
+ /**
+ * Update the object.
+ *
+ * @param object the object
+ * @param options the options
+ * @return the kubernetes api response
+ * @throws ApiException the api exception
+ */
+ public KubernetesApiResponse update(O object, UpdateOptions options)
+ throws ApiException {
+ return api.update(object, options).throwsApiException();
+ }
+
/**
* A supplier for generic stubs.
*
@@ -258,17 +285,13 @@ public class K8sGenericStub objectClass, Class objectListClass, K8sClient client,
- APIResource context, String namespace, String name);
+ R get(K8sClient client, String namespace, String name);
}
@Override
@@ -278,68 +301,6 @@ public class K8sGenericStub 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 >
- 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.UseObjectForClearerAPI" })
- public static >
- R get(Class objectClass, Class objectListClass,
- K8sClient client, APIResource context, String namespace,
- String name, GenericSupplier provider) {
- return provider.get(objectClass, objectListClass, client,
- context, namespace, name);
- }
-
/**
* Get a namespaced object stub for a newly created object.
*
@@ -366,8 +327,7 @@ public class K8sGenericStub {
public static Collection list(K8sClient client,
String namespace, ListOptions options) throws ApiException {
return K8sGenericStub.list(V1Pod.class, V1PodList.class, client,
- CONTEXT, namespace, options, K8sV1PodStub::getGeneric);
+ CONTEXT, namespace, options, (clnt, nscp,
+ name) -> new K8sV1PodStub(clnt, nscp, name));
}
-
- /**
- * Provide {@link GenericSupplier}.
- */
- @SuppressWarnings({ "PMD.UnusedFormalParameter",
- "PMD.UnusedPrivateMethod" })
- private static K8sV1PodStub getGeneric(Class objectClass,
- Class objectListClass, K8sClient client,
- APIResource context, String namespace, String name) {
- return new K8sV1PodStub(client, namespace, name);
- }
-
}
\ No newline at end of file
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1SecretStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1SecretStub.java
index 9a43883..a847d36 100644
--- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1SecretStub.java
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1SecretStub.java
@@ -25,7 +25,6 @@ import io.kubernetes.client.openapi.models.V1SecretList;
import io.kubernetes.client.util.generic.options.ListOptions;
import java.util.Collection;
import java.util.List;
-import org.jdrupes.vmoperator.common.K8sGenericStub.GenericSupplier;
/**
* A stub for secrets (v1).
@@ -62,6 +61,20 @@ public class K8sV1SecretStub extends K8sGenericStub {
return new K8sV1SecretStub(client, namespace, name);
}
+ /**
+ * Creates an object stub from a model.
+ *
+ * @param client the client
+ * @param model the model
+ * @return the k 8 s dynamic stub
+ * @throws ApiException the api exception
+ */
+ public static K8sV1SecretStub create(K8sClient client, V1Secret model)
+ throws ApiException {
+ return K8sGenericStub.create(V1Secret.class,
+ V1SecretList.class, client, CONTEXT, model, K8sV1SecretStub::new);
+ }
+
/**
* Get the stubs for the objects in the given namespace that match
* the criteria from the given options.
@@ -75,18 +88,6 @@ public class K8sV1SecretStub extends K8sGenericStub {
public static Collection list(K8sClient client,
String namespace, ListOptions options) throws ApiException {
return K8sGenericStub.list(V1Secret.class, V1SecretList.class, client,
- CONTEXT, namespace, options, K8sV1SecretStub::getGeneric);
+ CONTEXT, namespace, options, K8sV1SecretStub::new);
}
-
- /**
- * Provide {@link GenericSupplier}.
- */
- @SuppressWarnings({ "PMD.UnusedFormalParameter",
- "PMD.UnusedPrivateMethod" })
- private static K8sV1SecretStub getGeneric(Class objectClass,
- Class objectListClass, K8sClient client,
- APIResource context, String namespace, String name) {
- return new K8sV1SecretStub(client, namespace, name);
- }
-
}
\ No newline at end of file
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1ServiceStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1ServiceStub.java
index 74f7f61..2157a1d 100644
--- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1ServiceStub.java
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1ServiceStub.java
@@ -25,7 +25,6 @@ import io.kubernetes.client.openapi.models.V1ServiceList;
import io.kubernetes.client.util.generic.options.ListOptions;
import java.util.Collection;
import java.util.List;
-import org.jdrupes.vmoperator.common.K8sGenericStub.GenericSupplier;
/**
* A stub for secrets (v1).
@@ -75,18 +74,7 @@ public class K8sV1ServiceStub extends K8sGenericStub {
public static Collection list(K8sClient client,
String namespace, ListOptions options) throws ApiException {
return K8sGenericStub.list(V1Service.class, V1ServiceList.class, client,
- CONTEXT, namespace, options, K8sV1ServiceStub::getGeneric);
+ CONTEXT, namespace, options,
+ (clnt, nscp, name) -> new K8sV1ServiceStub(clnt, nscp, name));
}
-
- /**
- * Provide {@link GenericSupplier}.
- */
- @SuppressWarnings({ "PMD.UnusedFormalParameter",
- "PMD.UnusedPrivateMethod" })
- private static K8sV1ServiceStub getGeneric(Class objectClass,
- Class objectListClass, K8sClient client,
- APIResource context, String namespace, String name) {
- return new K8sV1ServiceStub(client, namespace, name);
- }
-
}
\ No newline at end of file
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionModel.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionModel.java
new file mode 100644
index 0000000..fa59c82
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionModel.java
@@ -0,0 +1,123 @@
+/*
+ * 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 com.google.gson.JsonPrimitive;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import org.jdrupes.vmoperator.util.GsonPtr;
+
+/**
+ * Represents a VM definition.
+ */
+@SuppressWarnings("PMD.DataClass")
+public class VmDefinitionModel extends K8sDynamicModel {
+
+ /**
+ * Permissions for accessing and manipulating the VM.
+ */
+ public enum Permission {
+ START("start"), STOP("stop"), ACCESS_CONSOLE("accessConsole");
+
+ @SuppressWarnings("PMD.UseConcurrentHashMap")
+ private static Map reprs = new HashMap<>();
+
+ static {
+ for (var value : EnumSet.allOf(Permission.class)) {
+ reprs.put(value.repr, value);
+ }
+ }
+
+ private final String repr;
+
+ Permission(String repr) {
+ this.repr = repr;
+ }
+
+ /**
+ * Create permission from representation in CRD.
+ *
+ * @param value the value
+ * @return the permission
+ */
+ @SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
+ public static Set parse(String value) {
+ if ("*".equals(value)) {
+ return EnumSet.allOf(Permission.class);
+ }
+ return Set.of(reprs.get(value));
+ }
+
+ @Override
+ public String toString() {
+ return repr;
+ }
+ }
+
+ /**
+ * Instantiates a new model from the JSON representation.
+ *
+ * @param delegate the gson instance to use for extracting structured data
+ * @param json the JSON
+ */
+ public VmDefinitionModel(Gson delegate, JsonObject json) {
+ super(delegate, json);
+ }
+
+ /**
+ * Collect all permissions for the given user with the given roles.
+ *
+ * @param user the user
+ * @param roles the roles
+ * @return the sets the
+ */
+ public Set permissionsFor(String user,
+ Collection roles) {
+ return GsonPtr.to(data())
+ .getAsListOf(JsonObject.class, "spec", "permissions")
+ .stream().filter(p -> GsonPtr.to(p).getAsString("user")
+ .map(u -> u.equals(user)).orElse(false)
+ || GsonPtr.to(p).getAsString("role").map(roles::contains)
+ .orElse(false))
+ .map(p -> GsonPtr.to(p).getAsListOf(JsonPrimitive.class, "may")
+ .stream())
+ .flatMap(Function.identity()).map(p -> p.getAsString())
+ .map(Permission::parse).map(Set::stream)
+ .flatMap(Function.identity()).collect(Collectors.toSet());
+ }
+
+ /**
+ * Get the display password serial.
+ *
+ * @return the optional
+ */
+ public Optional displayPasswordSerial() {
+ return GsonPtr.to(status())
+ .get(JsonPrimitive.class, "displayPasswordSerial")
+ .map(JsonPrimitive::getAsLong);
+ }
+}
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionModels.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionModels.java
new file mode 100644
index 0000000..5ac412f
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionModels.java
@@ -0,0 +1,39 @@
+/*
+ * 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;
+
+/**
+ * Represents a list of {@link VmDefinitionModel}s.
+ */
+public class VmDefinitionModels
+ extends K8sDynamicModelsBase {
+
+ /**
+ * 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 VmDefinitionModels(Gson delegate, JsonObject data) {
+ super(VmDefinitionModel.class, delegate, data);
+ }
+}
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionStub.java
new file mode 100644
index 0000000..49da3e0
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionStub.java
@@ -0,0 +1,157 @@
+/*
+ * 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 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
+ * (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 VmDefinitionStub
+ extends K8sDynamicStubBase {
+
+ private static DynamicTypeAdapterFactory taf = new VmDefintionModelTypeAdapterFactory();
+
+ /**
+ * Instantiates a new stub for VM defintions.
+ *
+ * @param client the client
+ * @param context the context
+ * @param namespace the namespace
+ * @param name the name
+ */
+ public VmDefinitionStub(K8sClient client, APIResource context,
+ String namespace, String name) {
+ super(VmDefinitionModel.class, VmDefinitionModels.class, taf, 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 VmDefinitionStub get(K8sClient client,
+ GroupVersionKind gvk, String namespace, String name)
+ throws ApiException {
+ return new VmDefinitionStub(client, apiResource(client, gvk), namespace,
+ name);
+ }
+
+ /**
+ * 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 VmDefinitionStub get(K8sClient client,
+ APIResource context, String namespace, String name) {
+ return new VmDefinitionStub(client, context, namespace, name);
+ }
+
+ /**
+ * 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 VmDefinitionStub createFromYaml(K8sClient client,
+ APIResource context, Reader yaml) throws ApiException {
+ var model = new VmDefinitionModel(client.getJSON().getGson(),
+ K8s.yamlToJson(client, yaml));
+ return K8sGenericStub.create(VmDefinitionModel.class,
+ VmDefinitionModels.class, client, context, model,
+ (c, ns, n) -> new VmDefinitionStub(c, context, ns, n));
+ }
+
+ /**
+ * 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(VmDefinitionModel.class,
+ VmDefinitionModels.class, client, context, namespace, options,
+ (c, ns, n) -> new VmDefinitionStub(c, context, ns, n));
+ }
+
+ /**
+ * 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());
+ }
+
+ /**
+ * A factory for creating VmDefinitionModel(s) objects.
+ */
+ public static class VmDefintionModelTypeAdapterFactory extends
+ DynamicTypeAdapterFactory {
+
+ /**
+ * Instantiates a new dynamic model type adapter factory.
+ */
+ public VmDefintionModelTypeAdapterFactory() {
+ super(VmDefinitionModel.class, VmDefinitionModels.class);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/DisplayPasswordChanged.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/DisplayPasswordChanged.java
deleted file mode 100644
index 9185bbc..0000000
--- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/DisplayPasswordChanged.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * 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 io.kubernetes.client.openapi.models.V1Secret;
-import org.jdrupes.vmoperator.common.K8sObserver.ResponseType;
-import org.jgrapes.core.Channel;
-import org.jgrapes.core.Components;
-import org.jgrapes.core.Event;
-
-/**
- * Indicates that a display secret has changed.
- */
-@SuppressWarnings("PMD.DataClass")
-public class DisplayPasswordChanged extends Event {
-
- private final ResponseType type;
- private final V1Secret secret;
-
- /**
- * Initializes a new display secret changed event.
- *
- * @param type the type
- * @param secret the secret
- */
- public DisplayPasswordChanged(ResponseType type, V1Secret secret) {
- this.type = type;
- this.secret = secret;
- }
-
- /**
- * Returns the type.
- *
- * @return the type
- */
- public ResponseType type() {
- return type;
- }
-
- /**
- * Gets the secret.
- *
- * @return the secret
- */
- public V1Secret secret() {
- return secret;
- }
-
- @Override
- public String toString() {
- StringBuilder builder = new StringBuilder();
- builder.append(Components.objectName(this)).append(" [")
- .append(secret.getMetadata().getName()).append(' ').append(type);
- if (channels() != null) {
- builder.append(", channels=").append(Channel.toString(channels()));
- }
- builder.append(']');
- return builder.toString();
- }
-}
diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetDisplayPassword.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetDisplayPassword.java
index 77dc298..37eddec 100644
--- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetDisplayPassword.java
+++ b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetDisplayPassword.java
@@ -19,40 +19,44 @@
package org.jdrupes.vmoperator.manager.events;
import java.util.Optional;
+import org.jdrupes.vmoperator.common.VmDefinitionModel;
import org.jgrapes.core.Event;
/**
- * Gets the current display secret.
+ * Gets the current display secret and optionally updates it.
*/
@SuppressWarnings("PMD.DataClass")
public class GetDisplayPassword extends Event {
- private final String vmName;
+ private final VmDefinitionModel vmDef;
/**
* Instantiates a new returns the display secret.
*
- * @param vmName the vm name
+ * @param vmDef the vm name
*/
- public GetDisplayPassword(String vmName) {
- this.vmName = vmName;
+ public GetDisplayPassword(VmDefinitionModel vmDef) {
+ this.vmDef = vmDef;
}
/**
- * Gets the vm name.
+ * Gets the vm definition.
*
- * @return the vm name
+ * @return the vm definition
*/
- public String vmName() {
- return vmName;
+ public VmDefinitionModel vmDefinition() {
+ return vmDef;
}
/**
- * Return the password. Should only be called when the event is completed.
+ * Return the password. May only be called when the event is completed.
*
* @return the optional
*/
public Optional password() {
+ if (!isDone()) {
+ throw new IllegalStateException("Event is not done.");
+ }
return currentResults().stream().findFirst();
}
}
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 972693a..46861ce 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
@@ -19,7 +19,7 @@
package org.jdrupes.vmoperator.manager.events;
import org.jdrupes.vmoperator.common.K8sClient;
-import org.jdrupes.vmoperator.common.K8sDynamicModel;
+import org.jdrupes.vmoperator.common.VmDefinitionModel;
import org.jgrapes.core.Channel;
import org.jgrapes.core.EventPipeline;
import org.jgrapes.core.Subchannel.DefaultSubchannel;
@@ -32,7 +32,7 @@ public class VmChannel extends DefaultSubchannel {
private final EventPipeline pipeline;
private final K8sClient client;
- private K8sDynamicModel vmDefinition;
+ private VmDefinitionModel vmDefinition;
private long generation = -1;
/**
@@ -56,7 +56,7 @@ public class VmChannel extends DefaultSubchannel {
* @return the watch channel
*/
@SuppressWarnings("PMD.LinguisticNaming")
- public VmChannel setVmDefinition(K8sDynamicModel definition) {
+ public VmChannel setVmDefinition(VmDefinitionModel definition) {
this.vmDefinition = definition;
return this;
}
@@ -66,7 +66,7 @@ public class VmChannel extends DefaultSubchannel {
*
* @return the json object
*/
- public K8sDynamicModel vmDefinition() {
+ public VmDefinitionModel vmDefinition() {
return vmDefinition;
}
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 5e93790..a2bafb7 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
@@ -18,8 +18,8 @@
package org.jdrupes.vmoperator.manager.events;
-import org.jdrupes.vmoperator.common.K8sDynamicModel;
import org.jdrupes.vmoperator.common.K8sObserver;
+import org.jdrupes.vmoperator.common.VmDefinitionModel;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Components;
import org.jgrapes.core.Event;
@@ -36,7 +36,7 @@ public class VmDefChanged extends Event {
private final K8sObserver.ResponseType type;
private final boolean specChanged;
- private final K8sDynamicModel vmDef;
+ private final VmDefinitionModel vmDef;
/**
* Instantiates a new VM changed event.
@@ -46,7 +46,7 @@ public class VmDefChanged extends Event {
* @param vmDefinition the VM definition
*/
public VmDefChanged(K8sObserver.ResponseType type, boolean specChanged,
- K8sDynamicModel vmDefinition) {
+ VmDefinitionModel vmDefinition) {
this.type = type;
this.specChanged = specChanged;
this.vmDef = vmDefinition;
@@ -73,7 +73,7 @@ public class VmDefChanged extends Event {
*
* @return the object.
*/
- public K8sDynamicModel vmDefinition() {
+ public VmDefinitionModel vmDefinition() {
return vmDef;
}
diff --git a/org.jdrupes.vmoperator.manager/build.gradle b/org.jdrupes.vmoperator.manager/build.gradle
index 7dc07a2..a8b67a0 100644
--- a/org.jdrupes.vmoperator.manager/build.gradle
+++ b/org.jdrupes.vmoperator.manager/build.gradle
@@ -22,6 +22,7 @@ dependencies {
implementation 'org.jgrapes:org.jgrapes.webconsole.vuejs:[1.5.0,2)'
implementation 'org.jgrapes:org.jgrapes.webconsole.rbac:[1.3.0,2)'
implementation 'org.jgrapes:org.jgrapes.webconlet.oidclogin:[1.3.0,2)'
+ implementation 'org.jgrapes:org.jgrapes.webconlet.markdowndisplay:[1.2.0,2)'
runtimeOnly 'org.jgrapes:org.jgrapes.webconlet.sysinfo:[1.4.0,2)'
runtimeOnly 'org.jgrapes:org.jgrapes.webconlet.logviewer:[0.2.0,2)'
diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/ManagerIntro-Preview.md b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/ManagerIntro-Preview.md
new file mode 100644
index 0000000..50a3024
--- /dev/null
+++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/ManagerIntro-Preview.md
@@ -0,0 +1,5 @@
+You can use the "puzzle piece" icon on the top right corner of the
+page to add display widgets (conlets) to the overview tab.
+
+Use the "full screen" icon on the top right corner of any
+conlet (if available) to get a detailed view.
\ No newline at end of file
diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/ManagerIntro-Preview_de.md b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/ManagerIntro-Preview_de.md
new file mode 100644
index 0000000..e5e4d68
--- /dev/null
+++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/ManagerIntro-Preview_de.md
@@ -0,0 +1,6 @@
+Verwenden Sie das "Puzzle"-Icon auf der rechten oberen Ecke
+der Seite, um Anzeige-Widgets (Conlets) hinzuzufügen.
+
+Wenn sich in der rechten oberen Ecke eines Conlets ein Vollbild-Icon
+befindet, können Sie es verwenden, um eine Detailansicht in einem neuen
+Register anzufordern.
diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/l10n.properties b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/l10n.properties
index ec22a06..6bcc3a2 100644
--- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/l10n.properties
+++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/l10n.properties
@@ -17,3 +17,4 @@
#
consoleTitle = VM-Operator
+introTitle = Usage
\ No newline at end of file
diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/l10n_de.properties b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/l10n_de.properties
new file mode 100644
index 0000000..dcbba93
--- /dev/null
+++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/l10n_de.properties
@@ -0,0 +1,19 @@
+#
+# 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 .
+#
+
+introTitle = Benutzung
diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/AvoidEmptyPolicy.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/AvoidEmptyPolicy.java
index 000a21e..912b623 100644
--- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/AvoidEmptyPolicy.java
+++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/AvoidEmptyPolicy.java
@@ -18,11 +18,17 @@
package org.jdrupes.vmoperator.manager;
+import java.io.BufferedReader;
import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.Collections;
+import java.util.ResourceBundle;
+import java.util.stream.Collectors;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Component;
import org.jgrapes.core.annotation.Handler;
-import org.jgrapes.webconsole.base.Conlet;
+import org.jgrapes.webconlet.markdowndisplay.MarkdownDisplayConlet;
+import org.jgrapes.webconsole.base.Conlet.RenderMode;
import org.jgrapes.webconsole.base.ConsoleConnection;
import org.jgrapes.webconsole.base.events.AddConletRequest;
import org.jgrapes.webconsole.base.events.ConsoleConfigured;
@@ -63,10 +69,13 @@ public class AvoidEmptyPolicy extends Component {
* @param event the event
* @param connection the connection
*/
- @Handler
+ @Handler(priority = 100)
public void onRenderConlet(RenderConlet event,
ConsoleConnection connection) {
- connection.session().put(renderedFlagName, true);
+ if (event.renderAs().contains(RenderMode.Preview)
+ || event.renderAs().contains(RenderMode.View)) {
+ connection.session().put(renderedFlagName, true);
+ }
}
/**
@@ -76,18 +85,42 @@ public class AvoidEmptyPolicy extends Component {
* @param connection the console connection
* @throws InterruptedException the interrupted exception
*/
- @Handler
+ @Handler(priority = -100)
public void onConsoleConfigured(ConsoleConfigured event,
ConsoleConnection connection) throws InterruptedException,
IOException {
- if ((Boolean) connection.session().getOrDefault(
- renderedFlagName, false)) {
+ if ((Boolean) connection.session().getOrDefault(renderedFlagName,
+ false)) {
return;
}
+ var resourceBundle = ResourceBundle.getBundle(
+ getClass().getPackage().getName() + ".l10n", connection.locale(),
+ getClass().getClassLoader(),
+ ResourceBundle.Control.getNoFallbackControl(
+ ResourceBundle.Control.FORMAT_DEFAULT));
+ var locale = resourceBundle.getLocale().toString();
+ String shortDesc;
+ try (BufferedReader shortDescReader
+ = new BufferedReader(new InputStreamReader(
+ AvoidEmptyPolicy.class.getResourceAsStream(
+ "ManagerIntro-Preview" + (locale.isEmpty() ? ""
+ : "_" + locale) + ".md"),
+ "utf-8"))) {
+ shortDesc
+ = shortDescReader.lines().collect(Collectors.joining("\n"));
+ }
fire(new AddConletRequest(event.event().event().renderSupport(),
- "org.jdrupes.vmoperator.vmconlet.VmConlet",
- Conlet.RenderMode
- .asSet(Conlet.RenderMode.Preview, Conlet.RenderMode.View)),
+ MarkdownDisplayConlet.class.getName(),
+ RenderMode.asSet(RenderMode.Preview))
+ .addProperty(MarkdownDisplayConlet.CONLET_ID,
+ getClass().getName())
+ .addProperty(MarkdownDisplayConlet.TITLE,
+ resourceBundle.getString("consoleTitle"))
+ .addProperty(MarkdownDisplayConlet.PREVIEW_SOURCE,
+ shortDesc)
+ .addProperty(MarkdownDisplayConlet.DELETABLE, true)
+ .addProperty(MarkdownDisplayConlet.EDITABLE_BY,
+ Collections.EMPTY_SET),
connection);
}
diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ConfigMapReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ConfigMapReconciler.java
index 0c5f0cd..a882a79 100644
--- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ConfigMapReconciler.java
+++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ConfigMapReconciler.java
@@ -104,7 +104,9 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
ListOptions listOpts = new ListOptions();
listOpts.setLabelSelector(
"app.kubernetes.io/managed-by=" + VM_OP_NAME + ","
- + "app.kubernetes.io/name=" + APP_NAME);
+ + "app.kubernetes.io/name=" + APP_NAME + ","
+ + "app.kubernetes.io/instance=" + newCm.getMetadata()
+ .getLabels().get("app.kubernetes.io/instance"));
// Get pod, selected by label
var podApi = new DynamicKubernetesApi("", "v1", "pods", client);
var pods = podApi
diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Constants.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Constants.java
index a7b84a3..7de839b 100644
--- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Constants.java
+++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Constants.java
@@ -27,6 +27,12 @@ public class Constants extends org.jdrupes.vmoperator.common.Constants {
/** The Constant COMP_DISPLAY_SECRET. */
public static final String COMP_DISPLAY_SECRET = "display-secret";
+ /** The Constant DATA_DISPLAY_PASSWORD. */
+ public static final String DATA_DISPLAY_PASSWORD = "display-password";
+
+ /** The Constant DATA_PASSWORD_EXPIRY. */
+ public static final String DATA_PASSWORD_EXPIRY = "password-expiry";
+
/** The Constant STATE_RUNNING. */
public static final String STATE_RUNNING = "Running";
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 89b5eac..66c11a7 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
@@ -101,7 +101,7 @@ public class Controller extends Component {
}
});
attach(new VmMonitor(channel()).channelManager(chanMgr));
- attach(new DisplayPasswordMonitor(channel())
+ attach(new DisplaySecretMonitor(channel())
.channelManager(chanMgr.fixed()));
// Currently, we don't use the IP assigned by the load balancer
// to access the VM's console. Might change in the future.
diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplayPasswordMonitor.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplayPasswordMonitor.java
deleted file mode 100644
index 9959aec..0000000
--- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplayPasswordMonitor.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * 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;
-
-import io.kubernetes.client.openapi.ApiException;
-import io.kubernetes.client.openapi.models.V1Secret;
-import io.kubernetes.client.openapi.models.V1SecretList;
-import io.kubernetes.client.util.Watch.Response;
-import io.kubernetes.client.util.generic.options.ListOptions;
-import java.io.IOException;
-import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
-import org.jdrupes.vmoperator.common.K8sClient;
-import org.jdrupes.vmoperator.common.K8sObserver.ResponseType;
-import org.jdrupes.vmoperator.common.K8sV1SecretStub;
-import static org.jdrupes.vmoperator.manager.Constants.COMP_DISPLAY_SECRET;
-import org.jdrupes.vmoperator.manager.events.DisplayPasswordChanged;
-import org.jdrupes.vmoperator.manager.events.GetDisplayPassword;
-import org.jdrupes.vmoperator.manager.events.VmChannel;
-import org.jgrapes.core.Channel;
-import org.jgrapes.core.annotation.Handler;
-
-/**
- * Watches for changes of display secrets.
- */
-@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
-public class DisplayPasswordMonitor
- extends AbstractMonitor {
-
- /**
- * Instantiates a new display secrets monitor.
- *
- * @param componentChannel the component channel
- */
- public DisplayPasswordMonitor(Channel componentChannel) {
- super(componentChannel, V1Secret.class, V1SecretList.class);
- context(K8sV1SecretStub.CONTEXT);
- ListOptions options = new ListOptions();
- options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
- + "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET);
- options(options);
- }
-
- @Override
- protected void prepareMonitoring() throws IOException, ApiException {
- client(new K8sClient());
- }
-
- @Override
- protected void handleChange(K8sClient client, Response change) {
- String vmName = change.object.getMetadata().getLabels()
- .get("app.kubernetes.io/instance");
- if (vmName == null) {
- return;
- }
- var channel = channel(vmName).orElse(null);
- if (channel == null || channel.vmDefinition() == null) {
- return;
- }
- channel.pipeline().fire(new DisplayPasswordChanged(
- ResponseType.valueOf(change.type), change.object), channel);
- }
-
- /**
- * On get display secrets.
- *
- * @param event the event
- * @param channel the channel
- * @throws ApiException the api exception
- */
- @Handler
- @SuppressWarnings("PMD.StringInstantiation")
- public void onGetDisplaySecrets(GetDisplayPassword event, VmChannel channel)
- throws ApiException {
- ListOptions options = new ListOptions();
- options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
- + "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET + ","
- + "app.kubernetes.io/instance=" + event.vmName());
- var stubs = K8sV1SecretStub.list(client(), namespace(), options);
- if (stubs.isEmpty()) {
- return;
- }
- stubs.iterator().next().model().map(m -> m.getData())
- .map(m -> m.get("display-password"))
- .ifPresent(p -> event.setResult(new String(p)));
- }
-}
diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretMonitor.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretMonitor.java
new file mode 100644
index 0000000..8bc1db0
--- /dev/null
+++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretMonitor.java
@@ -0,0 +1,285 @@
+/*
+ * 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;
+
+import io.kubernetes.client.custom.V1Patch;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.models.V1Secret;
+import io.kubernetes.client.openapi.models.V1SecretList;
+import io.kubernetes.client.util.Watch.Response;
+import io.kubernetes.client.util.generic.options.ListOptions;
+import io.kubernetes.client.util.generic.options.PatchOptions;
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Scanner;
+import java.util.logging.Level;
+import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
+import static org.jdrupes.vmoperator.common.Constants.VM_OP_NAME;
+import org.jdrupes.vmoperator.common.K8sClient;
+import org.jdrupes.vmoperator.common.K8sV1PodStub;
+import org.jdrupes.vmoperator.common.K8sV1SecretStub;
+import static org.jdrupes.vmoperator.manager.Constants.COMP_DISPLAY_SECRET;
+import static org.jdrupes.vmoperator.manager.Constants.DATA_DISPLAY_PASSWORD;
+import static org.jdrupes.vmoperator.manager.Constants.DATA_PASSWORD_EXPIRY;
+import org.jdrupes.vmoperator.manager.events.GetDisplayPassword;
+import org.jdrupes.vmoperator.manager.events.VmChannel;
+import org.jdrupes.vmoperator.manager.events.VmDefChanged;
+import org.jgrapes.core.Channel;
+import org.jgrapes.core.CompletionLock;
+import org.jgrapes.core.Event;
+import org.jgrapes.core.annotation.Handler;
+import org.jgrapes.util.events.ConfigurationUpdate;
+import org.jose4j.base64url.Base64;
+
+/**
+ * Watches for changes of display secrets.
+ */
+@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.TooManyStaticImports" })
+public class DisplaySecretMonitor
+ extends AbstractMonitor {
+
+ private int passwordValidity = 10;
+ private final List pendingGets
+ = Collections.synchronizedList(new LinkedList<>());
+
+ /**
+ * Instantiates a new display secrets monitor.
+ *
+ * @param componentChannel the component channel
+ */
+ public DisplaySecretMonitor(Channel componentChannel) {
+ super(componentChannel, V1Secret.class, V1SecretList.class);
+ context(K8sV1SecretStub.CONTEXT);
+ ListOptions options = new ListOptions();
+ options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
+ + "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET);
+ options(options);
+ }
+
+ /**
+ * On configuration update.
+ *
+ * @param event the event
+ */
+ @Handler
+ @Override
+ public void onConfigurationUpdate(ConfigurationUpdate event) {
+ super.onConfigurationUpdate(event);
+ event.structured(componentPath()).ifPresent(c -> {
+ try {
+ if (c.containsKey("passwordValidity")) {
+ passwordValidity = Integer
+ .parseInt((String) c.get("passwordValidity"));
+ }
+ } catch (ClassCastException e) {
+ logger.config("Malformed configuration: " + e.getMessage());
+ }
+ });
+ }
+
+ @Override
+ protected void prepareMonitoring() throws IOException, ApiException {
+ client(new K8sClient());
+ }
+
+ @Override
+ protected void handleChange(K8sClient client, Response change) {
+ String vmName = change.object.getMetadata().getLabels()
+ .get("app.kubernetes.io/instance");
+ if (vmName == null) {
+ return;
+ }
+ var channel = channel(vmName).orElse(null);
+ if (channel == null || channel.vmDefinition() == null) {
+ return;
+ }
+
+ try {
+ patchPod(client, change);
+ } catch (ApiException e) {
+ logger.log(Level.WARNING, e,
+ () -> "Cannot patch pod annotations: " + e.getMessage());
+ }
+ }
+
+ private void patchPod(K8sClient client, Response change)
+ throws ApiException {
+ // Force update for pod
+ ListOptions listOpts = new ListOptions();
+ listOpts.setLabelSelector(
+ "app.kubernetes.io/managed-by=" + VM_OP_NAME + ","
+ + "app.kubernetes.io/name=" + APP_NAME + ","
+ + "app.kubernetes.io/instance=" + change.object.getMetadata()
+ .getLabels().get("app.kubernetes.io/instance"));
+ // Get pod, selected by label
+ var pods = K8sV1PodStub.list(client, namespace(), listOpts);
+
+ // If the VM is being created, the pod may not exist yet.
+ if (pods.isEmpty()) {
+ return;
+ }
+ var pod = pods.iterator().next();
+
+ // Patch pod annotation
+ PatchOptions patchOpts = new PatchOptions();
+ patchOpts.setFieldManager("kubernetes-java-kubectl-apply");
+ pod.patch(V1Patch.PATCH_FORMAT_JSON_PATCH,
+ new V1Patch("[{\"op\": \"replace\", \"path\": "
+ + "\"/metadata/annotations/vmrunner.jdrupes.org~1dpVersion\", "
+ + "\"value\": \""
+ + change.object.getMetadata().getResourceVersion()
+ + "\"}]"),
+ patchOpts);
+ }
+
+ /**
+ * On get display secrets.
+ *
+ * @param event the event
+ * @param channel the channel
+ * @throws ApiException the api exception
+ */
+ @Handler
+ @SuppressWarnings("PMD.StringInstantiation")
+ public void onGetDisplaySecrets(GetDisplayPassword event, VmChannel channel)
+ throws ApiException {
+ ListOptions options = new ListOptions();
+ options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
+ + "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET + ","
+ + "app.kubernetes.io/instance="
+ + event.vmDefinition().metadata().getName());
+ var stubs = K8sV1SecretStub.list(client(),
+ event.vmDefinition().metadata().getNamespace(), options);
+ if (stubs.isEmpty()) {
+ return;
+ }
+ var stub = stubs.iterator().next();
+
+ // Check validity
+ var model = stub.model().get();
+ @SuppressWarnings("PMD.StringInstantiation")
+ var expiry = new String(model.getData().get(DATA_PASSWORD_EXPIRY));
+ if (model.getData().get(DATA_DISPLAY_PASSWORD) != null
+ && stillValid(expiry)) {
+ event.setResult(
+ new String(model.getData().get(DATA_DISPLAY_PASSWORD)));
+ return;
+ }
+ updatePassword(stub, event);
+ }
+
+ @SuppressWarnings("PMD.StringInstantiation")
+ private void updatePassword(K8sV1SecretStub stub, GetDisplayPassword event)
+ throws ApiException {
+ SecureRandom random = null;
+ try {
+ random = SecureRandom.getInstanceStrong();
+ } catch (NoSuchAlgorithmException e) { // NOPMD
+ // "Every implementation of the Java platform is required
+ // to support at least one strong SecureRandom implementation."
+ }
+ byte[] bytes = new byte[16];
+ random.nextBytes(bytes);
+ var password = Base64.encode(bytes);
+ var model = stub.model().get();
+ model.setStringData(Map.of(DATA_DISPLAY_PASSWORD, password,
+ DATA_PASSWORD_EXPIRY,
+ Long.toString(Instant.now().getEpochSecond() + passwordValidity)));
+ event.setResult(password);
+
+ // Prepare wait for confirmation (by VM status change)
+ var pending = new PendingGet(event,
+ event.vmDefinition().displayPasswordSerial().orElse(0L) + 1,
+ new CompletionLock(event, 1500));
+ pendingGets.add(pending);
+ Event.onCompletion(event, e -> {
+ pendingGets.remove(pending);
+ });
+
+ // Update, will (eventually) trigger confirmation
+ stub.update(model).getObject();
+ }
+
+ private boolean stillValid(String expiry) {
+ if (expiry == null || "never".equals(expiry)) {
+ return true;
+ }
+ @SuppressWarnings({ "PMD.CloseResource", "resource" })
+ var scanner = new Scanner(expiry);
+ if (!scanner.hasNextLong()) {
+ return false;
+ }
+ long expTime = scanner.nextLong();
+ return expTime > Instant.now().getEpochSecond() + passwordValidity;
+ }
+
+ /**
+ * On vm def changed.
+ *
+ * @param event the event
+ * @param channel the channel
+ */
+ @Handler
+ public void onVmDefChanged(VmDefChanged event, Channel channel) {
+ synchronized (pendingGets) {
+ String vmName = event.vmDefinition().metadata().getName();
+ for (var pending : pendingGets) {
+ if (pending.event.vmDefinition().metadata().getName()
+ .equals(vmName)
+ && event.vmDefinition().displayPasswordSerial()
+ .map(s -> s >= pending.expectedSerial).orElse(false)) {
+ pending.lock.remove();
+ // pending will be removed from pendingGest by
+ // waiting thread, see updatePassword
+ continue;
+ }
+ }
+ }
+ }
+
+ /**
+ * The Class PendingGet.
+ */
+ @SuppressWarnings("PMD.DataClass")
+ private static class PendingGet {
+ public final GetDisplayPassword event;
+ public final long expectedSerial;
+ public final CompletionLock lock;
+
+ /**
+ * Instantiates a new pending get.
+ *
+ * @param event the event
+ * @param expectedSerial the expected serial
+ */
+ public PendingGet(GetDisplayPassword event, long expectedSerial,
+ CompletionLock lock) {
+ super();
+ this.event = event;
+ this.expectedSerial = expectedSerial;
+ this.lock = lock;
+ }
+ }
+}
diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretReconciler.java
new file mode 100644
index 0000000..14b8890
--- /dev/null
+++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretReconciler.java
@@ -0,0 +1,106 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2023 Michael N. Lipp
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.jdrupes.vmoperator.manager;
+
+import com.google.gson.JsonPrimitive;
+import freemarker.template.TemplateException;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.models.V1ObjectMeta;
+import io.kubernetes.client.openapi.models.V1Secret;
+import io.kubernetes.client.util.generic.options.ListOptions;
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Map;
+import java.util.logging.Logger;
+import org.jdrupes.vmoperator.common.K8sV1SecretStub;
+import static org.jdrupes.vmoperator.manager.Constants.APP_NAME;
+import static org.jdrupes.vmoperator.manager.Constants.COMP_DISPLAY_SECRET;
+import static org.jdrupes.vmoperator.manager.Constants.DATA_DISPLAY_PASSWORD;
+import static org.jdrupes.vmoperator.manager.Constants.DATA_PASSWORD_EXPIRY;
+import org.jdrupes.vmoperator.manager.events.VmChannel;
+import org.jdrupes.vmoperator.manager.events.VmDefChanged;
+import org.jdrupes.vmoperator.util.GsonPtr;
+import org.jose4j.base64url.Base64;
+
+/**
+ * Delegee for reconciling the display secret
+ */
+@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
+/* default */ class DisplaySecretReconciler {
+
+ protected final Logger logger = Logger.getLogger(getClass().getName());
+
+ /**
+ * Reconcile.
+ *
+ * @param event the event
+ * @param model the model
+ * @param channel the channel
+ * @throws IOException Signals that an I/O exception has occurred.
+ * @throws TemplateException the template exception
+ * @throws ApiException the api exception
+ */
+ public void reconcile(VmDefChanged event,
+ Map model, VmChannel channel)
+ throws IOException, TemplateException, ApiException {
+ // Secret needed at all?
+ var display = GsonPtr.to(event.vmDefinition().data()).to("spec", "vm",
+ "display");
+ if (!display.get(JsonPrimitive.class, "spice", "generateSecret")
+ .map(JsonPrimitive::getAsBoolean).orElse(false)) {
+ return;
+ }
+
+ // Check if exists
+ var metadata = event.vmDefinition().getMetadata();
+ ListOptions options = new ListOptions();
+ options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
+ + "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET + ","
+ + "app.kubernetes.io/instance=" + metadata.getName());
+ var stubs = K8sV1SecretStub.list(channel.client(),
+ metadata.getNamespace(), options);
+ if (!stubs.isEmpty()) {
+ return;
+ }
+
+ // Create secret
+ var secret = new V1Secret();
+ secret.setMetadata(new V1ObjectMeta().namespace(metadata.getNamespace())
+ .name(metadata.getName() + "-" + COMP_DISPLAY_SECRET)
+ .putLabelsItem("app.kubernetes.io/name", APP_NAME)
+ .putLabelsItem("app.kubernetes.io/component", COMP_DISPLAY_SECRET)
+ .putLabelsItem("app.kubernetes.io/instance", metadata.getName()));
+ secret.setType("Opaque");
+ SecureRandom random = null;
+ try {
+ random = SecureRandom.getInstanceStrong();
+ } catch (NoSuchAlgorithmException e) { // NOPMD
+ // "Every implementation of the Java platform is required
+ // to support at least one strong SecureRandom implementation."
+ }
+ byte[] bytes = new byte[16];
+ random.nextBytes(bytes);
+ var password = Base64.encode(bytes);
+ secret.setStringData(Map.of(DATA_DISPLAY_PASSWORD, password,
+ DATA_PASSWORD_EXPIRY, "now"));
+ K8sV1SecretStub.create(channel.client(), secret);
+ }
+
+}
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 5ba9dc5..5bbfe38 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
@@ -135,6 +135,7 @@ public class Reconciler extends Component {
@SuppressWarnings("PMD.SingularField")
private final Configuration fmConfig;
private final ConfigMapReconciler cmReconciler;
+ private final DisplaySecretReconciler dsReconciler;
private final StatefulSetReconciler stsReconciler;
private final LoadBalancerReconciler lbReconciler;
@SuppressWarnings("PMD.UseConcurrentHashMap")
@@ -159,6 +160,7 @@ public class Reconciler extends Component {
fmConfig.setClassForTemplateLoading(Reconciler.class, "");
cmReconciler = new ConfigMapReconciler(fmConfig);
+ dsReconciler = new DisplaySecretReconciler();
stsReconciler = new StatefulSetReconciler(fmConfig);
lbReconciler = new LoadBalancerReconciler(fmConfig);
}
@@ -209,6 +211,7 @@ public class Reconciler extends Component {
= prepareModel(channel.client(), patchCr(event.vmDefinition()));
var configMap = cmReconciler.reconcile(event, model, channel);
model.put("cm", configMap.getRaw());
+ dsReconciler.reconcile(event, model, channel);
stsReconciler.reconcile(event, model, channel);
lbReconciler.reconcile(event, model, channel);
}
diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmMonitor.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmMonitor.java
index 7027808..41f08ce 100644
--- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmMonitor.java
+++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmMonitor.java
@@ -32,12 +32,14 @@ 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.K8sDynamicModels;
import org.jdrupes.vmoperator.common.K8sDynamicStub;
import org.jdrupes.vmoperator.common.K8sObserver.ResponseType;
import org.jdrupes.vmoperator.common.K8sV1ConfigMapStub;
import org.jdrupes.vmoperator.common.K8sV1PodStub;
import org.jdrupes.vmoperator.common.K8sV1StatefulSetStub;
+import org.jdrupes.vmoperator.common.VmDefinitionModel;
+import org.jdrupes.vmoperator.common.VmDefinitionModels;
+import org.jdrupes.vmoperator.common.VmDefinitionStub;
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;
@@ -50,8 +52,8 @@ import org.jgrapes.core.Channel;
* Watches for changes of VM definitions.
*/
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports" })
-public class VmMonitor
- extends AbstractMonitor {
+public class VmMonitor extends
+ AbstractMonitor {
/**
* Instantiates a new VM definition watcher.
@@ -59,7 +61,8 @@ public class VmMonitor
* @param componentChannel the component channel
*/
public VmMonitor(Channel componentChannel) {
- super(componentChannel, K8sDynamicModel.class, K8sDynamicModels.class);
+ super(componentChannel, VmDefinitionModel.class,
+ VmDefinitionModels.class);
}
@Override
@@ -102,7 +105,7 @@ public class VmMonitor
@Override
protected void handleChange(K8sClient client,
- Watch.Response response) {
+ Watch.Response response) {
V1ObjectMeta metadata = response.object.getMetadata();
VmChannel channel = channel(metadata.getName()).orElse(null);
if (channel == null) {
@@ -138,9 +141,10 @@ public class VmMonitor
vmDef), channel);
}
- private K8sDynamicModel getModel(K8sClient client, K8sDynamicModel vmDef) {
+ private VmDefinitionModel getModel(K8sClient client,
+ VmDefinitionModel vmDef) {
try {
- return K8sDynamicStub.get(client, context(), namespace(),
+ return VmDefinitionStub.get(client, context(), namespace(),
vmDef.metadata().getName()).model().orElse(null);
} catch (ApiException e) {
return null;
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 06ed64c..bbcba5e 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
@@ -40,7 +40,8 @@ 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.common.VmDefinitionModel;
+import org.jdrupes.vmoperator.common.VmDefinitionStub;
import org.jdrupes.vmoperator.runner.qemu.events.BalloonChangeEvent;
import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu;
import org.jdrupes.vmoperator.runner.qemu.events.DisplayPasswordChanged;
@@ -73,7 +74,7 @@ public class StatusUpdater extends Component {
private long observedGeneration;
private boolean guestShutdownStops;
private boolean shutdownByGuest;
- private K8sDynamicStub vmStub;
+ private VmDefinitionStub vmStub;
/**
* Instantiates a new status updater.
@@ -158,7 +159,7 @@ public class StatusUpdater extends Component {
return;
}
try {
- vmStub = K8sDynamicStub.get(apiClient,
+ vmStub = VmDefinitionStub.get(apiClient,
new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM),
namespace, vmName);
vmStub.model().ifPresent(model -> {
@@ -226,7 +227,7 @@ public class StatusUpdater extends Component {
"PMD.AvoidLiteralsInIfCondition" })
public void onRunnerStateChanged(RunnerStateChange event)
throws ApiException {
- K8sDynamicModel vmDef;
+ VmDefinitionModel vmDef;
if (vmStub == null || (vmDef = vmStub.model().orElse(null)) == null) {
return;
}
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 1882173..a8bb1ae 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
@@ -35,8 +35,8 @@ import java.util.Optional;
import java.util.Set;
import org.jdrupes.json.JsonBeanDecoder;
import org.jdrupes.json.JsonDecodeException;
-import org.jdrupes.vmoperator.common.K8sDynamicModel;
import org.jdrupes.vmoperator.common.K8sObserver;
+import org.jdrupes.vmoperator.common.VmDefinitionModel;
import org.jdrupes.vmoperator.manager.events.ChannelCache;
import org.jdrupes.vmoperator.manager.events.ModifyVm;
import org.jdrupes.vmoperator.manager.events.VmChannel;
@@ -69,7 +69,7 @@ public class VmConlet extends FreeMarkerConlet {
private static final Set MODES = RenderMode.asSet(
RenderMode.Preview, RenderMode.View);
private final ChannelCache channelManager = new ChannelCache<>();
+ VmDefinitionModel> channelManager = new ChannelCache<>();
private final TimeSeries summarySeries = new TimeSeries(Duration.ofDays(1));
private Summary cachedSummary;
@@ -196,8 +196,8 @@ public class VmConlet extends FreeMarkerConlet {
}
}
} else {
- var vmDef = new K8sDynamicModel(channel.client().getJSON()
- .getGson(), convertQuantities(event.vmDefinition().data()));
+ var vmDef = new VmDefinitionModel(channel.client().getJSON()
+ .getGson(), cleanup(event.vmDefinition().data()));
channelManager.put(vmName, channel, vmDef);
var def = JsonBeanDecoder.create(vmDef.data().toString())
.readObject();
@@ -220,7 +220,7 @@ public class VmConlet extends FreeMarkerConlet {
}
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
- private JsonObject convertQuantities(JsonObject vmDef) {
+ private JsonObject cleanup(JsonObject vmDef) {
// Clone and remove managed fields
var json = vmDef.deepCopy();
GsonPtr.to(json).to("metadata").get(JsonObject.class)
diff --git a/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/VmConlet-style.scss b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/VmConlet-style.scss
index 3649bff..a5658e9 100644
--- a/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/VmConlet-style.scss
+++ b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/VmConlet-style.scss
@@ -100,7 +100,7 @@
[role=button] {
padding: 0.25rem;
- &:not([aria-disabled]):hover {
+ &:not([aria-disabled]):hover, &[aria-disabled='false']:hover {
box-shadow: var(--darkening);
}
}
diff --git a/org.jdrupes.vmoperator.vmviewer/resources/org/jdrupes/vmoperator/vmviewer/l10n_de.properties b/org.jdrupes.vmoperator.vmviewer/resources/org/jdrupes/vmoperator/vmviewer/l10n_de.properties
index e81a0fe..f05dfff 100644
--- a/org.jdrupes.vmoperator.vmviewer/resources/org/jdrupes/vmoperator/vmviewer/l10n_de.properties
+++ b/org.jdrupes.vmoperator.vmviewer/resources/org/jdrupes/vmoperator/vmviewer/l10n_de.properties
@@ -3,5 +3,6 @@ conletName = VM-Konsole
okayLabel = Anwenden und Schließen
Select\ VM = VM auswählen
-Start\ VM = VM Starten
-Stop\ VM = VM Anhalten
+Start\ VM = VM starten
+Stop\ VM = VM anhalten
+Open\ console = Konsole anzeigen
diff --git a/org.jdrupes.vmoperator.vmviewer/src/org/jdrupes/vmoperator/vmviewer/VmViewer.java b/org.jdrupes.vmoperator.vmviewer/src/org/jdrupes/vmoperator/vmviewer/VmViewer.java
index b97ff6f..c06cc1a 100644
--- a/org.jdrupes.vmoperator.vmviewer/src/org/jdrupes/vmoperator/vmviewer/VmViewer.java
+++ b/org.jdrupes.vmoperator.vmviewer/src/org/jdrupes/vmoperator/vmviewer/VmViewer.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
@@ -22,6 +22,7 @@ import com.fasterxml.jackson.annotation.JsonGetter;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import freemarker.core.ParseException;
import freemarker.template.MalformedTemplateNameException;
@@ -33,18 +34,23 @@ import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
+import java.time.Duration;
import java.util.Base64;
import java.util.Collections;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.logging.Level;
+import org.bouncycastle.util.Objects;
import org.jdrupes.json.JsonBeanDecoder;
import org.jdrupes.json.JsonDecodeException;
import org.jdrupes.vmoperator.common.K8sDynamicModel;
import org.jdrupes.vmoperator.common.K8sObserver;
+import org.jdrupes.vmoperator.common.VmDefinitionModel;
+import org.jdrupes.vmoperator.common.VmDefinitionModel.Permission;
import org.jdrupes.vmoperator.manager.events.ChannelCache;
import org.jdrupes.vmoperator.manager.events.GetDisplayPassword;
import org.jdrupes.vmoperator.manager.events.ModifyVm;
@@ -52,6 +58,7 @@ import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jdrupes.vmoperator.util.GsonPtr;
import org.jgrapes.core.Channel;
+import org.jgrapes.core.Components;
import org.jgrapes.core.Event;
import org.jgrapes.core.Manager;
import org.jgrapes.core.annotation.Handler;
@@ -62,11 +69,15 @@ import org.jgrapes.util.events.KeyValueStoreUpdate;
import org.jgrapes.webconsole.base.Conlet.RenderMode;
import org.jgrapes.webconsole.base.ConletBaseModel;
import org.jgrapes.webconsole.base.ConsoleConnection;
+import org.jgrapes.webconsole.base.ConsoleRole;
import org.jgrapes.webconsole.base.ConsoleUser;
import org.jgrapes.webconsole.base.WebConsoleUtils;
+import org.jgrapes.webconsole.base.events.AddConletRequest;
import org.jgrapes.webconsole.base.events.AddConletType;
import org.jgrapes.webconsole.base.events.AddPageResources.ScriptResource;
import org.jgrapes.webconsole.base.events.ConletDeleted;
+import org.jgrapes.webconsole.base.events.ConsoleConfigured;
+import org.jgrapes.webconsole.base.events.ConsolePrepared;
import org.jgrapes.webconsole.base.events.ConsoleReady;
import org.jgrapes.webconsole.base.events.DeleteConlet;
import org.jgrapes.webconsole.base.events.NotifyConletModel;
@@ -75,22 +86,32 @@ import org.jgrapes.webconsole.base.events.OpenModalDialog;
import org.jgrapes.webconsole.base.events.RenderConlet;
import org.jgrapes.webconsole.base.events.RenderConletRequestBase;
import org.jgrapes.webconsole.base.events.SetLocale;
+import org.jgrapes.webconsole.base.events.UpdateConletType;
import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet;
/**
* The Class VmConlet.
*/
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports",
- "PMD.CouplingBetweenObjects" })
+ "PMD.CouplingBetweenObjects", "PMD.GodClass" })
public class VmViewer extends FreeMarkerConlet {
+ private static final String VM_NAME_PROPERTY = "vmName";
+ private static final String RENDERED
+ = VmViewer.class.getName() + ".rendered";
+ private static final String PENDING
+ = VmViewer.class.getName() + ".pending";
private static final Set MODES = RenderMode.asSet(
RenderMode.Preview, RenderMode.Edit);
+ private static final Set MODES_FOR_GENERATED = RenderMode.asSet(
+ RenderMode.Preview, RenderMode.StickyPreview);
private final ChannelCache channelManager = new ChannelCache<>();
+ VmDefinitionModel> channelManager = new ChannelCache<>();
private static ObjectMapper objectMapper
= new ObjectMapper().registerModule(new JavaTimeModule());
private Class> preferredIpVersion = Inet4Address.class;
+ private final Set syncUsers = new HashSet<>();
+ private final Set syncRoles = new HashSet<>();
/**
* The periodically generated update event.
@@ -114,24 +135,47 @@ public class VmViewer extends FreeMarkerConlet {
*
* @param event the event
*/
+ @SuppressWarnings("unchecked")
@Handler
public void onConfigurationUpdate(ConfigurationUpdate event) {
event.structured(componentPath()).ifPresent(c -> {
- @SuppressWarnings("unchecked")
- var dispRes = (Map) c
- .getOrDefault("displayResource", Collections.emptyMap());
- switch ((String) dispRes.getOrDefault("preferredIpVersion", "")) {
- case "ipv6":
- preferredIpVersion = Inet6Address.class;
- break;
- case "ipv4":
- default:
- preferredIpVersion = Inet4Address.class;
- break;
+ try {
+ var dispRes = (Map) c
+ .getOrDefault("displayResource", Collections.emptyMap());
+ switch ((String) dispRes.getOrDefault("preferredIpVersion",
+ "")) {
+ case "ipv6":
+ preferredIpVersion = Inet6Address.class;
+ break;
+ case "ipv4":
+ default:
+ preferredIpVersion = Inet4Address.class;
+ break;
+ }
+
+ // Sync
+ for (var entry : (List