Support for display secrets (#21)
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
This commit is contained in:
parent
85b0a160f3
commit
3103452170
38 changed files with 2081 additions and 658 deletions
12
dev-example/test-vm-display-secret.yaml
Normal file
12
dev-example/test-vm-display-secret.yaml
Normal 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==
|
||||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
1
org.jdrupes.vmoperator.runner.qemu/display-password
Normal file
1
org.jdrupes.vmoperator.runner.qemu/display-password
Normal file
|
|
@ -0,0 +1 @@
|
|||
test-vm
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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)";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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" ]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue