Support for display secrets (#21)
Some checks failed
Java CI with Gradle / build (push) Has been cancelled

This commit is contained in:
Michael N. Lipp 2024-03-20 11:03:09 +01:00 committed by GitHub
parent 85b0a160f3
commit 3103452170
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 2081 additions and 658 deletions

View file

@ -0,0 +1,12 @@
kind: Secret
apiVersion: v1
metadata:
name: test-vm-display-secret
namespace: vmop-dev
labels:
app.kubernetes.io/name: vm-runner
app.kubernetes.io/instance: test-vm
app.kubernetes.io/component: display-secret
type: Opaque
data:
display-password: dGVzdC12bQ==

View file

@ -26,6 +26,9 @@ public class Constants {
/** The Constant APP_NAME. */
public static final String APP_NAME = "vm-runner";
/** The Constant COMP_DISPLAY_SECRETS. */
public static final String COMP_DISPLAY_SECRET = "display-secret";
/** The Constant VM_OP_NAME. */
public static final String VM_OP_NAME = "vm-operator";

View file

@ -44,6 +44,7 @@ import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;
// TODO: Auto-generated Javadoc
/**
* Helpers for K8s API.
*/
@ -74,6 +75,35 @@ public class K8s {
return Optional.empty();
}
/**
* Returns a new context with the given version as preferred version.
*
* @param context the context
* @param version the version
* @return the API resource
*/
public static APIResource preferred(APIResource context, String version) {
assert context.getVersions().contains(version);
return new APIResource(context.getGroup(),
context.getVersions(), version, context.getKind(),
context.getNamespaced(), context.getResourcePlural(),
context.getResourceSingular());
}
/**
* Return a string representation of the context (API resource).
*
* @param context the context
* @return the string
*/
@SuppressWarnings("PMD.UseLocaleWithCaseConversions")
public static String toString(APIResource context) {
return (Strings.isNullOrEmpty(context.getGroup()) ? ""
: context.getGroup() + "/")
+ context.getPreferredVersion().toUpperCase()
+ context.getKind();
}
/**
* Convert Yaml to Json.
*
@ -156,6 +186,7 @@ public class K8s {
* @param api the api
* @param existing the existing
* @param update the update
* @return the t
* @throws ApiException the api exception
*/
public static <T extends KubernetesObject, LT extends KubernetesListObject>
@ -199,8 +230,10 @@ public class K8s {
* * If `type` is not set, set it to "Normal"
* * If `regarding` is not set, set it to the given object.
*
* @param client the client
* @param object the object
* @param event the event
* @throws ApiException
* @throws ApiException the api exception
*/
@SuppressWarnings("PMD.NPathComplexity")
public static void createEvent(ApiClient client,

View file

@ -18,10 +18,14 @@
package org.jdrupes.vmoperator.common;
import com.google.gson.Gson;
import io.kubernetes.client.Discovery.APIResource;
import io.kubernetes.client.apimachinery.GroupVersionKind;
import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.util.generic.options.ListOptions;
import java.io.Reader;
import java.util.Collection;
/**
* A stub for namespaced custom objects. It uses a dynamic model
@ -47,6 +51,24 @@ public class K8sDynamicStub
Class<K8sDynamicModels> objectListClass, K8sClient client,
APIResource context, String namespace, String name) {
super(objectClass, objectListClass, client, context, namespace, name);
// Make sure that we have an adapter for our type
Gson gson = client.getJSON().getGson();
if (!checkAdapters(client)) {
client.getJSON().setGson(gson.newBuilder()
.registerTypeAdapterFactory(
new K8sDynamicModelTypeAdapterFactory())
.create());
}
}
private boolean checkAdapters(ApiClient client) {
return K8sDynamicModelTypeAdapterFactory.K8sDynamicModelCreator.class
.equals(client.getJSON().getGson().getAdapter(K8sDynamicModel.class)
.getClass())
&& K8sDynamicModelTypeAdapterFactory.K8sDynamicModelsCreator.class
.equals(client.getJSON().getGson()
.getAdapter(K8sDynamicModels.class).getClass());
}
/**
@ -83,8 +105,7 @@ public class K8sDynamicStub
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
public static K8sDynamicStub get(K8sClient client,
APIResource context, String namespace, String name)
throws ApiException {
APIResource context, String namespace, String name) {
return K8sGenericStub.get(K8sDynamicModel.class, K8sDynamicModels.class,
client, context, namespace, name, K8sDynamicStub::new);
}
@ -106,4 +127,37 @@ public class K8sDynamicStub
K8sDynamicModels.class, client, context, model,
K8sDynamicStub::new);
}
/**
* Get the stubs for the objects in the given namespace that match
* the criteria from the given options.
*
* @param client the client
* @param namespace the namespace
* @param options the options
* @return the collection
* @throws ApiException the api exception
*/
public static Collection<K8sDynamicStub> list(K8sClient client,
APIResource context, String namespace, ListOptions options)
throws ApiException {
return K8sGenericStub.list(K8sDynamicModel.class,
K8sDynamicModels.class, client, context, namespace, options,
K8sDynamicStub::new);
}
/**
* Get the stubs for the objects in the given namespace.
*
* @param client the client
* @param namespace the namespace
* @return the collection
* @throws ApiException the api exception
*/
public static Collection<K8sDynamicStub> list(K8sClient client,
APIResource context, String namespace)
throws ApiException {
return list(client, context, namespace, new ListOptions());
}
}

View file

@ -18,21 +18,22 @@
package org.jdrupes.vmoperator.common;
import com.google.gson.Gson;
import io.kubernetes.client.Discovery.APIResource;
import io.kubernetes.client.apimachinery.GroupVersionKind;
import io.kubernetes.client.common.KubernetesListObject;
import io.kubernetes.client.common.KubernetesObject;
import io.kubernetes.client.custom.V1Patch;
import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.util.Strings;
import io.kubernetes.client.util.generic.GenericKubernetesApi;
import io.kubernetes.client.util.generic.options.GetOptions;
import io.kubernetes.client.util.generic.options.ListOptions;
import io.kubernetes.client.util.generic.options.PatchOptions;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
@ -49,145 +50,16 @@ public class K8sGenericStub<O extends KubernetesObject,
L extends KubernetesListObject> {
protected final K8sClient client;
private final GenericKubernetesApi<O, L> api;
protected final String group;
protected final String version;
protected final String kind;
protected final String plural;
protected final APIResource context;
protected final String namespace;
protected final String name;
/**
* Get a namespaced object stub. If the version in parameter
* `gvk` is an empty string, the stub refers to the first object
* found with matching group and kind.
*
* @param <O> the object type
* @param <L> the object list type
* @param <R> the stub type
* @param objectClass the object class
* @param objectListClass the object list class
* @param client the client
* @param gvk the group, version and kind
* @param namespace the namespace
* @param name the name
* @param provider the provider
* @return the stub if the object exists
* @throws ApiException the api exception
*/
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
"PMD.AvoidInstantiatingObjectsInLoops" })
public static <O extends KubernetesObject, L extends KubernetesListObject,
R extends K8sGenericStub<O, L>>
R get(Class<O> objectClass, Class<L> objectListClass,
K8sClient client, GroupVersionKind gvk, String namespace,
String name, GenericSupplier<O, L, R> 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 <O> the object type
* @param <L> the object list type
* @param <R> the stub type
* @param objectClass the object class
* @param objectListClass the object list class
* @param client the client
* @param context the context
* @param namespace the namespace
* @param name the name
* @param provider the provider
* @return the stub if the object exists
* @throws ApiException the api exception
*/
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
public static <O extends KubernetesObject, L extends KubernetesListObject,
R extends K8sGenericStub<O, L>>
R get(Class<O> objectClass, Class<L> objectListClass,
K8sClient client, APIResource context, String namespace,
String name, GenericSupplier<O, L, R> provider)
throws ApiException {
return provider.get(objectClass, objectListClass, client,
context, namespace, name);
}
/**
* Get a namespaced object stub for a newly created object.
*
* @param <O> the object type
* @param <L> the object list type
* @param <R> the stub type
* @param objectClass the object class
* @param objectListClass the object list class
* @param client the client
* @param context the context
* @param model the model
* @param provider the provider
* @return the stub if the object exists
* @throws ApiException the api exception
*/
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
public static <O extends KubernetesObject, L extends KubernetesListObject,
R extends K8sGenericStub<O, L>>
R create(Class<O> objectClass, Class<L> objectListClass,
K8sClient client, APIResource context, O model,
GenericSupplier<O, L, R> provider) throws ApiException {
var api = new GenericKubernetesApi<>(objectClass, objectListClass,
context.getGroup(), context.getPreferredVersion(),
context.getResourcePlural(), client);
api.create(model).throwsApiException();
return provider.get(objectClass, objectListClass, client,
context, model.getMetadata().getNamespace(),
model.getMetadata().getName());
}
/**
* Get the stubs for the objects in the given namespace that match
* the criteria from the given options.
*
* @param <O> the object type
* @param <L> the object list type
* @param <R> the stub type
* @param objectClass the object class
* @param objectListClass the object list class
* @param client the client
* @param context the context
* @param namespace the namespace
* @param options the options
* @param provider the provider
* @return the collection
* @throws ApiException the api exception
*/
public static <O extends KubernetesObject, L extends KubernetesListObject,
R extends K8sGenericStub<O, L>>
Collection<R> list(Class<O> objectClass, Class<L> objectListClass,
K8sClient client, APIResource context, String namespace,
ListOptions options, SpecificSupplier<O, L, R> provider)
throws ApiException {
var api = new GenericKubernetesApi<>(objectClass, objectListClass,
context.getGroup(), context.getPreferredVersion(),
context.getResourcePlural(), client);
var objs = api.list(namespace, options).throwsApiException();
var result = new ArrayList<R>();
for (var item : objs.getObject().getItems()) {
result.add(
provider.get(client, namespace, item.getMetadata().getName()));
}
return result;
}
/**
* Instantiates a new namespaced custom object stub.
* Instantiates a new stub for the object specified. If the object
* exists in the context specified, the version (see
* {@link #version()} is bound to the existing object's version.
* Else the stub is dangling with the version set to the context's
* preferred version.
*
* @param objectClass the object class
* @param objectListClass the object list class
@ -196,35 +68,47 @@ public class K8sGenericStub<O extends KubernetesObject,
* @param namespace the namespace
* @param name the name
*/
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
protected K8sGenericStub(Class<O> objectClass, Class<L> objectListClass,
K8sClient client, APIResource context, String namespace,
String name) {
this.client = client;
group = context.getGroup();
version = context.getPreferredVersion();
kind = context.getKind();
plural = context.getResourcePlural();
this.namespace = namespace;
this.name = name;
Gson gson = client.getJSON().getGson();
if (!checkAdapters(client)) {
client.getJSON().setGson(gson.newBuilder()
.registerTypeAdapterFactory(
new K8sDynamicModelTypeAdapterFactory())
.create());
// Bind version
var foundVersion = context.getPreferredVersion();
GenericKubernetesApi<O, L> testApi = null;
GetOptions mdOpts
= new GetOptions().isPartialObjectMetadataRequest(true);
for (var version : candidateVersions(context)) {
testApi = new GenericKubernetesApi<>(objectClass, objectListClass,
context.getGroup(), version, context.getResourcePlural(),
client);
if (testApi.get(namespace, name, mdOpts)
.isSuccess()) {
foundVersion = version;
break;
}
}
api = new GenericKubernetesApi<>(objectClass,
objectListClass, group, version, plural, client);
if (foundVersion.equals(context.getPreferredVersion())) {
this.context = context;
} else {
this.context = K8s.preferred(context, foundVersion);
}
api = Optional.ofNullable(testApi)
.orElseGet(() -> new GenericKubernetesApi<>(objectClass,
objectListClass, group(), version(), plural(), client));
}
private boolean checkAdapters(ApiClient client) {
return K8sDynamicModelTypeAdapterFactory.K8sDynamicModelCreator.class
.equals(client.getJSON().getGson().getAdapter(K8sDynamicModel.class)
.getClass())
&& K8sDynamicModelTypeAdapterFactory.K8sDynamicModelsCreator.class
.equals(client.getJSON().getGson()
.getAdapter(K8sDynamicModels.class).getClass());
/**
* Gets the context.
*
* @return the context
*/
public APIResource context() {
return context;
}
/**
@ -233,7 +117,7 @@ public class K8sGenericStub<O extends KubernetesObject,
* @return the group
*/
public String group() {
return group;
return context.getGroup();
}
/**
@ -242,7 +126,7 @@ public class K8sGenericStub<O extends KubernetesObject,
* @return the version
*/
public String version() {
return version;
return context.getPreferredVersion();
}
/**
@ -251,7 +135,7 @@ public class K8sGenericStub<O extends KubernetesObject,
* @return the kind
*/
public String kind() {
return kind;
return context.getKind();
}
/**
@ -260,7 +144,7 @@ public class K8sGenericStub<O extends KubernetesObject,
* @return the plural
*/
public String plural() {
return plural;
return context.getResourcePlural();
}
/**
@ -387,32 +271,149 @@ public class K8sGenericStub<O extends KubernetesObject,
APIResource context, String namespace, String name);
}
/**
* A supplier for specific stubs.
*
* @param <O> the object type
* @param <L> the object list type
* @param <R> the result type
*/
public interface SpecificSupplier<O extends KubernetesObject,
L extends KubernetesListObject, R extends K8sGenericStub<O, L>> {
/**
* Gets a new stub.
*
* @param client the client
* @param namespace the namespace
* @param name the name
* @return the result
*/
R get(K8sClient client, String namespace, String name);
}
@Override
@SuppressWarnings("PMD.UseLocaleWithCaseConversions")
public String toString() {
return (Strings.isNullOrEmpty(group) ? "" : group + "/")
+ version.toUpperCase() + kind + " " + namespace + ":" + name;
return (Strings.isNullOrEmpty(group()) ? "" : group() + "/")
+ version().toUpperCase() + kind() + " " + namespace + ":" + name;
}
/**
* Get a namespaced object stub. If the version in parameter
* `gvk` is an empty string, the stub refers to the first object
* found with matching group and kind.
*
* @param <O> the object type
* @param <L> the object list type
* @param <R> 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 <O extends KubernetesObject, L extends KubernetesListObject,
R extends K8sGenericStub<O, L>>
R get(Class<O> objectClass, Class<L> objectListClass,
K8sClient client, GroupVersionKind gvk, String namespace,
String name, GenericSupplier<O, L, R> 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 <O> the object type
* @param <L> the object list type
* @param <R> 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 <O extends KubernetesObject, L extends KubernetesListObject,
R extends K8sGenericStub<O, L>>
R get(Class<O> objectClass, Class<L> objectListClass,
K8sClient client, APIResource context, String namespace,
String name, GenericSupplier<O, L, R> provider) {
return provider.get(objectClass, objectListClass, client,
context, namespace, name);
}
/**
* Get a namespaced object stub for a newly created object.
*
* @param <O> the object type
* @param <L> the object list type
* @param <R> the stub type
* @param objectClass the object class
* @param objectListClass the object list class
* @param client the client
* @param context the context
* @param model the model
* @param provider the provider
* @return the stub if the object exists
* @throws ApiException the api exception
*/
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
public static <O extends KubernetesObject, L extends KubernetesListObject,
R extends K8sGenericStub<O, L>>
R create(Class<O> objectClass, Class<L> objectListClass,
K8sClient client, APIResource context, O model,
GenericSupplier<O, L, R> provider) throws ApiException {
var api = new GenericKubernetesApi<>(objectClass, objectListClass,
context.getGroup(), context.getPreferredVersion(),
context.getResourcePlural(), client);
api.create(model).throwsApiException();
return provider.get(objectClass, objectListClass, client,
context, model.getMetadata().getNamespace(),
model.getMetadata().getName());
}
/**
* Get the stubs for the objects in the given namespace that match
* the criteria from the given options.
*
* @param <O> the object type
* @param <L> the object list type
* @param <R> the stub type
* @param objectClass the object class
* @param objectListClass the object list class
* @param client the client
* @param context the context
* @param namespace the namespace
* @param options the options
* @param provider the provider
* @return the collection
* @throws ApiException the api exception
*/
public static <O extends KubernetesObject, L extends KubernetesListObject,
R extends K8sGenericStub<O, L>>
Collection<R> list(Class<O> objectClass, Class<L> objectListClass,
K8sClient client, APIResource context, String namespace,
ListOptions options, GenericSupplier<O, L, R> provider)
throws ApiException {
var result = new ArrayList<R>();
for (var version : candidateVersions(context)) {
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
var api = new GenericKubernetesApi<>(objectClass, objectListClass,
context.getGroup(), version, context.getResourcePlural(),
client);
var objs = api.list(namespace, options).throwsApiException();
for (var item : objs.getObject().getItems()) {
result.add(provider.get(objectClass, objectListClass, client,
context, namespace, item.getMetadata().getName()));
}
}
return result;
}
private static List<String> candidateVersions(APIResource context) {
var result = new LinkedList<>(context.getVersions());
result.remove(context.getPreferredVersion());
result.add(0, context.getPreferredVersion());
return result;
}
}

View file

@ -0,0 +1,230 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.common;
import io.kubernetes.client.Discovery.APIResource;
import io.kubernetes.client.common.KubernetesListObject;
import io.kubernetes.client.common.KubernetesObject;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.util.Watch.Response;
import io.kubernetes.client.util.generic.GenericKubernetesApi;
import io.kubernetes.client.util.generic.options.ListOptions;
import java.time.Duration;
import java.time.Instant;
import java.util.function.BiConsumer;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* An observer that watches namespaced resources in a given context and
* invokes a handler on changes.
*
* @param <O> the object type for the context
* @param <L> the object list type for the context
*/
public class K8sObserver<O extends KubernetesObject,
L extends KubernetesListObject> {
/**
* The type of change reported by {@link Response} as enum.
*/
public enum ResponseType {
ADDED, MODIFIED, DELETED
}
@SuppressWarnings("PMD.FieldNamingConventions")
protected final Logger logger = Logger.getLogger(getClass().getName());
protected final K8sClient client;
protected final GenericKubernetesApi<O, L> api;
protected final APIResource context;
protected final String namespace;
protected final ListOptions options;
protected final Thread thread;
protected BiConsumer<K8sClient, Response<O>> handler;
protected BiConsumer<K8sObserver<O, L>, Throwable> onTerminated;
/**
* Create and start a new observer for objects in the given context
* (using preferred version) and namespace with the given options.
*
* @param objectClass the object class
* @param objectListClass the object list class
* @param client the client
* @param context the context
* @param namespace the namespace
* @param options the options
*/
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
"PMD.UseObjectForClearerAPI", "PMD.AvoidCatchingThrowable",
"PMD.CognitiveComplexity" })
public K8sObserver(Class<O> objectClass, Class<L> objectListClass,
K8sClient client, APIResource context, String namespace,
ListOptions options) {
this.client = client;
this.context = context;
this.namespace = namespace;
this.options = options;
api = new GenericKubernetesApi<>(objectClass, objectListClass,
context.getGroup(), context.getPreferredVersion(),
context.getResourcePlural(), client);
thread = new Thread(() -> {
try {
logger.info(() -> "Watching " + context.getResourcePlural()
+ " (" + context.getPreferredVersion() + ")"
+ " in " + namespace);
// Watch sometimes terminates without apparent reason.
while (!Thread.currentThread().isInterrupted()) {
Instant startedAt = Instant.now();
try {
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
var changed = api.watch(namespace, options).iterator();
while (changed.hasNext()) {
handler.accept(client, changed.next());
}
} catch (ApiException e) {
logger.log(Level.FINE, e, () -> "Problem watching"
+ " (will retry): " + e.getMessage());
delayRestart(startedAt);
}
}
if (onTerminated != null) {
onTerminated.accept(this, null);
}
} catch (Throwable e) {
logger.log(Level.SEVERE, e, () -> "Probem watching: "
+ e.getMessage());
if (onTerminated != null) {
onTerminated.accept(this, e);
}
}
});
thread.setDaemon(true);
}
@SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
private void delayRestart(Instant started) {
var runningFor = Duration
.between(started, Instant.now()).toMillis();
if (runningFor < 5000) {
logger.log(Level.FINE, () -> "Waiting... ");
try {
Thread.sleep(5000 - runningFor);
} catch (InterruptedException e1) { // NOPMD
// Retry
}
logger.log(Level.FINE, () -> "Retrying");
}
}
/**
* Sets the handler.
*
* @param handler the handler
* @return the observer
*/
public K8sObserver<O, L>
handler(BiConsumer<K8sClient, Response<O>> handler) {
this.handler = handler;
return this;
}
/**
* Sets a function to invoke if the observer terminates. First argument
* is this observer, the second is the throwable that caused the
* abnormal termination or `null` if the observer was terminated
* by {@link #stop()}.
*
* @param onTerminated the on terminated
* @return the observer
*/
public K8sObserver<O, L> onTerminated(
BiConsumer<K8sObserver<O, L>, Throwable> onTerminated) {
this.onTerminated = onTerminated;
return this;
}
/**
* Start the observer.
*
* @return the observer
*/
public K8sObserver<O, L> start() {
if (handler == null) {
throw new IllegalStateException("No handler defined");
}
thread.start();
return this;
}
/**
* Stops the observer.
*
* @return the observer
*/
public K8sObserver<O, L> stop() {
thread.interrupt();
return this;
}
/**
* Returns the client.
*
* @return the client
*/
public K8sClient client() {
return client;
}
/**
* Returns the context.
*
* @return the context
*/
public APIResource context() {
return context;
}
/**
* Returns the observed namespace.
*
* @return the namespace
*/
public String getNamespace() {
return namespace;
}
/**
* Returns the options for object selection.
*
* @return the list options
*/
public ListOptions options() {
return options;
}
@Override
@SuppressWarnings("PMD.UseLocaleWithCaseConversions")
public String toString() {
return "Observer for " + K8s.toString(context) + " " + namespace;
}
}

View file

@ -30,6 +30,9 @@ import java.util.List;
public class K8sV1ConfigMapStub
extends K8sGenericStub<V1ConfigMap, V1ConfigMapList> {
public static final APIResource CONTEXT = new APIResource("", List.of("v1"),
"v1", "ConfigMap", true, "configmaps", "configmap");
/**
* Instantiates a new stub.
*
@ -40,9 +43,7 @@ public class K8sV1ConfigMapStub
protected K8sV1ConfigMapStub(K8sClient client, String namespace,
String name) {
super(V1ConfigMap.class, V1ConfigMapList.class, client,
new APIResource("", List.of("v1"), "v1", "ConfigMap", true,
"configmaps", "configmap"),
namespace, name);
CONTEXT, namespace, name);
}
/**

View file

@ -33,6 +33,10 @@ import java.util.Optional;
public class K8sV1DeploymentStub
extends K8sGenericStub<V1Deployment, V1DeploymentList> {
/** The deployment's context. */
public static final APIResource CONTEXT = new APIResource("apps",
List.of("v1"), "v1", "Pod", true, "deployments", "deployment");
/**
* Instantiates a new stub.
*
@ -43,22 +47,7 @@ public class K8sV1DeploymentStub
protected K8sV1DeploymentStub(K8sClient client, String namespace,
String name) {
super(V1Deployment.class, V1DeploymentList.class, client,
new APIResource("apps", List.of("v1"), "v1", "Pod", true,
"deployments", "deployment"),
namespace, name);
}
/**
* Gets the stub for the given namespace and name.
*
* @param client the client
* @param namespace the namespace
* @param name the name
* @return the deployment stub
*/
public static K8sV1DeploymentStub get(K8sClient client, String namespace,
String name) {
return new K8sV1DeploymentStub(client, namespace, name);
CONTEXT, namespace, name);
}
/**
@ -74,4 +63,17 @@ public class K8sV1DeploymentStub
+ "\", \"value\": " + replicas + "}]"),
client.defaultPatchOptions());
}
/**
* Gets the stub for the given namespace and name.
*
* @param client the client
* @param namespace the namespace
* @param name the name
* @return the deployment stub
*/
public static K8sV1DeploymentStub get(K8sClient client, String namespace,
String name) {
return new K8sV1DeploymentStub(client, namespace, name);
}
}

View file

@ -32,6 +32,7 @@ import java.util.List;
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class K8sV1PodStub extends K8sGenericStub<V1Pod, V1PodList> {
/** The pods' context. */
public static final APIResource CONTEXT
= new APIResource("", List.of("v1"), "v1", "Pod", true, "pods", "pod");
@ -72,7 +73,17 @@ public class K8sV1PodStub extends K8sGenericStub<V1Pod, V1PodList> {
public static Collection<K8sV1PodStub> list(K8sClient client,
String namespace, ListOptions options) throws ApiException {
return K8sGenericStub.list(V1Pod.class, V1PodList.class, client,
CONTEXT, namespace, options, K8sV1PodStub::new);
CONTEXT, namespace, options, K8sV1PodStub::getGeneric);
}
/**
* Provide {@link GenericSupplier}.
*/
@SuppressWarnings("PMD.UnusedFormalParameter")
private static K8sV1PodStub getGeneric(Class<V1Pod> objectClass,
Class<V1PodList> objectListClass, K8sClient client,
APIResource context, String namespace, String name) {
return new K8sV1PodStub(client, namespace, name);
}
}

View file

@ -0,0 +1,60 @@
/*
* VM-Operator
* Copyright (C) 2024 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.common;
import io.kubernetes.client.Discovery.APIResource;
import io.kubernetes.client.openapi.models.V1Secret;
import io.kubernetes.client.openapi.models.V1SecretList;
import java.util.List;
/**
* A stub for secrets (v1).
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class K8sV1SecretStub extends K8sGenericStub<V1Secret, V1SecretList> {
public static final APIResource CONTEXT = new APIResource("", List.of("v1"),
"v1", "Secret", true, "secrets", "secret");
/**
* Instantiates a new stub.
*
* @param client the client
* @param namespace the namespace
* @param name the name
*/
protected K8sV1SecretStub(K8sClient client, String namespace,
String name) {
super(V1Secret.class, V1SecretList.class, client,
CONTEXT, namespace, name);
}
/**
* Gets the stub for the given namespace and name.
*
* @param client the client
* @param namespace the namespace
* @param name the name
* @return the config map stub
*/
public static K8sV1SecretStub get(K8sClient client, String namespace,
String name) {
return new K8sV1SecretStub(client, namespace, name);
}
}

View file

@ -30,6 +30,11 @@ import java.util.List;
public class K8sV1StatefulSetStub
extends K8sGenericStub<V1StatefulSet, V1StatefulSetList> {
/** The stateful sets' context */
public static final APIResource CONTEXT
= new APIResource("apps", List.of("v1"), "v1", "StatefulSet", true,
"statefulsets", "statefulset");
/**
* Instantiates a new stub.
*
@ -39,9 +44,7 @@ public class K8sV1StatefulSetStub
*/
protected K8sV1StatefulSetStub(K8sClient client, String namespace,
String name) {
super(V1StatefulSet.class, V1StatefulSetList.class, client,
new APIResource("apps", List.of("v1"), "v1", "StatefulSet", true,
"statefulsets", "statefulset"),
super(V1StatefulSet.class, V1StatefulSetList.class, client, CONTEXT,
namespace, name);
}

View file

@ -0,0 +1,207 @@
/*
* VM-Operator
* Copyright (C) 2023 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.manager.events;
import java.lang.ref.WeakReference;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.jgrapes.core.Channel;
/**
* A channel manager that tracks mappings from a key to a channel using
* "add/remove" (or "open/close") events and the channels on which they
* are delivered.
*
* @param <K> the key type
* @param <C> the channel type
* @param <A> the type of the associated data
*/
public class ChannelCache<K, C extends Channel, A> {
private final Map<K, Data<C, A>> channels = new ConcurrentHashMap<>();
/**
* Helper
*/
@SuppressWarnings("PMD.ShortClassName")
private static class Data<C extends Channel, A> {
public WeakReference<C> channel;
public A associated;
/**
* Instantiates a new value.
*
* @param channel the channel
*/
public Data(C channel) {
this.channel = new WeakReference<>(channel);
}
}
/**
* Combines the channel and the associated data.
*
* @param <C> the generic type
* @param <A> the generic type
*/
@SuppressWarnings("PMD.ShortClassName")
public static class Both<C extends Channel, A> {
/** The channel. */
public C channel;
/** The associated. */
public A associated;
/**
* Instantiates a new both.
*
* @param channel the channel
* @param associated the associated
*/
public Both(C channel, A associated) {
super();
this.channel = channel;
this.associated = associated;
}
}
/**
* Returns the channel and associates data registered for the key
* or an empty optional if no mapping exists.
*
* @param key the key
* @return the result
*/
public Optional<Both<C, A>> both(K key) {
synchronized (channels) {
var value = channels.get(key);
if (value == null) {
return Optional.empty();
}
var channel = value.channel.get();
if (channel == null) {
// Cleanup old reference
channels.remove(key);
return Optional.empty();
}
return Optional.of(new Both<>(channel, value.associated));
}
}
/**
* Store the given data.
*
* @param key the key
* @param channel the channel
* @param associated the associated
* @return the channel manager
*/
public ChannelCache<K, C, A> put(K key, C channel, A associated) {
Data<C, A> data = new Data<>(channel);
data.associated = associated;
channels.put(key, data);
return this;
}
/**
* Store the given data.
*
* @param key the key
* @param channel the channel
* @return the channel manager
*/
public ChannelCache<K, C, A> put(K key, C channel) {
put(key, channel, null);
return this;
}
/**
* Returns the channel registered for the key or an empty optional
* if no mapping exists.
*
* @param key the key
* @return the optional
*/
public Optional<C> channel(K key) {
return both(key).map(b -> b.channel);
}
/**
* Associate the entry for the channel with the given data. The entry
* for the channel must already exist.
*
* @param key the key
* @param data the data
* @return the channel manager
*/
public ChannelCache<K, C, A> associate(K key, A data) {
synchronized (channels) {
Optional.ofNullable(channels.get(key))
.ifPresent(v -> v.associated = data);
}
return this;
}
/**
* Return the data associated with the entry for the channel.
*
* @param key the key
* @return the data
*/
public Optional<A> associated(K key) {
return both(key).map(b -> b.associated);
}
/**
* Returns all associated data.
*
* @return the collection
*/
public Collection<A> associated() {
synchronized (channels) {
return channels.values().stream()
.filter(v -> v.channel.get() != null && v.associated != null)
.map(v -> v.associated).toList();
}
}
/**
* Removes the channel with the given name.
*
* @param name the name
*/
public void remove(String name) {
synchronized (channels) {
channels.remove(name);
}
}
/**
* Returns all known keys.
*
* @return the sets the
*/
public Set<K> keys() {
return channels.keySet();
}
}

View file

@ -0,0 +1,303 @@
/*
* VM-Operator
* Copyright (C) 2023 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.manager.events;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import org.jgrapes.core.Channel;
/**
* A channel manager that maintains mappings from a key to a channel.
* As a convenience, it is possible to additionally associate arbitrary
* data with the entry (and thus with the channel).
*
* The manager should be used by a component that defines channels for
* housekeeping. It can be shared between this component and another
* component, preferably using the {@link #fixed()} view for the
* second component. Alternatively, the second component can use a
* {@link ChannelCache} to track the mappings using events.
*
* @param <K> the key type
* @param <C> the channel type
* @param <A> the type of the associated data
*/
public class ChannelManager<K, C extends Channel, A> {
private final Map<K, Both<C, A>> channels = new ConcurrentHashMap<>();
private final Function<K, C> supplier;
private ChannelManager<K, C, A> readOnly;
/**
* Combines the channel and the associated data.
*
* @param <C> the generic type
* @param <A> the generic type
*/
@SuppressWarnings("PMD.ShortClassName")
public static class Both<C extends Channel, A> {
/** The channel. */
public C channel;
/** The associated. */
public A associated;
/**
* Instantiates a new both.
*
* @param channel the channel
* @param associated the associated
*/
public Both(C channel, A associated) {
super();
this.channel = channel;
this.associated = associated;
}
}
/**
* Instantiates a new channel manager.
*
* @param supplier the supplier that creates new channels
*/
public ChannelManager(Function<K, C> supplier) {
this.supplier = supplier;
}
/**
* Instantiates a new channel manager without a default supplier.
*/
public ChannelManager() {
this(k -> null);
}
/**
* Returns the channel and associates data registered for the key
* or an empty optional if no mapping exists.
*
* @param key the key
* @return the result
*/
public Optional<Both<C, A>> both(K key) {
synchronized (channels) {
return Optional.ofNullable(channels.get(key));
}
}
/**
* Store the given data.
*
* @param key the key
* @param channel the channel
* @param associated the associated
* @return the channel manager
*/
public ChannelManager<K, C, A> put(K key, C channel, A associated) {
channels.put(key, new Both<>(channel, associated));
return this;
}
/**
* Store the given data.
*
* @param key the key
* @param channel the channel
* @return the channel manager
*/
public ChannelManager<K, C, A> put(K key, C channel) {
put(key, channel, null);
return this;
}
/**
* Returns the channel registered for the key or an empty optional
* if no mapping exists.
*
* @param key the key
* @return the optional
*/
public Optional<C> channel(K key) {
return both(key).map(b -> b.channel);
}
/**
* Returns the {@link Channel} for the given name, creating it using
* the supplier passed to the constructor if it doesn't exist yet.
*
* @param key the key
* @return the channel
*/
public Optional<C> getChannel(K key) {
return getChannel(key, supplier);
}
/**
* Returns the {@link Channel} for the given name, creating it using
* the given supplier if it doesn't exist yet.
*
* @param key the key
* @param supplier the supplier
* @return the channel
*/
@SuppressWarnings({ "PMD.AssignmentInOperand",
"PMD.DataflowAnomalyAnalysis" })
public Optional<C> getChannel(K key, Function<K, C> supplier) {
synchronized (channels) {
return Optional
.of(Optional.ofNullable(channels.get(key))
.map(v -> v.channel)
.orElseGet(() -> {
var channel = supplier.apply(key);
channels.put(key, new Both<>(channel, null));
return channel;
}));
}
}
/**
* Associate the entry for the channel with the given data. The entry
* for the channel must already exist.
*
* @param key the key
* @param data the data
* @return the channel manager
*/
public ChannelManager<K, C, A> associate(K key, A data) {
synchronized (channels) {
Optional.ofNullable(channels.get(key))
.ifPresent(v -> v.associated = data);
}
return this;
}
/**
* Return the data associated with the entry for the channel.
*
* @param key the key
* @return the data
*/
public Optional<A> associated(K key) {
return both(key).map(b -> b.associated);
}
/**
* Returns all associated data.
*
* @return the collection
*/
public Collection<A> associated() {
synchronized (channels) {
return channels.values().stream()
.filter(v -> v.associated != null)
.map(v -> v.associated).toList();
}
}
/**
* Removes the channel with the given name.
*
* @param name the name
*/
public void remove(String name) {
synchronized (channels) {
channels.remove(name);
}
}
/**
* Returns all known keys.
*
* @return the sets the
*/
public Set<K> keys() {
return channels.keySet();
}
/**
* Returns a read only view of this channel manager. The methods
* that usually create a new entry refrain from doing so. The
* methods that change the value of channel and {@link #remove(String)}
* do nothing. The associated data, however, can still be changed.
*
* @return the channel manager
*/
public ChannelManager<K, C, A> fixed() {
if (readOnly == null) {
readOnly = new ChannelManager<>(supplier) {
@Override
public Optional<Both<C, A>> both(K key) {
return ChannelManager.this.both(key);
}
@Override
public ChannelManager<K, C, A> put(K key, C channel,
A associated) {
return associate(key, associated);
}
@Override
public Optional<C> getChannel(K key) {
return ChannelManager.this.channel(key);
}
@Override
public Optional<C> getChannel(K key, Function<K, C> supplier) {
return ChannelManager.this.channel(key);
}
@Override
public ChannelManager<K, C, A> associate(K key, A data) {
return ChannelManager.this.associate(key, data);
}
@Override
public Optional<A> associated(K key) {
return ChannelManager.this.associated(key);
}
@Override
public Collection<A> associated() {
return ChannelManager.this.associated();
}
@Override
public void remove(String name) {
// Do nothing
}
@Override
public Set<K> keys() {
return ChannelManager.this.keys();
}
@Override
public ChannelManager<K, C, A> fixed() {
return ChannelManager.this.fixed();
}
};
}
return readOnly;
}
}

View file

@ -0,0 +1,77 @@
/*
* VM-Operator
* Copyright (C) 2024 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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 DisplaySecretChanged extends Event<Void> {
private final ResponseType type;
private final V1Secret secret;
/**
* Initializes a new display secret changed event.
*
* @param type the type
* @param secret the secret
*/
public DisplaySecretChanged(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=");
builder.append(Channel.toString(channels()));
}
builder.append(']');
return builder.toString();
}
}

View file

@ -18,8 +18,8 @@
package org.jdrupes.vmoperator.manager.events;
import io.kubernetes.client.openapi.models.V1APIResource;
import org.jdrupes.vmoperator.common.K8sDynamicModel;
import org.jdrupes.vmoperator.common.K8sObserver;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Components;
import org.jgrapes.core.Event;
@ -34,16 +34,8 @@ import org.jgrapes.core.Event;
@SuppressWarnings("PMD.DataClass")
public class VmDefChanged extends Event<Void> {
/**
* The type of change.
*/
public enum Type {
ADDED, MODIFIED, DELETED
}
private final Type type;
private final K8sObserver.ResponseType type;
private final boolean specChanged;
private final V1APIResource crd;
private final K8sDynamicModel vmDef;
/**
@ -51,14 +43,12 @@ public class VmDefChanged extends Event<Void> {
*
* @param type the type
* @param specChanged the spec part changed
* @param crd the crd
* @param vmDefinition the VM definition
*/
public VmDefChanged(Type type, boolean specChanged, V1APIResource crd,
public VmDefChanged(K8sObserver.ResponseType type, boolean specChanged,
K8sDynamicModel vmDefinition) {
this.type = type;
this.specChanged = specChanged;
this.crd = crd;
this.vmDef = vmDefinition;
}
@ -67,7 +57,7 @@ public class VmDefChanged extends Event<Void> {
*
* @return the type
*/
public Type type() {
public K8sObserver.ResponseType type() {
return type;
}
@ -78,15 +68,6 @@ public class VmDefChanged extends Event<Void> {
return specChanged;
}
/**
* Returns the Crd.
*
* @return the v 1 API resource
*/
public V1APIResource crd() {
return crd;
}
/**
* Returns the object.
*

View file

@ -126,8 +126,14 @@ spec:
# hostPath:
# path: /sys/fs/cgroup
- name: config
configMap:
name: ${ cr.metadata.name.asString }
projected:
sources:
- configMap:
name: ${ cr.metadata.name.asString }
<#if displaySecret??>
- secret:
name: ${ displaySecret }
</#if>
- name: vmop-image-repository
persistentVolumeClaim:
claimName: vmop-image-repository

View file

@ -0,0 +1,287 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.manager;
import io.kubernetes.client.Discovery.APIResource;
import io.kubernetes.client.common.KubernetesListObject;
import io.kubernetes.client.common.KubernetesObject;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.util.Watch.Response;
import io.kubernetes.client.util.generic.options.ListOptions;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import org.jdrupes.vmoperator.common.K8s;
import org.jdrupes.vmoperator.common.K8sClient;
import org.jdrupes.vmoperator.common.K8sObserver;
import org.jdrupes.vmoperator.common.K8sObserver.ResponseType;
import org.jdrupes.vmoperator.manager.events.ChannelManager;
import org.jdrupes.vmoperator.manager.events.Exit;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Component;
import org.jgrapes.core.Components;
import org.jgrapes.core.annotation.Handler;
import org.jgrapes.core.events.Start;
import org.jgrapes.core.events.Stop;
import org.jgrapes.util.events.ConfigurationUpdate;
/**
* A base class for monitoring VM related resources.
*
* @param <O> the object type for the context
* @param <L> the object list type for the context
*/
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis" })
public abstract class AbstractMonitor<O extends KubernetesObject,
L extends KubernetesListObject, C extends Channel> extends Component {
private final Class<O> objectClass;
private final Class<L> objectListClass;
private K8sClient client;
private APIResource context;
private String namespace;
private ListOptions options = new ListOptions();
private final AtomicInteger observerCounter = new AtomicInteger(0);
private ChannelManager<String, C, ?> channelManager;
private boolean channelManagerMaster;
/**
* Initializes the instance.
*
* @param componentChannel the component channel
*/
protected AbstractMonitor(Channel componentChannel, Class<O> objectClass,
Class<L> objectListClass) {
super(componentChannel);
this.objectClass = objectClass;
this.objectListClass = objectListClass;
}
/**
* Return the client.
*
* @return the client
*/
public K8sClient client() {
return client;
}
/**
* Sets the client to be used.
*
* @param client the client
* @return the abstract monitor
*/
public AbstractMonitor<O, L, C> client(K8sClient client) {
this.client = client;
return this;
}
/**
* Return the observed namespace.
*
* @return the namespace
*/
public String namespace() {
return namespace;
}
/**
* Sets the namespace to be observed.
*
* @param namespace the namespaceToWatch to set
* @return the abstract monitor
*/
public AbstractMonitor<O, L, C> namespace(String namespace) {
this.namespace = namespace;
return this;
}
/**
* Returns the options for selecting the objects to observe.
*
* @return the options
*/
public ListOptions options() {
return options;
}
/**
* Sets the options for selecting the objects to observe.
*
* @param options the options to set
* @return the abstract monitor
*/
public AbstractMonitor<O, L, C> options(ListOptions options) {
this.options = options;
return this;
}
/**
* Returns the observed context.
*
* @return the context
*/
public APIResource context() {
return context;
}
/**
* Sets the context to observe.
*
* @param context the context
* @return the abstract monitor
*/
public AbstractMonitor<O, L, C> context(APIResource context) {
this.context = context;
return this;
}
/**
* Returns the channel manager.
*
* @return the context
*/
public ChannelManager<String, C, ?> channelManager() {
return channelManager;
}
/**
* Sets the channel manager.
*
* @param channelManager the channel manager
* @return the abstract monitor
*/
public AbstractMonitor<O, L, C>
channelManager(ChannelManager<String, C, ?> channelManager) {
this.channelManager = channelManager;
return this;
}
/**
* Looks for a key "namespace" in the configuration and, if found,
* sets the namespace to its value.
*
* @param event the event
*/
@Handler
public void onConfigurationUpdate(ConfigurationUpdate event) {
event.structured(Components.manager(parent()).componentPath())
.ifPresent(c -> {
if (c.containsKey("namespace")) {
namespace = (String) c.get("namespace");
}
});
}
/**
* Handle the start event. Configures the namespace invokes
* {@link #prepareMonitoring()} and starts the observers.
*
* @param event the event
*/
@Handler(priority = 10)
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
public void onStart(Start event) {
try {
// Get namespace
if (namespace == null) {
var path = Path
.of("/var/run/secrets/kubernetes.io/serviceaccount/namespace");
if (Files.isReadable(path)) {
namespace
= Files.lines(path).findFirst().orElse(null);
}
}
// Additional preparations by derived class
prepareMonitoring();
assert client != null;
assert context != null;
assert namespace != null;
logger.fine(() -> "Observing " + K8s.toString(context)
+ " objects in " + namespace);
// Monitor all versions
for (var version : context.getVersions()) {
createObserver(version);
}
registerAsGenerator();
} catch (IOException | ApiException e) {
logger.log(Level.SEVERE, e,
() -> "Cannot watch VMs, terminating.");
event.cancel(true);
fire(new Exit(1));
}
}
private void createObserver(String version) {
observerCounter.incrementAndGet();
new K8sObserver<>(objectClass, objectListClass, client,
K8s.preferred(context, version), namespace, options)
.handler((c, r) -> {
handleChange(c, r);
if (ResponseType.valueOf(r.type) == ResponseType.DELETED
&& channelManagerMaster) {
channelManager.remove(r.object.getMetadata().getName());
}
}).onTerminated((o, t) -> {
if (observerCounter.decrementAndGet() == 0) {
unregisterAsGenerator();
}
// Exception has been logged already
if (t != null) {
fire(new Stop());
}
}).start();
}
/**
* Invoked by {@link #onStart(Start)} after the namespace has
* been configured and before starting the observer.
*
* @throws IOException Signals that an I/O exception has occurred.
* @throws ApiException the api exception
*/
@SuppressWarnings("PMD.EmptyMethodInAbstractClassShouldBeAbstract")
protected void prepareMonitoring() throws IOException, ApiException {
// To be overridden by derived class.
}
/**
* Handle an observed change.
*
* @param client the client
* @param change the change
*/
protected abstract void handleChange(K8sClient client, Response<O> change);
/**
* Returns the {@link Channel} for the given name.
*
* @param name the name
* @return the channel used for events related to the specified object
*/
protected Optional<C> channel(String name) {
return channelManager.getChannel(name);
}
}

View file

@ -23,6 +23,9 @@ package org.jdrupes.vmoperator.manager;
*/
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 STATE_RUNNING. */
public static final String STATE_RUNNING = "Running";

View file

@ -30,6 +30,7 @@ import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM;
import org.jdrupes.vmoperator.common.K8sClient;
import org.jdrupes.vmoperator.common.K8sDynamicStub;
import org.jdrupes.vmoperator.manager.events.ChannelManager;
import org.jdrupes.vmoperator.manager.events.Exit;
import org.jdrupes.vmoperator.manager.events.ModifyVm;
import org.jdrupes.vmoperator.manager.events.VmChannel;
@ -46,7 +47,7 @@ import org.jgrapes.util.events.ConfigurationUpdate;
* [Operator Whitepaper](https://github.com/cncf/tag-app-delivery/blob/eece8f7307f2970f46f100f51932db106db46968/operator-wg/whitepaper/Operator-WhitePaper_v1-0.md#operator-components-in-kubernetes).
*
* The implementation splits the controller in two components. The
* {@link VmWatcher} and the {@link Reconciler}. The former watches
* {@link VmMonitor} and the {@link Reconciler}. The former watches
* the VM definitions (CRs) and generates {@link VmDefChanged} events
* when they change. The latter handles the changes and reconciles the
* resources in the cluster.
@ -87,7 +88,20 @@ public class Controller extends Component {
public Controller(Channel componentChannel) {
super(componentChannel);
// Prepare component tree
attach(new VmWatcher(channel()));
ChannelManager<String, VmChannel, ?> chanMgr
= new ChannelManager<>(name -> {
try {
return new VmChannel(channel(), newEventPipeline(),
new K8sClient());
} catch (IOException e) {
logger.log(Level.SEVERE, e, () -> "Failed to create client"
+ " for handling changes: " + e.getMessage());
return null;
}
});
attach(new VmMonitor(channel()).channelManager(chanMgr));
attach(new DisplaySecretsMonitor(channel())
.channelManager(chanMgr.fixed()));
attach(new Reconciler(channel()));
}

View file

@ -0,0 +1,77 @@
/*
* VM-Operator
* Copyright (C) 2024 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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.DisplaySecretChanged;
import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jgrapes.core.Channel;
/**
* Watches for changes of display secrets.
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class DisplaySecretsMonitor
extends AbstractMonitor<V1Secret, V1SecretList, VmChannel> {
/**
* Instantiates a new display secrets monitor.
*
* @param componentChannel the component channel
*/
public DisplaySecretsMonitor(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<V1Secret> 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 DisplaySecretChanged(
ResponseType.valueOf(change.type), change.object), channel);
}
}

View file

@ -45,9 +45,9 @@ import java.util.Map;
import java.util.Optional;
import org.jdrupes.vmoperator.common.Convertions;
import org.jdrupes.vmoperator.common.K8sDynamicModel;
import org.jdrupes.vmoperator.common.K8sObserver;
import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jdrupes.vmoperator.manager.events.VmDefChanged.Type;
import org.jdrupes.vmoperator.util.ExtendedObjectWrapper;
import org.jdrupes.vmoperator.util.GsonPtr;
import org.jgrapes.core.Channel;
@ -194,7 +194,7 @@ public class Reconciler extends Component {
// Ownership relationships takes care of deletions
var defMeta = event.vmDefinition().getMetadata();
if (event.type() == Type.DELETED) {
if (event.type() == K8sObserver.ResponseType.DELETED) {
logger.fine(() -> "VM \"" + defMeta.getName() + "\" deleted");
return;
}

View file

@ -28,6 +28,7 @@ import java.io.IOException;
import java.io.StringWriter;
import java.util.Map;
import java.util.logging.Logger;
import org.jdrupes.vmoperator.common.K8sV1SecretStub;
import org.jdrupes.vmoperator.common.K8sV1StatefulSetStub;
import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
@ -69,6 +70,13 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
throws IOException, TemplateException, ApiException {
var metadata = event.vmDefinition().getMetadata();
// Check if we have a display secret
var dsStub = K8sV1SecretStub.get(channel.client(),
metadata.getNamespace(), metadata.getName() + "-display-secret");
dsStub.model().ifPresent(m -> {
model.put("displaySecret", m.getMetadata().getName());
});
// Combine template and data and parse result
var fmTemplate = fmConfig.getTemplate("runnerSts.ftl.yaml");
StringWriter out = new StringWriter();

View file

@ -0,0 +1,183 @@
/*
* VM-Operator
* 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
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.manager;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.models.V1ObjectMeta;
import io.kubernetes.client.util.Watch;
import io.kubernetes.client.util.generic.options.ListOptions;
import java.io.IOException;
import java.util.Set;
import java.util.logging.Level;
import java.util.stream.Collectors;
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 static org.jdrupes.vmoperator.manager.Constants.APP_NAME;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_KIND_VM;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME;
import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jdrupes.vmoperator.util.GsonPtr;
import org.jgrapes.core.Channel;
/**
* Watches for changes of VM definitions.
*/
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports" })
public class VmMonitor
extends AbstractMonitor<K8sDynamicModel, K8sDynamicModels, VmChannel> {
/**
* Instantiates a new VM definition watcher.
*
* @param componentChannel the component channel
*/
public VmMonitor(Channel componentChannel) {
super(componentChannel, K8sDynamicModel.class, K8sDynamicModels.class);
}
@Override
protected void prepareMonitoring() throws IOException, ApiException {
client(new K8sClient());
// Get all our API versions
var ctx = K8s.context(client(), VM_OP_GROUP, "", VM_OP_KIND_VM);
if (ctx.isEmpty()) {
logger.severe(() -> "Cannot get CRD context.");
return;
}
context(ctx.get());
// Remove left over resources
purge();
}
@SuppressWarnings("PMD.CognitiveComplexity")
private void purge() throws ApiException {
// Get existing CRs (VMs)
var known = K8sDynamicStub.list(client(), context(), namespace())
.stream().map(stub -> stub.name()).collect(Collectors.toSet());
ListOptions opts = new ListOptions();
opts.setLabelSelector(
"app.kubernetes.io/managed-by=" + VM_OP_NAME + ","
+ "app.kubernetes.io/name=" + APP_NAME);
for (var context : Set.of(K8sV1StatefulSetStub.CONTEXT,
K8sV1ConfigMapStub.CONTEXT)) {
for (var resStub : K8sDynamicStub.list(client(), context,
namespace(), opts)) {
String instance = resStub.model()
.map(m -> m.metadata().getName()).orElse("(unknown)");
if (!known.contains(instance)) {
resStub.delete();
}
}
}
}
@Override
protected void handleChange(K8sClient client,
Watch.Response<K8sDynamicModel> response) {
V1ObjectMeta metadata = response.object.getMetadata();
VmChannel channel = channel(metadata.getName()).orElse(null);
if (channel == null) {
return;
}
// Get full definition and associate with channel as backup
var vmDef = response.object;
if (vmDef.data() == null) {
// ADDED event does not provide data, see
// https://github.com/kubernetes-client/java/issues/3215
vmDef = getModel(client, vmDef);
}
if (vmDef.data() != null) {
// New data, augment and save
addDynamicData(channel.client(), vmDef);
channel.setVmDefinition(vmDef);
} else {
// Reuse cached
vmDef = channel.vmDefinition();
}
if (vmDef == null) {
logger.warning(
() -> "Cannot get model for " + response.object.getMetadata());
return;
}
// Create and fire event
channel.pipeline()
.fire(new VmDefChanged(ResponseType.valueOf(response.type),
channel.setGeneration(
response.object.getMetadata().getGeneration()),
vmDef), channel);
}
private K8sDynamicModel getModel(K8sClient client, K8sDynamicModel vmDef) {
try {
return K8sDynamicStub.get(client, context(), namespace(),
vmDef.metadata().getName()).model().orElse(null);
} catch (ApiException e) {
return null;
}
}
private void addDynamicData(K8sClient client, K8sDynamicModel vmState) {
var rootNode = GsonPtr.to(vmState.data()).get(JsonObject.class);
rootNode.addProperty("nodeName", "");
// VM definition status changes before the pod terminates.
// This results in pod information being shown for a stopped
// VM which is irritating. So check condition first.
var isRunning = GsonPtr.to(rootNode).to("status", "conditions")
.get(JsonArray.class)
.asList().stream().filter(el -> "Running"
.equals(((JsonObject) el).get("type").getAsString()))
.findFirst().map(el -> "True"
.equals(((JsonObject) el).get("status").getAsString()))
.orElse(false);
if (!isRunning) {
return;
}
var podSearch = new ListOptions();
podSearch.setLabelSelector("app.kubernetes.io/name=" + APP_NAME
+ ",app.kubernetes.io/component=" + APP_NAME
+ ",app.kubernetes.io/instance=" + vmState.getMetadata().getName());
try {
var podList
= K8sV1PodStub.list(client, namespace(), podSearch);
for (var podStub : podList) {
rootNode.addProperty("nodeName",
podStub.model().get().getSpec().getNodeName());
}
} catch (ApiException e) {
logger.log(Level.WARNING, e,
() -> "Cannot access node information: " + e.getMessage());
}
}
}

View file

@ -1,360 +0,0 @@
/*
* VM-Operator
* 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
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.manager;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
import io.kubernetes.client.apimachinery.GroupVersion;
import io.kubernetes.client.apimachinery.GroupVersionKind;
import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.apis.ApisApi;
import io.kubernetes.client.openapi.apis.CustomObjectsApi;
import io.kubernetes.client.openapi.models.V1APIGroup;
import io.kubernetes.client.openapi.models.V1APIResource;
import io.kubernetes.client.openapi.models.V1GroupVersionForDiscovery;
import io.kubernetes.client.openapi.models.V1Namespace;
import io.kubernetes.client.openapi.models.V1ObjectMeta;
import io.kubernetes.client.util.Config;
import io.kubernetes.client.util.Watch;
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi;
import io.kubernetes.client.util.generic.options.ListOptions;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP;
import org.jdrupes.vmoperator.common.K8sClient;
import org.jdrupes.vmoperator.common.K8sDynamicModel;
import org.jdrupes.vmoperator.common.K8sDynamicStub;
import org.jdrupes.vmoperator.common.K8sV1PodStub;
import static org.jdrupes.vmoperator.manager.Constants.APP_NAME;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_KIND_VM;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME;
import org.jdrupes.vmoperator.manager.events.Exit;
import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jdrupes.vmoperator.manager.events.VmDefChanged.Type;
import org.jdrupes.vmoperator.util.GsonPtr;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Component;
import org.jgrapes.core.Components;
import org.jgrapes.core.annotation.Handler;
import org.jgrapes.core.events.Start;
import org.jgrapes.core.events.Stop;
import org.jgrapes.util.events.ConfigurationUpdate;
/**
* Watches for changes of VM definitions.
*/
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports" })
public class VmWatcher extends Component {
private String namespaceToWatch;
private final Map<String, VmChannel> channels = new ConcurrentHashMap<>();
/**
* Instantiates a new VM definition watcher.
*
* @param componentChannel the component channel
*/
public VmWatcher(Channel componentChannel) {
super(componentChannel);
}
/**
* Configure the component.
*
* @param event the event
*/
@Handler
public void onConfigurationUpdate(ConfigurationUpdate event) {
event.structured(Components.manager(parent()).componentPath())
.ifPresent(c -> {
if (c.containsKey("namespace")) {
namespaceToWatch = (String) c.get("namespace");
}
});
}
/**
* Handle the start event.
*
* @param event the event
* @throws IOException
* @throws ApiException
*/
@Handler(priority = 10)
public void onStart(Start event) {
try {
startWatching();
} catch (IOException | ApiException e) {
logger.log(Level.SEVERE, e,
() -> "Cannot watch VMs, terminating.");
event.cancel(true);
fire(new Exit(1));
}
}
private void startWatching() throws IOException, ApiException {
// Get namespace
if (namespaceToWatch == null) {
var path = Path
.of("/var/run/secrets/kubernetes.io/serviceaccount/namespace");
if (Files.isReadable(path)) {
namespaceToWatch = Files.lines(path).findFirst().orElse(null);
}
}
// Availability already checked by Controller.onStart
logger.fine(() -> "Watching namespace \"" + namespaceToWatch + "\".");
// Get all our API versions
var client = Config.defaultClient();
var apis = new ApisApi(client).getAPIVersions();
var vmOpApiVersions = apis.getGroups().stream()
.filter(g -> g.getName().equals(VM_OP_GROUP)).findFirst()
.map(V1APIGroup::getVersions).stream().flatMap(l -> l.stream())
.map(V1GroupVersionForDiscovery::getVersion).toList();
// Remove left overs
var coa = new CustomObjectsApi(client);
purge(client, coa, vmOpApiVersions);
// Start a watcher thread for each existing CRD version.
// The watcher will send us an "ADDED" for each existing VM.
for (var version : vmOpApiVersions) {
coa.getAPIResources(VM_OP_GROUP, version)
.getResources().stream()
.filter(r -> VM_OP_KIND_VM.equals(r.getKind()))
.findFirst()
.ifPresent(crd -> watchVmDefs(crd, version));
}
}
@SuppressWarnings("PMD.CognitiveComplexity")
private void purge(ApiClient client, CustomObjectsApi coa,
List<String> vmOpApiVersions) throws ApiException {
// Get existing CRs (VMs)
Set<String> known = new HashSet<>();
for (var version : vmOpApiVersions) {
// Get all known CR instances.
coa.getAPIResources(VM_OP_GROUP, version)
.getResources().stream()
.filter(r -> VM_OP_KIND_VM.equals(r.getKind()))
.findFirst()
.ifPresent(crd -> known.addAll(getKnown(client, crd, version)));
}
ListOptions opts = new ListOptions();
opts.setLabelSelector(
"app.kubernetes.io/managed-by=" + VM_OP_NAME + ","
+ "app.kubernetes.io/name=" + APP_NAME);
for (String resource : List.of("apps/v1/statefulsets",
"v1/configmaps", "v1/secrets")) {
@SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops",
"PMD.AvoidDuplicateLiterals" })
var resParts = new LinkedList<>(List.of(resource.split("/")));
var group = resParts.size() == 3 ? resParts.poll() : "";
var version = resParts.poll();
var plural = resParts.poll();
// Get resources, selected by label
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
var api = new DynamicKubernetesApi(group, version, plural, client);
var listObj = api.list(namespaceToWatch, opts).getObject();
if (listObj == null) {
continue;
}
for (var obj : listObj.getItems()) {
String instance = obj.getMetadata().getLabels()
.get("app.kubernetes.io/instance");
if (!known.contains(instance)) {
var resName = obj.getMetadata().getName();
var result = api.delete(namespaceToWatch, resName);
if (!result.isSuccess()) {
logger.warning(() -> "Cannot cleanup resource \""
+ resName + "\": " + result.toString());
}
}
}
}
}
private Set<String> getKnown(ApiClient client, V1APIResource crd,
String version) {
Set<String> result = new HashSet<>();
var api = new DynamicKubernetesApi(VM_OP_GROUP, version,
crd.getName(), client);
for (var item : api.list(namespaceToWatch).getObject().getItems()) {
if (!VM_OP_KIND_VM.equals(item.getKind())) {
continue;
}
result.add(item.getMetadata().getName());
}
return result;
}
private void watchVmDefs(V1APIResource crd, String version) {
@SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops",
"PMD.AvoidCatchingThrowable", "PMD.AvoidCatchingGenericException" })
var watcher = new Thread(() -> {
try {
logger.info(() -> "Watching objects created from "
+ crd.getName() + "." + VM_OP_GROUP + "/" + version);
// Watch sometimes terminates without apparent reason.
while (true) {
Instant startedAt = Instant.now();
var client = Config.defaultClient();
var coa = new CustomObjectsApi(client);
var call = coa.listNamespacedCustomObjectCall(VM_OP_GROUP,
version, namespaceToWatch, crd.getName(), null, false,
null, null, null, null, null, null, null, true, null);
try (Watch<V1Namespace> watch
= Watch.createWatch(client, call,
new TypeToken<Watch.Response<V1Namespace>>() {
}.getType())) {
for (Watch.Response<V1Namespace> item : watch) {
handleVmDefinitionChange(crd, item);
}
} catch (IOException | ApiException | RuntimeException e) {
logger.log(Level.FINE, e, () -> "Problem watching \""
+ crd.getName() + "\" (will retry): "
+ e.getMessage());
delayRestart(startedAt);
}
}
} catch (Throwable e) {
logger.log(Level.SEVERE, e, () -> "Probem watching: "
+ e.getMessage());
}
fire(new Stop());
});
watcher.setDaemon(true);
watcher.start();
}
@SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
private void delayRestart(Instant started) {
var runningFor = Duration
.between(started, Instant.now()).toMillis();
if (runningFor < 5000) {
logger.log(Level.FINE, () -> "Waiting... ");
try {
Thread.sleep(5000 - runningFor);
} catch (InterruptedException e1) { // NOPMD
// Retry
}
logger.log(Level.FINE, () -> "Retrying");
}
}
private void handleVmDefinitionChange(V1APIResource vmsCrd,
Watch.Response<V1Namespace> vmDefRef) throws ApiException {
V1ObjectMeta metadata = vmDefRef.object.getMetadata();
VmChannel channel = channels.computeIfAbsent(metadata.getName(),
k -> {
try {
return new VmChannel(channel(), newEventPipeline(),
new K8sClient());
} catch (IOException e) {
logger.log(Level.SEVERE, e, () -> "Failed to create client"
+ " for handling changes: " + e.getMessage());
return null;
}
});
if (channel == null) {
return;
}
// Get full definition and associate with channel as backup
@SuppressWarnings("PMD.ShortVariable")
var gv = GroupVersion.parse(vmDefRef.object.getApiVersion());
var vmStub = K8sDynamicStub.get(channel.client(),
new GroupVersionKind(gv.getGroup(), gv.getVersion(), VM_OP_KIND_VM),
metadata.getNamespace(), metadata.getName());
vmStub.model().ifPresent(vmDef -> {
addDynamicData(channel.client(), vmDef);
channel.setVmDefinition(vmDef);
// Create and fire event
channel.pipeline().fire(new VmDefChanged(VmDefChanged.Type
.valueOf(vmDefRef.type),
channel
.setGeneration(
vmDefRef.object.getMetadata().getGeneration()),
vmsCrd, vmDef), channel);
});
}
private void addDynamicData(K8sClient client, K8sDynamicModel vmState) {
var rootNode = GsonPtr.to(vmState.data()).get(JsonObject.class);
rootNode.addProperty("nodeName", "");
// VM definition status changes before the pod terminates.
// This results in pod information being shown for a stopped
// VM which is irritating. So check condition first.
var isRunning = GsonPtr.to(rootNode).to("status", "conditions")
.get(JsonArray.class)
.asList().stream().filter(el -> "Running"
.equals(((JsonObject) el).get("type").getAsString()))
.findFirst().map(el -> "True"
.equals(((JsonObject) el).get("status").getAsString()))
.orElse(false);
if (!isRunning) {
return;
}
var podSearch = new ListOptions();
podSearch.setLabelSelector("app.kubernetes.io/name=" + APP_NAME
+ ",app.kubernetes.io/component=" + APP_NAME
+ ",app.kubernetes.io/instance=" + vmState.getMetadata().getName());
try {
var podList
= K8sV1PodStub.list(client, namespaceToWatch, podSearch);
for (var podStub : podList) {
rootNode.addProperty("nodeName",
podStub.model().get().getSpec().getNodeName());
}
} catch (ApiException e) {
logger.log(Level.WARNING, e,
() -> "Cannot access node information: " + e.getMessage());
}
}
/**
* Remove VM channel when VM is deleted.
*
* @param event the event
* @param channel the channel
*/
@Handler(priority = -10_000)
public void onVmDefChanged(VmDefChanged event, VmChannel channel) {
if (event.type() == Type.DELETED) {
channels.remove(event.vmDefinition().getMetadata().getName());
}
}
}

View file

@ -0,0 +1 @@
test-vm

View file

@ -25,8 +25,8 @@ import java.util.concurrent.ConcurrentHashMap;
import org.jdrupes.vmoperator.runner.qemu.commands.QmpChangeMedium;
import org.jdrupes.vmoperator.runner.qemu.commands.QmpOpenTray;
import org.jdrupes.vmoperator.runner.qemu.commands.QmpRemoveMedium;
import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu;
import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerConfigurationUpdate;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State;
import org.jdrupes.vmoperator.runner.qemu.events.TrayMovedEvent;
import org.jgrapes.core.Channel;
@ -68,7 +68,7 @@ public class CdMediaController extends Component {
@Handler
@SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition",
"PMD.AvoidInstantiatingObjectsInLoops" })
public void onConfigureQemu(RunnerConfigurationUpdate event) {
public void onConfigureQemu(ConfigureQemu event) {
if (event.state() == State.TERMINATING) {
return;
}

View file

@ -46,7 +46,7 @@ public class Configuration implements Dto {
@SuppressWarnings("PMD.FieldNamingConventions")
protected final Logger logger = Logger.getLogger(getClass().getName());
/** Configuration timestamp */
/** Configuration timestamp. */
public Instant asOf;
/** The data dir. */
@ -73,6 +73,9 @@ public class Configuration implements Dto {
/** The firmware vars. */
public Path firmwareVars;
/** The display password. */
public boolean hasDisplayPassword;
/** Optional cloud-init data. */
public CloudInit cloudInit;
@ -87,10 +90,16 @@ public class Configuration implements Dto {
* Subsection "cloud-init".
*/
public static class CloudInit implements Dto {
/** The meta data. */
@SuppressWarnings("PMD.UseConcurrentHashMap")
public Map<String, Object> metaData;
/** The user data. */
@SuppressWarnings("PMD.UseConcurrentHashMap")
public Map<String, Object> userData;
/** The network config. */
@SuppressWarnings("PMD.UseConcurrentHashMap")
public Map<String, Object> networkConfig;
}
@ -230,6 +239,8 @@ public class Configuration implements Dto {
* The Class Display.
*/
public static class Display implements Dto {
/** The spice. */
public Spice spice;
}

View file

@ -27,11 +27,11 @@ import java.util.Set;
import org.jdrupes.vmoperator.runner.qemu.commands.QmpAddCpu;
import org.jdrupes.vmoperator.runner.qemu.commands.QmpDelCpu;
import org.jdrupes.vmoperator.runner.qemu.commands.QmpQueryHotpluggableCpus;
import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu;
import org.jdrupes.vmoperator.runner.qemu.events.CpuAdded;
import org.jdrupes.vmoperator.runner.qemu.events.CpuDeleted;
import org.jdrupes.vmoperator.runner.qemu.events.HotpluggableCpuStatus;
import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerConfigurationUpdate;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Component;
@ -45,7 +45,7 @@ public class CpuController extends Component {
private Integer currentCpus;
private Integer desiredCpus;
private RunnerConfigurationUpdate suspendedConfigure;
private ConfigureQemu suspendedConfigure;
/**
* Instantiates a new CPU controller.
@ -62,7 +62,7 @@ public class CpuController extends Component {
* @param event the event
*/
@Handler
public void onConfigureQemu(RunnerConfigurationUpdate event) {
public void onConfigureQemu(ConfigureQemu event) {
if (event.state() == State.TERMINATING) {
return;
}

View file

@ -0,0 +1,117 @@
/*
* VM-Operator
* Copyright (C) 2023 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.runner.qemu;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
import java.util.logging.Level;
import org.jdrupes.vmoperator.runner.qemu.commands.QmpSetDisplayPassword;
import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu;
import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Component;
import org.jgrapes.core.annotation.Handler;
import org.jgrapes.util.events.FileChanged;
import org.jgrapes.util.events.WatchFile;
/**
* The Class DisplayController.
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class DisplayController extends Component {
public static final String DISPLAY_PASSWORD_FILE = "display-password";
private String currentPassword;
private String protocol;
private final Path configDir;
/**
* Instantiates a new Display controller.
*
* @param componentChannel the component channel
* @param configDir
*/
@SuppressWarnings("PMD.AssignmentToNonFinalStatic")
public DisplayController(Channel componentChannel, Path configDir) {
super(componentChannel);
this.configDir = configDir;
fire(new WatchFile(configDir.resolve(DISPLAY_PASSWORD_FILE)));
}
/**
* On configure qemu.
*
* @param event the event
*/
@Handler
public void onConfigureQemu(ConfigureQemu event) {
if (event.state() == State.TERMINATING) {
return;
}
protocol
= event.configuration().vm.display.spice != null ? "spice" : null;
updatePassword();
}
/**
* Watch for changes of the password file.
*
* @param event the event
*/
@Handler
@SuppressWarnings("PMD.EmptyCatchBlock")
public void onFileChanged(FileChanged event) {
if (event.path().equals(configDir.resolve(DISPLAY_PASSWORD_FILE))) {
updatePassword();
}
}
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
private void updatePassword() {
if (protocol == null) {
return;
}
String password;
Path dpPath = configDir.resolve(DISPLAY_PASSWORD_FILE);
if (dpPath.toFile().canRead()) {
logger.finer(() -> "Found display password");
try {
password = Files.readString(dpPath);
} catch (IOException e) {
logger.log(Level.WARNING, e, () -> "Cannot read display"
+ " password: " + e.getMessage());
return;
}
} else {
logger.finer(() -> "No display password");
return;
}
if (Objects.equals(this.currentPassword, password)) {
return;
}
logger.fine(() -> "Updating display password");
fire(new MonitorCommand(new QmpSetDisplayPassword(protocol, password)));
}
}

View file

@ -35,12 +35,12 @@ import java.util.logging.Level;
import org.jdrupes.vmoperator.runner.qemu.commands.QmpCapabilities;
import org.jdrupes.vmoperator.runner.qemu.commands.QmpCommand;
import org.jdrupes.vmoperator.runner.qemu.commands.QmpPowerdown;
import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu;
import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand;
import org.jdrupes.vmoperator.runner.qemu.events.MonitorEvent;
import org.jdrupes.vmoperator.runner.qemu.events.MonitorReady;
import org.jdrupes.vmoperator.runner.qemu.events.MonitorResult;
import org.jdrupes.vmoperator.runner.qemu.events.PowerdownEvent;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerConfigurationUpdate;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Component;
import org.jgrapes.core.Components;
@ -87,13 +87,16 @@ public class QemuMonitor extends Component {
* Instantiates a new qemu monitor.
*
* @param componentChannel the component channel
* @param configDir the config dir
* @throws IOException Signals that an I/O exception has occurred.
*/
@SuppressWarnings("PMD.AssignmentToNonFinalStatic")
public QemuMonitor(Channel componentChannel) throws IOException {
public QemuMonitor(Channel componentChannel, Path configDir)
throws IOException {
super(componentChannel);
attach(new RamController(channel()));
attach(new CpuController(channel()));
attach(new DisplayController(channel(), configDir));
attach(new CdMediaController(channel()));
}
@ -254,17 +257,18 @@ public class QemuMonitor extends Component {
* @param event the event
*/
@Handler
@SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
public void onExecQmpCommand(MonitorCommand event) {
var command = event.command();
logger.fine(() -> "monitor(out): " + command.toString());
String asText;
try {
asText = mapper.writeValueAsString(command.toJson());
asText = command.asText();
} catch (JsonProcessingException e) {
logger.log(Level.SEVERE, e,
() -> "Cannot serialize Json: " + e.getMessage());
return;
}
logger.fine(() -> "monitor(out): " + asText);
synchronized (executing) {
monitorChannel.associated(Writer.class).ifPresent(writer -> {
try {
@ -343,7 +347,7 @@ public class QemuMonitor extends Component {
* @param event the event
*/
@Handler
public void onConfigureQemu(RunnerConfigurationUpdate event) {
public void onConfigureQemu(ConfigureQemu event) {
int newTimeout = event.configuration().vm.powerdownTimeout;
if (powerdownTimeout != newTimeout) {
powerdownTimeout = newTimeout;

View file

@ -21,8 +21,8 @@ package org.jdrupes.vmoperator.runner.qemu;
import java.math.BigInteger;
import java.util.Optional;
import org.jdrupes.vmoperator.runner.qemu.commands.QmpSetBalloon;
import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu;
import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerConfigurationUpdate;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Component;
import org.jgrapes.core.annotation.Handler;
@ -50,7 +50,7 @@ public class RamController extends Component {
* @param event the event
*/
@Handler
public void onConfigureQemu(RunnerConfigurationUpdate event) {
public void onConfigureQemu(ConfigureQemu event) {
Optional.ofNullable(event.configuration().vm.currentRam)
.ifPresent(cr -> {
if (currentRam != null && currentRam.equals(cr)) {

View file

@ -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
@ -55,10 +55,10 @@ import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
import org.jdrupes.vmoperator.runner.qemu.commands.QmpCont;
import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu;
import org.jdrupes.vmoperator.runner.qemu.events.Exit;
import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand;
import org.jdrupes.vmoperator.runner.qemu.events.QmpConfigured;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerConfigurationUpdate;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State;
import org.jdrupes.vmoperator.util.ExtendedObjectWrapper;
@ -143,8 +143,8 @@ import org.jgrapes.util.events.WatchFile;
* waitForConfigured: entry/fire QmpCapabilities
* waitForConfigured --> configure: QmpConfigured
*
* configure: entry/fire RunnerConfigurationUpdate
* configure --> success: RunnerConfigurationUpdate (last handler)/fire cont command
* configure: entry/fire ConfigureQemu
* configure --> success: ConfigureQemu (last handler)/fire cont command
* }
*
* Initializing --> prepFork: Started
@ -207,6 +207,7 @@ public class Runner extends Component {
private final JsonNode defaults;
@SuppressWarnings("PMD.UseConcurrentHashMap")
private final File configFile;
private final Path configDir;
private Configuration config = new Configuration();
private final freemarker.template.Configuration fmConfig;
private CommandDefinition swtpmDefinition;
@ -240,6 +241,17 @@ public class Runner extends Component {
defaults = yamlMapper.readValue(
Runner.class.getResourceAsStream("defaults.yaml"), JsonNode.class);
// Get the config
configFile = new File(cmdLine.getOptionValue('c',
"/etc/opt/" + APP_NAME.replace("-", "") + "/config.yaml"));
// Don't rely on night config to produce a good exception
// for this simple case
if (!Files.isReadable(configFile.toPath())) {
throw new IOException(
"Cannot read configuration file " + configFile);
}
configDir = configFile.getParentFile().toPath().toRealPath();
// Configure freemarker library
fmConfig = new freemarker.template.Configuration(
freemarker.template.Configuration.VERSION_2_3_32);
@ -256,17 +268,8 @@ public class Runner extends Component {
attach(new FileSystemWatcher(channel()));
attach(new ProcessManager(channel()));
attach(new SocketConnector(channel()));
attach(qemuMonitor = new QemuMonitor(channel()));
attach(qemuMonitor = new QemuMonitor(channel(), configDir));
attach(new StatusUpdater(channel()));
configFile = new File(cmdLine.getOptionValue('c',
"/etc/opt/" + APP_NAME.replace("-", "") + "/config.yaml"));
// Don't rely on night config to produce a good exception
// for this simple case
if (!Files.isReadable(configFile.toPath())) {
throw new IOException(
"Cannot read configuration file " + configFile);
}
attach(new YamlConfigurationStore(channel(), configFile, false));
fire(new WatchFile(configFile.toPath()));
}
@ -294,13 +297,20 @@ public class Runner extends Component {
public void onConfigurationUpdate(ConfigurationUpdate event) {
event.structured(componentPath()).ifPresent(c -> {
var newConf = yamlMapper.convertValue(c, Configuration.class);
// Add some values from other sources to configuration
newConf.asOf = Instant.ofEpochSecond(configFile.lastModified());
Path dsPath
= configDir.resolve(DisplayController.DISPLAY_PASSWORD_FILE);
newConf.hasDisplayPassword = dsPath.toFile().canRead();
// Special actions for initial configuration (startup)
if (event instanceof InitialConfiguration) {
processInitialConfiguration(newConf);
return;
}
logger.fine(() -> "Updating configuration");
rep.fire(new RunnerConfigurationUpdate(newConf, state));
rep.fire(new ConfigureQemu(newConf, state));
});
}
@ -388,12 +398,9 @@ public class Runner extends Component {
.map(Object::toString).orElse(null));
model.put("firmwareVars", Optional.ofNullable(config.firmwareVars)
.map(Object::toString).orElse(null));
model.put("hasDisplayPassword", config.hasDisplayPassword);
model.put("cloudInit", config.cloudInit);
model.put("vm", config.vm);
if (Optional.ofNullable(config.vm.display)
.map(d -> d.spice).map(s -> s.ticket).isPresent()) {
model.put("ticketPath", config.runtimeDir.resolve("ticket.txt"));
}
// Combine template and data and parse result
// (tempting, but no need to use a pipe here)
@ -598,7 +605,7 @@ public class Runner extends Component {
*/
@Handler
public void onQmpConfigured(QmpConfigured event) {
rep.fire(new RunnerConfigurationUpdate(config, state));
rep.fire(new ConfigureQemu(config, state));
}
/**
@ -607,7 +614,7 @@ public class Runner extends Component {
* @param event the event
*/
@Handler(priority = -1000)
public void onConfigureQemu(RunnerConfigurationUpdate event) {
public void onConfigureQemu(ConfigureQemu event) {
if (state == State.STARTING) {
fire(new MonitorCommand(new QmpCont()));
state = State.RUNNING;

View file

@ -42,9 +42,9 @@ import org.jdrupes.vmoperator.common.K8sClient;
import org.jdrupes.vmoperator.common.K8sDynamicModel;
import org.jdrupes.vmoperator.common.K8sDynamicStub;
import org.jdrupes.vmoperator.runner.qemu.events.BalloonChangeEvent;
import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu;
import org.jdrupes.vmoperator.runner.qemu.events.Exit;
import org.jdrupes.vmoperator.runner.qemu.events.HotpluggableCpuStatus;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerConfigurationUpdate;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State;
import org.jdrupes.vmoperator.runner.qemu.events.ShutdownEvent;
@ -178,7 +178,7 @@ public class StatusUpdater extends Component {
* @throws ApiException
*/
@Handler
public void onRunnerConfigurationUpdate(RunnerConfigurationUpdate event)
public void onConfigureQemu(ConfigureQemu event)
throws ApiException {
guestShutdownStops = event.configuration().guestShutdownStops;

View file

@ -18,6 +18,7 @@
package org.jdrupes.vmoperator.runner.qemu.commands;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
@ -55,4 +56,30 @@ public abstract class QmpCommand {
* @return the json node
*/
public abstract JsonNode toJson();
/**
* Returns the string representation.
*
* @return the string
* @throws JsonProcessingException the JSON processing exception
*/
public String asText() throws JsonProcessingException {
return mapper.writeValueAsString(toJson());
}
/**
* Calls {@link #asText()} but suppresses the
* {@link JsonProcessingException}.
*
* @return the string
*/
@Override
public String toString() {
try {
return asText();
} catch (JsonProcessingException e) {
return "(no string representation)";
}
}
}

View file

@ -0,0 +1,68 @@
/*
* VM-Operator
* Copyright (C) 2023 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.runner.qemu.commands;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
/**
* A {@link QmpCommand} that sets the display password.
*/
public class QmpSetDisplayPassword extends QmpCommand {
private final String password;
private final String protocol;
/**
* Instantiates a new command.
*
* @param protocol the protocol
* @param password the password
*/
public QmpSetDisplayPassword(String protocol, String password) {
this.protocol = protocol;
this.password = password;
}
@Override
public JsonNode toJson() {
ObjectNode cmd = mapper.createObjectNode();
cmd.put("execute", "set_password");
ObjectNode args = mapper.createObjectNode();
cmd.set("arguments", args);
args.set("protocol", new TextNode(protocol));
args.set("password", new TextNode(password));
return cmd;
}
@Override
public String toString() {
try {
var json = toJson();
((ObjectNode) json.get("arguments")).set("password",
new TextNode("********"));
return mapper.writeValueAsString(json);
} catch (JsonProcessingException e) {
return "(no string representation)";
}
}
}

View file

@ -31,7 +31,7 @@ import org.jgrapes.core.Event;
* on the event and only {@link Event#resumeHandling() resume handling}
* when the adaption has completed.
*/
public class RunnerConfigurationUpdate extends Event<Void> {
public class ConfigureQemu extends Event<Void> {
private final Configuration configuration;
private final State state;
@ -41,7 +41,7 @@ public class RunnerConfigurationUpdate extends Event<Void> {
*
* @param channels the channels
*/
public RunnerConfigurationUpdate(Configuration configuration, State state,
public ConfigureQemu(Configuration configuration, State state,
Channel... channels) {
super(channels);
this.state = state;

View file

@ -215,12 +215,8 @@
<#assign spice = vm.display.spice/>
# SPICE (display, channels ...)
# https://www.linux-kvm.org/page/SPICE
<#if ticketPath??>
- [ "-object", "secret,id=spiceTicket,file=${ ticketPath }" ]
</#if>
- [ "-spice", "port=${ spice.port?c }\
<#if spice.ticket??>,password-secret=spiceTicket\
<#else>,disable-ticketing=on</#if>\
,disable-ticketing=<#if hasDisplayPassword!false>off<#else>on</#if>\
<#if spice.streamingVideo??>,streaming-video=${ spice.streamingVideo }</#if>\
,seamless-migration=on" ]
- [ "-chardev", "spicevmc,id=vdagentdev,name=vdagent" ]

View file

@ -31,17 +31,16 @@ import java.math.BigInteger;
import java.time.Duration;
import java.time.Instant;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.jdrupes.json.JsonBeanDecoder;
import org.jdrupes.json.JsonDecodeException;
import org.jdrupes.vmoperator.common.K8sDynamicModel;
import org.jdrupes.vmoperator.common.K8sObserver;
import org.jdrupes.vmoperator.manager.events.ChannelCache;
import org.jdrupes.vmoperator.manager.events.ModifyVm;
import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jdrupes.vmoperator.manager.events.VmDefChanged.Type;
import org.jdrupes.vmoperator.util.GsonPtr;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Event;
@ -69,10 +68,8 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
private static final Set<RenderMode> MODES = RenderMode.asSet(
RenderMode.Preview, RenderMode.View);
private final Map<String, K8sDynamicModel> vmInfos
= new ConcurrentHashMap<>();
private final Map<String, VmChannel> vmChannels
= new ConcurrentHashMap<>();
private final ChannelCache<String, VmChannel,
K8sDynamicModel> channelManager = new ChannelCache<>();
private final TimeSeries summarySeries = new TimeSeries(Duration.ofDays(1));
private Summary cachedSummary;
@ -162,9 +159,10 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
sendVmInfos = true;
}
if (sendVmInfos) {
for (var vmInfo : vmInfos.values()) {
var def = JsonBeanDecoder.create(vmInfo.data().toString())
.readObject();
for (var vmDef : channelManager.associated()) {
var def
= JsonBeanDecoder.create(vmDef.data().toString())
.readObject();
channel.respond(new NotifyConletView(type(),
conletId, "updateVm", def));
}
@ -187,9 +185,8 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
public void onVmDefChanged(VmDefChanged event, VmChannel channel)
throws JsonDecodeException, IOException {
var vmName = event.vmDefinition().getMetadata().getName();
if (event.type() == Type.DELETED) {
vmInfos.remove(vmName);
vmChannels.remove(vmName);
if (event.type() == K8sObserver.ResponseType.DELETED) {
channelManager.remove(vmName);
for (var entry : conletIdsByConsoleConnection().entrySet()) {
for (String conletId : entry.getValue()) {
entry.getKey().respond(new NotifyConletView(type(),
@ -199,8 +196,7 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
} else {
var vmDef = new K8sDynamicModel(channel.client().getJSON()
.getGson(), convertQuantities(event.vmDefinition().data()));
vmInfos.put(vmName, vmDef);
vmChannels.put(vmName, channel);
channelManager.put(vmName, channel, vmDef);
var def = JsonBeanDecoder.create(vmDef.data().toString())
.readObject();
for (var entry : conletIdsByConsoleConnection().entrySet()) {
@ -323,7 +319,7 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
return cachedSummary;
}
Summary summary = new Summary();
for (var vmDef : vmInfos.values()) {
for (var vmDef : channelManager.associated()) {
summary.totalVms += 1;
var status = GsonPtr.to(vmDef.data()).to("status");
summary.usedCpus += status.getAsInt("cpus").orElse(0);
@ -349,7 +345,7 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
throws Exception {
event.stop();
var vmName = event.params().asString(0);
var vmChannel = vmChannels.get(vmName);
var vmChannel = channelManager.channel(vmName).orElse(null);
if (vmChannel == null) {
return;
}