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. */
|
/** The Constant APP_NAME. */
|
||||||
public static final String APP_NAME = "vm-runner";
|
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. */
|
/** The Constant VM_OP_NAME. */
|
||||||
public static final String VM_OP_NAME = "vm-operator";
|
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.Yaml;
|
||||||
import org.yaml.snakeyaml.constructor.SafeConstructor;
|
import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
|
|
||||||
|
// TODO: Auto-generated Javadoc
|
||||||
/**
|
/**
|
||||||
* Helpers for K8s API.
|
* Helpers for K8s API.
|
||||||
*/
|
*/
|
||||||
|
|
@ -74,6 +75,35 @@ public class K8s {
|
||||||
return Optional.empty();
|
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.
|
* Convert Yaml to Json.
|
||||||
*
|
*
|
||||||
|
|
@ -156,6 +186,7 @@ public class K8s {
|
||||||
* @param api the api
|
* @param api the api
|
||||||
* @param existing the existing
|
* @param existing the existing
|
||||||
* @param update the update
|
* @param update the update
|
||||||
|
* @return the t
|
||||||
* @throws ApiException the api exception
|
* @throws ApiException the api exception
|
||||||
*/
|
*/
|
||||||
public static <T extends KubernetesObject, LT extends KubernetesListObject>
|
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 `type` is not set, set it to "Normal"
|
||||||
* * If `regarding` is not set, set it to the given object.
|
* * If `regarding` is not set, set it to the given object.
|
||||||
*
|
*
|
||||||
|
* @param client the client
|
||||||
|
* @param object the object
|
||||||
* @param event the event
|
* @param event the event
|
||||||
* @throws ApiException
|
* @throws ApiException the api exception
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("PMD.NPathComplexity")
|
@SuppressWarnings("PMD.NPathComplexity")
|
||||||
public static void createEvent(ApiClient client,
|
public static void createEvent(ApiClient client,
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,14 @@
|
||||||
|
|
||||||
package org.jdrupes.vmoperator.common;
|
package org.jdrupes.vmoperator.common;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
import io.kubernetes.client.Discovery.APIResource;
|
import io.kubernetes.client.Discovery.APIResource;
|
||||||
import io.kubernetes.client.apimachinery.GroupVersionKind;
|
import io.kubernetes.client.apimachinery.GroupVersionKind;
|
||||||
|
import io.kubernetes.client.openapi.ApiClient;
|
||||||
import io.kubernetes.client.openapi.ApiException;
|
import io.kubernetes.client.openapi.ApiException;
|
||||||
|
import io.kubernetes.client.util.generic.options.ListOptions;
|
||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A stub for namespaced custom objects. It uses a dynamic model
|
* A stub for namespaced custom objects. It uses a dynamic model
|
||||||
|
|
@ -47,6 +51,24 @@ public class K8sDynamicStub
|
||||||
Class<K8sDynamicModels> objectListClass, K8sClient client,
|
Class<K8sDynamicModels> objectListClass, K8sClient client,
|
||||||
APIResource context, String namespace, String name) {
|
APIResource context, String namespace, String name) {
|
||||||
super(objectClass, objectListClass, client, context, namespace, 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",
|
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
|
||||||
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
|
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
|
||||||
public static K8sDynamicStub get(K8sClient client,
|
public static K8sDynamicStub get(K8sClient client,
|
||||||
APIResource context, String namespace, String name)
|
APIResource context, String namespace, String name) {
|
||||||
throws ApiException {
|
|
||||||
return K8sGenericStub.get(K8sDynamicModel.class, K8sDynamicModels.class,
|
return K8sGenericStub.get(K8sDynamicModel.class, K8sDynamicModels.class,
|
||||||
client, context, namespace, name, K8sDynamicStub::new);
|
client, context, namespace, name, K8sDynamicStub::new);
|
||||||
}
|
}
|
||||||
|
|
@ -106,4 +127,37 @@ public class K8sDynamicStub
|
||||||
K8sDynamicModels.class, client, context, model,
|
K8sDynamicModels.class, client, context, model,
|
||||||
K8sDynamicStub::new);
|
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;
|
package org.jdrupes.vmoperator.common;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import io.kubernetes.client.Discovery.APIResource;
|
import io.kubernetes.client.Discovery.APIResource;
|
||||||
import io.kubernetes.client.apimachinery.GroupVersionKind;
|
import io.kubernetes.client.apimachinery.GroupVersionKind;
|
||||||
import io.kubernetes.client.common.KubernetesListObject;
|
import io.kubernetes.client.common.KubernetesListObject;
|
||||||
import io.kubernetes.client.common.KubernetesObject;
|
import io.kubernetes.client.common.KubernetesObject;
|
||||||
import io.kubernetes.client.custom.V1Patch;
|
import io.kubernetes.client.custom.V1Patch;
|
||||||
import io.kubernetes.client.openapi.ApiClient;
|
|
||||||
import io.kubernetes.client.openapi.ApiException;
|
import io.kubernetes.client.openapi.ApiException;
|
||||||
import io.kubernetes.client.util.Strings;
|
import io.kubernetes.client.util.Strings;
|
||||||
import io.kubernetes.client.util.generic.GenericKubernetesApi;
|
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.ListOptions;
|
||||||
import io.kubernetes.client.util.generic.options.PatchOptions;
|
import io.kubernetes.client.util.generic.options.PatchOptions;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
|
@ -49,145 +50,16 @@ public class K8sGenericStub<O extends KubernetesObject,
|
||||||
L extends KubernetesListObject> {
|
L extends KubernetesListObject> {
|
||||||
protected final K8sClient client;
|
protected final K8sClient client;
|
||||||
private final GenericKubernetesApi<O, L> api;
|
private final GenericKubernetesApi<O, L> api;
|
||||||
protected final String group;
|
protected final APIResource context;
|
||||||
protected final String version;
|
|
||||||
protected final String kind;
|
|
||||||
protected final String plural;
|
|
||||||
protected final String namespace;
|
protected final String namespace;
|
||||||
protected final String name;
|
protected final String name;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a namespaced object stub. If the version in parameter
|
* Instantiates a new stub for the object specified. If the object
|
||||||
* `gvk` is an empty string, the stub refers to the first object
|
* exists in the context specified, the version (see
|
||||||
* found with matching group and kind.
|
* {@link #version()} is bound to the existing object's version.
|
||||||
*
|
* Else the stub is dangling with the version set to the context's
|
||||||
* @param <O> the object type
|
* preferred version.
|
||||||
* @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.
|
|
||||||
*
|
*
|
||||||
* @param objectClass the object class
|
* @param objectClass the object class
|
||||||
* @param objectListClass the object list class
|
* @param objectListClass the object list class
|
||||||
|
|
@ -196,35 +68,47 @@ public class K8sGenericStub<O extends KubernetesObject,
|
||||||
* @param namespace the namespace
|
* @param namespace the namespace
|
||||||
* @param name the name
|
* @param name the name
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
|
||||||
protected K8sGenericStub(Class<O> objectClass, Class<L> objectListClass,
|
protected K8sGenericStub(Class<O> objectClass, Class<L> objectListClass,
|
||||||
K8sClient client, APIResource context, String namespace,
|
K8sClient client, APIResource context, String namespace,
|
||||||
String name) {
|
String name) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
group = context.getGroup();
|
|
||||||
version = context.getPreferredVersion();
|
|
||||||
kind = context.getKind();
|
|
||||||
plural = context.getResourcePlural();
|
|
||||||
this.namespace = namespace;
|
this.namespace = namespace;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
|
||||||
Gson gson = client.getJSON().getGson();
|
// Bind version
|
||||||
if (!checkAdapters(client)) {
|
var foundVersion = context.getPreferredVersion();
|
||||||
client.getJSON().setGson(gson.newBuilder()
|
GenericKubernetesApi<O, L> testApi = null;
|
||||||
.registerTypeAdapterFactory(
|
GetOptions mdOpts
|
||||||
new K8sDynamicModelTypeAdapterFactory())
|
= new GetOptions().isPartialObjectMetadataRequest(true);
|
||||||
.create());
|
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,
|
if (foundVersion.equals(context.getPreferredVersion())) {
|
||||||
objectListClass, group, version, plural, client);
|
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
|
* Gets the context.
|
||||||
.equals(client.getJSON().getGson().getAdapter(K8sDynamicModel.class)
|
*
|
||||||
.getClass())
|
* @return the context
|
||||||
&& K8sDynamicModelTypeAdapterFactory.K8sDynamicModelsCreator.class
|
*/
|
||||||
.equals(client.getJSON().getGson()
|
public APIResource context() {
|
||||||
.getAdapter(K8sDynamicModels.class).getClass());
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -233,7 +117,7 @@ public class K8sGenericStub<O extends KubernetesObject,
|
||||||
* @return the group
|
* @return the group
|
||||||
*/
|
*/
|
||||||
public String group() {
|
public String group() {
|
||||||
return group;
|
return context.getGroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -242,7 +126,7 @@ public class K8sGenericStub<O extends KubernetesObject,
|
||||||
* @return the version
|
* @return the version
|
||||||
*/
|
*/
|
||||||
public String version() {
|
public String version() {
|
||||||
return version;
|
return context.getPreferredVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -251,7 +135,7 @@ public class K8sGenericStub<O extends KubernetesObject,
|
||||||
* @return the kind
|
* @return the kind
|
||||||
*/
|
*/
|
||||||
public String kind() {
|
public String kind() {
|
||||||
return kind;
|
return context.getKind();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -260,7 +144,7 @@ public class K8sGenericStub<O extends KubernetesObject,
|
||||||
* @return the plural
|
* @return the plural
|
||||||
*/
|
*/
|
||||||
public String 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);
|
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
|
@Override
|
||||||
@SuppressWarnings("PMD.UseLocaleWithCaseConversions")
|
@SuppressWarnings("PMD.UseLocaleWithCaseConversions")
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return (Strings.isNullOrEmpty(group) ? "" : group + "/")
|
return (Strings.isNullOrEmpty(group()) ? "" : group() + "/")
|
||||||
+ version.toUpperCase() + kind + " " + namespace + ":" + name;
|
+ 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
|
public class K8sV1ConfigMapStub
|
||||||
extends K8sGenericStub<V1ConfigMap, V1ConfigMapList> {
|
extends K8sGenericStub<V1ConfigMap, V1ConfigMapList> {
|
||||||
|
|
||||||
|
public static final APIResource CONTEXT = new APIResource("", List.of("v1"),
|
||||||
|
"v1", "ConfigMap", true, "configmaps", "configmap");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiates a new stub.
|
* Instantiates a new stub.
|
||||||
*
|
*
|
||||||
|
|
@ -40,9 +43,7 @@ public class K8sV1ConfigMapStub
|
||||||
protected K8sV1ConfigMapStub(K8sClient client, String namespace,
|
protected K8sV1ConfigMapStub(K8sClient client, String namespace,
|
||||||
String name) {
|
String name) {
|
||||||
super(V1ConfigMap.class, V1ConfigMapList.class, client,
|
super(V1ConfigMap.class, V1ConfigMapList.class, client,
|
||||||
new APIResource("", List.of("v1"), "v1", "ConfigMap", true,
|
CONTEXT, namespace, name);
|
||||||
"configmaps", "configmap"),
|
|
||||||
namespace, name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,10 @@ import java.util.Optional;
|
||||||
public class K8sV1DeploymentStub
|
public class K8sV1DeploymentStub
|
||||||
extends K8sGenericStub<V1Deployment, V1DeploymentList> {
|
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.
|
* Instantiates a new stub.
|
||||||
*
|
*
|
||||||
|
|
@ -43,22 +47,7 @@ public class K8sV1DeploymentStub
|
||||||
protected K8sV1DeploymentStub(K8sClient client, String namespace,
|
protected K8sV1DeploymentStub(K8sClient client, String namespace,
|
||||||
String name) {
|
String name) {
|
||||||
super(V1Deployment.class, V1DeploymentList.class, client,
|
super(V1Deployment.class, V1DeploymentList.class, client,
|
||||||
new APIResource("apps", List.of("v1"), "v1", "Pod", true,
|
CONTEXT, namespace, name);
|
||||||
"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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -74,4 +63,17 @@ public class K8sV1DeploymentStub
|
||||||
+ "\", \"value\": " + replicas + "}]"),
|
+ "\", \"value\": " + replicas + "}]"),
|
||||||
client.defaultPatchOptions());
|
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")
|
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
|
||||||
public class K8sV1PodStub extends K8sGenericStub<V1Pod, V1PodList> {
|
public class K8sV1PodStub extends K8sGenericStub<V1Pod, V1PodList> {
|
||||||
|
|
||||||
|
/** The pods' context. */
|
||||||
public static final APIResource CONTEXT
|
public static final APIResource CONTEXT
|
||||||
= new APIResource("", List.of("v1"), "v1", "Pod", true, "pods", "pod");
|
= 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,
|
public static Collection<K8sV1PodStub> list(K8sClient client,
|
||||||
String namespace, ListOptions options) throws ApiException {
|
String namespace, ListOptions options) throws ApiException {
|
||||||
return K8sGenericStub.list(V1Pod.class, V1PodList.class, client,
|
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
|
public class K8sV1StatefulSetStub
|
||||||
extends K8sGenericStub<V1StatefulSet, V1StatefulSetList> {
|
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.
|
* Instantiates a new stub.
|
||||||
*
|
*
|
||||||
|
|
@ -39,9 +44,7 @@ public class K8sV1StatefulSetStub
|
||||||
*/
|
*/
|
||||||
protected K8sV1StatefulSetStub(K8sClient client, String namespace,
|
protected K8sV1StatefulSetStub(K8sClient client, String namespace,
|
||||||
String name) {
|
String name) {
|
||||||
super(V1StatefulSet.class, V1StatefulSetList.class, client,
|
super(V1StatefulSet.class, V1StatefulSetList.class, client, CONTEXT,
|
||||||
new APIResource("apps", List.of("v1"), "v1", "StatefulSet", true,
|
|
||||||
"statefulsets", "statefulset"),
|
|
||||||
namespace, name);
|
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;
|
package org.jdrupes.vmoperator.manager.events;
|
||||||
|
|
||||||
import io.kubernetes.client.openapi.models.V1APIResource;
|
|
||||||
import org.jdrupes.vmoperator.common.K8sDynamicModel;
|
import org.jdrupes.vmoperator.common.K8sDynamicModel;
|
||||||
|
import org.jdrupes.vmoperator.common.K8sObserver;
|
||||||
import org.jgrapes.core.Channel;
|
import org.jgrapes.core.Channel;
|
||||||
import org.jgrapes.core.Components;
|
import org.jgrapes.core.Components;
|
||||||
import org.jgrapes.core.Event;
|
import org.jgrapes.core.Event;
|
||||||
|
|
@ -34,16 +34,8 @@ import org.jgrapes.core.Event;
|
||||||
@SuppressWarnings("PMD.DataClass")
|
@SuppressWarnings("PMD.DataClass")
|
||||||
public class VmDefChanged extends Event<Void> {
|
public class VmDefChanged extends Event<Void> {
|
||||||
|
|
||||||
/**
|
private final K8sObserver.ResponseType type;
|
||||||
* The type of change.
|
|
||||||
*/
|
|
||||||
public enum Type {
|
|
||||||
ADDED, MODIFIED, DELETED
|
|
||||||
}
|
|
||||||
|
|
||||||
private final Type type;
|
|
||||||
private final boolean specChanged;
|
private final boolean specChanged;
|
||||||
private final V1APIResource crd;
|
|
||||||
private final K8sDynamicModel vmDef;
|
private final K8sDynamicModel vmDef;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -51,14 +43,12 @@ public class VmDefChanged extends Event<Void> {
|
||||||
*
|
*
|
||||||
* @param type the type
|
* @param type the type
|
||||||
* @param specChanged the spec part changed
|
* @param specChanged the spec part changed
|
||||||
* @param crd the crd
|
|
||||||
* @param vmDefinition the VM definition
|
* @param vmDefinition the VM definition
|
||||||
*/
|
*/
|
||||||
public VmDefChanged(Type type, boolean specChanged, V1APIResource crd,
|
public VmDefChanged(K8sObserver.ResponseType type, boolean specChanged,
|
||||||
K8sDynamicModel vmDefinition) {
|
K8sDynamicModel vmDefinition) {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.specChanged = specChanged;
|
this.specChanged = specChanged;
|
||||||
this.crd = crd;
|
|
||||||
this.vmDef = vmDefinition;
|
this.vmDef = vmDefinition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -67,7 +57,7 @@ public class VmDefChanged extends Event<Void> {
|
||||||
*
|
*
|
||||||
* @return the type
|
* @return the type
|
||||||
*/
|
*/
|
||||||
public Type type() {
|
public K8sObserver.ResponseType type() {
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,15 +68,6 @@ public class VmDefChanged extends Event<Void> {
|
||||||
return specChanged;
|
return specChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the Crd.
|
|
||||||
*
|
|
||||||
* @return the v 1 API resource
|
|
||||||
*/
|
|
||||||
public V1APIResource crd() {
|
|
||||||
return crd;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the object.
|
* Returns the object.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -126,8 +126,14 @@ spec:
|
||||||
# hostPath:
|
# hostPath:
|
||||||
# path: /sys/fs/cgroup
|
# path: /sys/fs/cgroup
|
||||||
- name: config
|
- name: config
|
||||||
configMap:
|
projected:
|
||||||
name: ${ cr.metadata.name.asString }
|
sources:
|
||||||
|
- configMap:
|
||||||
|
name: ${ cr.metadata.name.asString }
|
||||||
|
<#if displaySecret??>
|
||||||
|
- secret:
|
||||||
|
name: ${ displaySecret }
|
||||||
|
</#if>
|
||||||
- name: vmop-image-repository
|
- name: vmop-image-repository
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
claimName: vmop-image-repository
|
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 {
|
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. */
|
/** The Constant STATE_RUNNING. */
|
||||||
public static final String STATE_RUNNING = "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 static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM;
|
||||||
import org.jdrupes.vmoperator.common.K8sClient;
|
import org.jdrupes.vmoperator.common.K8sClient;
|
||||||
import org.jdrupes.vmoperator.common.K8sDynamicStub;
|
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.Exit;
|
||||||
import org.jdrupes.vmoperator.manager.events.ModifyVm;
|
import org.jdrupes.vmoperator.manager.events.ModifyVm;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
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).
|
* [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
|
* 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
|
* the VM definitions (CRs) and generates {@link VmDefChanged} events
|
||||||
* when they change. The latter handles the changes and reconciles the
|
* when they change. The latter handles the changes and reconciles the
|
||||||
* resources in the cluster.
|
* resources in the cluster.
|
||||||
|
|
@ -87,7 +88,20 @@ public class Controller extends Component {
|
||||||
public Controller(Channel componentChannel) {
|
public Controller(Channel componentChannel) {
|
||||||
super(componentChannel);
|
super(componentChannel);
|
||||||
// Prepare component tree
|
// 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()));
|
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 java.util.Optional;
|
||||||
import org.jdrupes.vmoperator.common.Convertions;
|
import org.jdrupes.vmoperator.common.Convertions;
|
||||||
import org.jdrupes.vmoperator.common.K8sDynamicModel;
|
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.VmChannel;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
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.ExtendedObjectWrapper;
|
||||||
import org.jdrupes.vmoperator.util.GsonPtr;
|
import org.jdrupes.vmoperator.util.GsonPtr;
|
||||||
import org.jgrapes.core.Channel;
|
import org.jgrapes.core.Channel;
|
||||||
|
|
@ -194,7 +194,7 @@ public class Reconciler extends Component {
|
||||||
|
|
||||||
// Ownership relationships takes care of deletions
|
// Ownership relationships takes care of deletions
|
||||||
var defMeta = event.vmDefinition().getMetadata();
|
var defMeta = event.vmDefinition().getMetadata();
|
||||||
if (event.type() == Type.DELETED) {
|
if (event.type() == K8sObserver.ResponseType.DELETED) {
|
||||||
logger.fine(() -> "VM \"" + defMeta.getName() + "\" deleted");
|
logger.fine(() -> "VM \"" + defMeta.getName() + "\" deleted");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import java.io.IOException;
|
||||||
import java.io.StringWriter;
|
import java.io.StringWriter;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
import org.jdrupes.vmoperator.common.K8sV1SecretStub;
|
||||||
import org.jdrupes.vmoperator.common.K8sV1StatefulSetStub;
|
import org.jdrupes.vmoperator.common.K8sV1StatefulSetStub;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
||||||
|
|
@ -69,6 +70,13 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
throws IOException, TemplateException, ApiException {
|
throws IOException, TemplateException, ApiException {
|
||||||
var metadata = event.vmDefinition().getMetadata();
|
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
|
// Combine template and data and parse result
|
||||||
var fmTemplate = fmConfig.getTemplate("runnerSts.ftl.yaml");
|
var fmTemplate = fmConfig.getTemplate("runnerSts.ftl.yaml");
|
||||||
StringWriter out = new StringWriter();
|
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.QmpChangeMedium;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.commands.QmpOpenTray;
|
import org.jdrupes.vmoperator.runner.qemu.commands.QmpOpenTray;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.commands.QmpRemoveMedium;
|
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.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.RunnerStateChange.State;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.events.TrayMovedEvent;
|
import org.jdrupes.vmoperator.runner.qemu.events.TrayMovedEvent;
|
||||||
import org.jgrapes.core.Channel;
|
import org.jgrapes.core.Channel;
|
||||||
|
|
@ -68,7 +68,7 @@ public class CdMediaController extends Component {
|
||||||
@Handler
|
@Handler
|
||||||
@SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition",
|
@SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition",
|
||||||
"PMD.AvoidInstantiatingObjectsInLoops" })
|
"PMD.AvoidInstantiatingObjectsInLoops" })
|
||||||
public void onConfigureQemu(RunnerConfigurationUpdate event) {
|
public void onConfigureQemu(ConfigureQemu event) {
|
||||||
if (event.state() == State.TERMINATING) {
|
if (event.state() == State.TERMINATING) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ public class Configuration implements Dto {
|
||||||
@SuppressWarnings("PMD.FieldNamingConventions")
|
@SuppressWarnings("PMD.FieldNamingConventions")
|
||||||
protected final Logger logger = Logger.getLogger(getClass().getName());
|
protected final Logger logger = Logger.getLogger(getClass().getName());
|
||||||
|
|
||||||
/** Configuration timestamp */
|
/** Configuration timestamp. */
|
||||||
public Instant asOf;
|
public Instant asOf;
|
||||||
|
|
||||||
/** The data dir. */
|
/** The data dir. */
|
||||||
|
|
@ -73,6 +73,9 @@ public class Configuration implements Dto {
|
||||||
/** The firmware vars. */
|
/** The firmware vars. */
|
||||||
public Path firmwareVars;
|
public Path firmwareVars;
|
||||||
|
|
||||||
|
/** The display password. */
|
||||||
|
public boolean hasDisplayPassword;
|
||||||
|
|
||||||
/** Optional cloud-init data. */
|
/** Optional cloud-init data. */
|
||||||
public CloudInit cloudInit;
|
public CloudInit cloudInit;
|
||||||
|
|
||||||
|
|
@ -87,10 +90,16 @@ public class Configuration implements Dto {
|
||||||
* Subsection "cloud-init".
|
* Subsection "cloud-init".
|
||||||
*/
|
*/
|
||||||
public static class CloudInit implements Dto {
|
public static class CloudInit implements Dto {
|
||||||
|
|
||||||
|
/** The meta data. */
|
||||||
@SuppressWarnings("PMD.UseConcurrentHashMap")
|
@SuppressWarnings("PMD.UseConcurrentHashMap")
|
||||||
public Map<String, Object> metaData;
|
public Map<String, Object> metaData;
|
||||||
|
|
||||||
|
/** The user data. */
|
||||||
@SuppressWarnings("PMD.UseConcurrentHashMap")
|
@SuppressWarnings("PMD.UseConcurrentHashMap")
|
||||||
public Map<String, Object> userData;
|
public Map<String, Object> userData;
|
||||||
|
|
||||||
|
/** The network config. */
|
||||||
@SuppressWarnings("PMD.UseConcurrentHashMap")
|
@SuppressWarnings("PMD.UseConcurrentHashMap")
|
||||||
public Map<String, Object> networkConfig;
|
public Map<String, Object> networkConfig;
|
||||||
}
|
}
|
||||||
|
|
@ -230,6 +239,8 @@ public class Configuration implements Dto {
|
||||||
* The Class Display.
|
* The Class Display.
|
||||||
*/
|
*/
|
||||||
public static class Display implements Dto {
|
public static class Display implements Dto {
|
||||||
|
|
||||||
|
/** The spice. */
|
||||||
public Spice 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.QmpAddCpu;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.commands.QmpDelCpu;
|
import org.jdrupes.vmoperator.runner.qemu.commands.QmpDelCpu;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.commands.QmpQueryHotpluggableCpus;
|
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.CpuAdded;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.events.CpuDeleted;
|
import org.jdrupes.vmoperator.runner.qemu.events.CpuDeleted;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.events.HotpluggableCpuStatus;
|
import org.jdrupes.vmoperator.runner.qemu.events.HotpluggableCpuStatus;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand;
|
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.RunnerStateChange.State;
|
||||||
import org.jgrapes.core.Channel;
|
import org.jgrapes.core.Channel;
|
||||||
import org.jgrapes.core.Component;
|
import org.jgrapes.core.Component;
|
||||||
|
|
@ -45,7 +45,7 @@ public class CpuController extends Component {
|
||||||
|
|
||||||
private Integer currentCpus;
|
private Integer currentCpus;
|
||||||
private Integer desiredCpus;
|
private Integer desiredCpus;
|
||||||
private RunnerConfigurationUpdate suspendedConfigure;
|
private ConfigureQemu suspendedConfigure;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiates a new CPU controller.
|
* Instantiates a new CPU controller.
|
||||||
|
|
@ -62,7 +62,7 @@ public class CpuController extends Component {
|
||||||
* @param event the event
|
* @param event the event
|
||||||
*/
|
*/
|
||||||
@Handler
|
@Handler
|
||||||
public void onConfigureQemu(RunnerConfigurationUpdate event) {
|
public void onConfigureQemu(ConfigureQemu event) {
|
||||||
if (event.state() == State.TERMINATING) {
|
if (event.state() == State.TERMINATING) {
|
||||||
return;
|
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.QmpCapabilities;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.commands.QmpCommand;
|
import org.jdrupes.vmoperator.runner.qemu.commands.QmpCommand;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.commands.QmpPowerdown;
|
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.MonitorCommand;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.events.MonitorEvent;
|
import org.jdrupes.vmoperator.runner.qemu.events.MonitorEvent;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.events.MonitorReady;
|
import org.jdrupes.vmoperator.runner.qemu.events.MonitorReady;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.events.MonitorResult;
|
import org.jdrupes.vmoperator.runner.qemu.events.MonitorResult;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.events.PowerdownEvent;
|
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.Channel;
|
||||||
import org.jgrapes.core.Component;
|
import org.jgrapes.core.Component;
|
||||||
import org.jgrapes.core.Components;
|
import org.jgrapes.core.Components;
|
||||||
|
|
@ -87,13 +87,16 @@ public class QemuMonitor extends Component {
|
||||||
* Instantiates a new qemu monitor.
|
* Instantiates a new qemu monitor.
|
||||||
*
|
*
|
||||||
* @param componentChannel the component channel
|
* @param componentChannel the component channel
|
||||||
|
* @param configDir the config dir
|
||||||
* @throws IOException Signals that an I/O exception has occurred.
|
* @throws IOException Signals that an I/O exception has occurred.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("PMD.AssignmentToNonFinalStatic")
|
@SuppressWarnings("PMD.AssignmentToNonFinalStatic")
|
||||||
public QemuMonitor(Channel componentChannel) throws IOException {
|
public QemuMonitor(Channel componentChannel, Path configDir)
|
||||||
|
throws IOException {
|
||||||
super(componentChannel);
|
super(componentChannel);
|
||||||
attach(new RamController(channel()));
|
attach(new RamController(channel()));
|
||||||
attach(new CpuController(channel()));
|
attach(new CpuController(channel()));
|
||||||
|
attach(new DisplayController(channel(), configDir));
|
||||||
attach(new CdMediaController(channel()));
|
attach(new CdMediaController(channel()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -254,17 +257,18 @@ public class QemuMonitor extends Component {
|
||||||
* @param event the event
|
* @param event the event
|
||||||
*/
|
*/
|
||||||
@Handler
|
@Handler
|
||||||
|
@SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
|
||||||
public void onExecQmpCommand(MonitorCommand event) {
|
public void onExecQmpCommand(MonitorCommand event) {
|
||||||
var command = event.command();
|
var command = event.command();
|
||||||
|
logger.fine(() -> "monitor(out): " + command.toString());
|
||||||
String asText;
|
String asText;
|
||||||
try {
|
try {
|
||||||
asText = mapper.writeValueAsString(command.toJson());
|
asText = command.asText();
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
logger.log(Level.SEVERE, e,
|
logger.log(Level.SEVERE, e,
|
||||||
() -> "Cannot serialize Json: " + e.getMessage());
|
() -> "Cannot serialize Json: " + e.getMessage());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logger.fine(() -> "monitor(out): " + asText);
|
|
||||||
synchronized (executing) {
|
synchronized (executing) {
|
||||||
monitorChannel.associated(Writer.class).ifPresent(writer -> {
|
monitorChannel.associated(Writer.class).ifPresent(writer -> {
|
||||||
try {
|
try {
|
||||||
|
|
@ -343,7 +347,7 @@ public class QemuMonitor extends Component {
|
||||||
* @param event the event
|
* @param event the event
|
||||||
*/
|
*/
|
||||||
@Handler
|
@Handler
|
||||||
public void onConfigureQemu(RunnerConfigurationUpdate event) {
|
public void onConfigureQemu(ConfigureQemu event) {
|
||||||
int newTimeout = event.configuration().vm.powerdownTimeout;
|
int newTimeout = event.configuration().vm.powerdownTimeout;
|
||||||
if (powerdownTimeout != newTimeout) {
|
if (powerdownTimeout != newTimeout) {
|
||||||
powerdownTimeout = newTimeout;
|
powerdownTimeout = newTimeout;
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ package org.jdrupes.vmoperator.runner.qemu;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.commands.QmpSetBalloon;
|
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.MonitorCommand;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.events.RunnerConfigurationUpdate;
|
|
||||||
import org.jgrapes.core.Channel;
|
import org.jgrapes.core.Channel;
|
||||||
import org.jgrapes.core.Component;
|
import org.jgrapes.core.Component;
|
||||||
import org.jgrapes.core.annotation.Handler;
|
import org.jgrapes.core.annotation.Handler;
|
||||||
|
|
@ -50,7 +50,7 @@ public class RamController extends Component {
|
||||||
* @param event the event
|
* @param event the event
|
||||||
*/
|
*/
|
||||||
@Handler
|
@Handler
|
||||||
public void onConfigureQemu(RunnerConfigurationUpdate event) {
|
public void onConfigureQemu(ConfigureQemu event) {
|
||||||
Optional.ofNullable(event.configuration().vm.currentRam)
|
Optional.ofNullable(event.configuration().vm.currentRam)
|
||||||
.ifPresent(cr -> {
|
.ifPresent(cr -> {
|
||||||
if (currentRam != null && currentRam.equals(cr)) {
|
if (currentRam != null && currentRam.equals(cr)) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* VM-Operator
|
* 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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as
|
* 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 org.apache.commons.cli.Options;
|
||||||
import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
|
import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.commands.QmpCont;
|
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.Exit;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand;
|
import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.events.QmpConfigured;
|
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;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State;
|
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State;
|
||||||
import org.jdrupes.vmoperator.util.ExtendedObjectWrapper;
|
import org.jdrupes.vmoperator.util.ExtendedObjectWrapper;
|
||||||
|
|
@ -143,8 +143,8 @@ import org.jgrapes.util.events.WatchFile;
|
||||||
* waitForConfigured: entry/fire QmpCapabilities
|
* waitForConfigured: entry/fire QmpCapabilities
|
||||||
* waitForConfigured --> configure: QmpConfigured
|
* waitForConfigured --> configure: QmpConfigured
|
||||||
*
|
*
|
||||||
* configure: entry/fire RunnerConfigurationUpdate
|
* configure: entry/fire ConfigureQemu
|
||||||
* configure --> success: RunnerConfigurationUpdate (last handler)/fire cont command
|
* configure --> success: ConfigureQemu (last handler)/fire cont command
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* Initializing --> prepFork: Started
|
* Initializing --> prepFork: Started
|
||||||
|
|
@ -207,6 +207,7 @@ public class Runner extends Component {
|
||||||
private final JsonNode defaults;
|
private final JsonNode defaults;
|
||||||
@SuppressWarnings("PMD.UseConcurrentHashMap")
|
@SuppressWarnings("PMD.UseConcurrentHashMap")
|
||||||
private final File configFile;
|
private final File configFile;
|
||||||
|
private final Path configDir;
|
||||||
private Configuration config = new Configuration();
|
private Configuration config = new Configuration();
|
||||||
private final freemarker.template.Configuration fmConfig;
|
private final freemarker.template.Configuration fmConfig;
|
||||||
private CommandDefinition swtpmDefinition;
|
private CommandDefinition swtpmDefinition;
|
||||||
|
|
@ -240,6 +241,17 @@ public class Runner extends Component {
|
||||||
defaults = yamlMapper.readValue(
|
defaults = yamlMapper.readValue(
|
||||||
Runner.class.getResourceAsStream("defaults.yaml"), JsonNode.class);
|
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
|
// Configure freemarker library
|
||||||
fmConfig = new freemarker.template.Configuration(
|
fmConfig = new freemarker.template.Configuration(
|
||||||
freemarker.template.Configuration.VERSION_2_3_32);
|
freemarker.template.Configuration.VERSION_2_3_32);
|
||||||
|
|
@ -256,17 +268,8 @@ public class Runner extends Component {
|
||||||
attach(new FileSystemWatcher(channel()));
|
attach(new FileSystemWatcher(channel()));
|
||||||
attach(new ProcessManager(channel()));
|
attach(new ProcessManager(channel()));
|
||||||
attach(new SocketConnector(channel()));
|
attach(new SocketConnector(channel()));
|
||||||
attach(qemuMonitor = new QemuMonitor(channel()));
|
attach(qemuMonitor = new QemuMonitor(channel(), configDir));
|
||||||
attach(new StatusUpdater(channel()));
|
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));
|
attach(new YamlConfigurationStore(channel(), configFile, false));
|
||||||
fire(new WatchFile(configFile.toPath()));
|
fire(new WatchFile(configFile.toPath()));
|
||||||
}
|
}
|
||||||
|
|
@ -294,13 +297,20 @@ public class Runner extends Component {
|
||||||
public void onConfigurationUpdate(ConfigurationUpdate event) {
|
public void onConfigurationUpdate(ConfigurationUpdate event) {
|
||||||
event.structured(componentPath()).ifPresent(c -> {
|
event.structured(componentPath()).ifPresent(c -> {
|
||||||
var newConf = yamlMapper.convertValue(c, Configuration.class);
|
var newConf = yamlMapper.convertValue(c, Configuration.class);
|
||||||
|
|
||||||
|
// Add some values from other sources to configuration
|
||||||
newConf.asOf = Instant.ofEpochSecond(configFile.lastModified());
|
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) {
|
if (event instanceof InitialConfiguration) {
|
||||||
processInitialConfiguration(newConf);
|
processInitialConfiguration(newConf);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logger.fine(() -> "Updating configuration");
|
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));
|
.map(Object::toString).orElse(null));
|
||||||
model.put("firmwareVars", Optional.ofNullable(config.firmwareVars)
|
model.put("firmwareVars", Optional.ofNullable(config.firmwareVars)
|
||||||
.map(Object::toString).orElse(null));
|
.map(Object::toString).orElse(null));
|
||||||
|
model.put("hasDisplayPassword", config.hasDisplayPassword);
|
||||||
model.put("cloudInit", config.cloudInit);
|
model.put("cloudInit", config.cloudInit);
|
||||||
model.put("vm", config.vm);
|
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
|
// Combine template and data and parse result
|
||||||
// (tempting, but no need to use a pipe here)
|
// (tempting, but no need to use a pipe here)
|
||||||
|
|
@ -598,7 +605,7 @@ public class Runner extends Component {
|
||||||
*/
|
*/
|
||||||
@Handler
|
@Handler
|
||||||
public void onQmpConfigured(QmpConfigured event) {
|
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
|
* @param event the event
|
||||||
*/
|
*/
|
||||||
@Handler(priority = -1000)
|
@Handler(priority = -1000)
|
||||||
public void onConfigureQemu(RunnerConfigurationUpdate event) {
|
public void onConfigureQemu(ConfigureQemu event) {
|
||||||
if (state == State.STARTING) {
|
if (state == State.STARTING) {
|
||||||
fire(new MonitorCommand(new QmpCont()));
|
fire(new MonitorCommand(new QmpCont()));
|
||||||
state = State.RUNNING;
|
state = State.RUNNING;
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,9 @@ import org.jdrupes.vmoperator.common.K8sClient;
|
||||||
import org.jdrupes.vmoperator.common.K8sDynamicModel;
|
import org.jdrupes.vmoperator.common.K8sDynamicModel;
|
||||||
import org.jdrupes.vmoperator.common.K8sDynamicStub;
|
import org.jdrupes.vmoperator.common.K8sDynamicStub;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.events.BalloonChangeEvent;
|
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.Exit;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.events.HotpluggableCpuStatus;
|
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;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State;
|
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.events.ShutdownEvent;
|
import org.jdrupes.vmoperator.runner.qemu.events.ShutdownEvent;
|
||||||
|
|
@ -178,7 +178,7 @@ public class StatusUpdater extends Component {
|
||||||
* @throws ApiException
|
* @throws ApiException
|
||||||
*/
|
*/
|
||||||
@Handler
|
@Handler
|
||||||
public void onRunnerConfigurationUpdate(RunnerConfigurationUpdate event)
|
public void onConfigureQemu(ConfigureQemu event)
|
||||||
throws ApiException {
|
throws ApiException {
|
||||||
guestShutdownStops = event.configuration().guestShutdownStops;
|
guestShutdownStops = event.configuration().guestShutdownStops;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
package org.jdrupes.vmoperator.runner.qemu.commands;
|
package org.jdrupes.vmoperator.runner.qemu.commands;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
@ -55,4 +56,30 @@ public abstract class QmpCommand {
|
||||||
* @return the json node
|
* @return the json node
|
||||||
*/
|
*/
|
||||||
public abstract JsonNode toJson();
|
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}
|
* on the event and only {@link Event#resumeHandling() resume handling}
|
||||||
* when the adaption has completed.
|
* when the adaption has completed.
|
||||||
*/
|
*/
|
||||||
public class RunnerConfigurationUpdate extends Event<Void> {
|
public class ConfigureQemu extends Event<Void> {
|
||||||
|
|
||||||
private final Configuration configuration;
|
private final Configuration configuration;
|
||||||
private final State state;
|
private final State state;
|
||||||
|
|
@ -41,7 +41,7 @@ public class RunnerConfigurationUpdate extends Event<Void> {
|
||||||
*
|
*
|
||||||
* @param channels the channels
|
* @param channels the channels
|
||||||
*/
|
*/
|
||||||
public RunnerConfigurationUpdate(Configuration configuration, State state,
|
public ConfigureQemu(Configuration configuration, State state,
|
||||||
Channel... channels) {
|
Channel... channels) {
|
||||||
super(channels);
|
super(channels);
|
||||||
this.state = state;
|
this.state = state;
|
||||||
|
|
@ -215,12 +215,8 @@
|
||||||
<#assign spice = vm.display.spice/>
|
<#assign spice = vm.display.spice/>
|
||||||
# SPICE (display, channels ...)
|
# SPICE (display, channels ...)
|
||||||
# https://www.linux-kvm.org/page/SPICE
|
# https://www.linux-kvm.org/page/SPICE
|
||||||
<#if ticketPath??>
|
|
||||||
- [ "-object", "secret,id=spiceTicket,file=${ ticketPath }" ]
|
|
||||||
</#if>
|
|
||||||
- [ "-spice", "port=${ spice.port?c }\
|
- [ "-spice", "port=${ spice.port?c }\
|
||||||
<#if spice.ticket??>,password-secret=spiceTicket\
|
,disable-ticketing=<#if hasDisplayPassword!false>off<#else>on</#if>\
|
||||||
<#else>,disable-ticketing=on</#if>\
|
|
||||||
<#if spice.streamingVideo??>,streaming-video=${ spice.streamingVideo }</#if>\
|
<#if spice.streamingVideo??>,streaming-video=${ spice.streamingVideo }</#if>\
|
||||||
,seamless-migration=on" ]
|
,seamless-migration=on" ]
|
||||||
- [ "-chardev", "spicevmc,id=vdagentdev,name=vdagent" ]
|
- [ "-chardev", "spicevmc,id=vdagentdev,name=vdagent" ]
|
||||||
|
|
|
||||||
|
|
@ -31,17 +31,16 @@ import java.math.BigInteger;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import org.jdrupes.json.JsonBeanDecoder;
|
import org.jdrupes.json.JsonBeanDecoder;
|
||||||
import org.jdrupes.json.JsonDecodeException;
|
import org.jdrupes.json.JsonDecodeException;
|
||||||
import org.jdrupes.vmoperator.common.K8sDynamicModel;
|
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.ModifyVm;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmDefChanged.Type;
|
|
||||||
import org.jdrupes.vmoperator.util.GsonPtr;
|
import org.jdrupes.vmoperator.util.GsonPtr;
|
||||||
import org.jgrapes.core.Channel;
|
import org.jgrapes.core.Channel;
|
||||||
import org.jgrapes.core.Event;
|
import org.jgrapes.core.Event;
|
||||||
|
|
@ -69,10 +68,8 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
|
|
||||||
private static final Set<RenderMode> MODES = RenderMode.asSet(
|
private static final Set<RenderMode> MODES = RenderMode.asSet(
|
||||||
RenderMode.Preview, RenderMode.View);
|
RenderMode.Preview, RenderMode.View);
|
||||||
private final Map<String, K8sDynamicModel> vmInfos
|
private final ChannelCache<String, VmChannel,
|
||||||
= new ConcurrentHashMap<>();
|
K8sDynamicModel> channelManager = new ChannelCache<>();
|
||||||
private final Map<String, VmChannel> vmChannels
|
|
||||||
= new ConcurrentHashMap<>();
|
|
||||||
private final TimeSeries summarySeries = new TimeSeries(Duration.ofDays(1));
|
private final TimeSeries summarySeries = new TimeSeries(Duration.ofDays(1));
|
||||||
private Summary cachedSummary;
|
private Summary cachedSummary;
|
||||||
|
|
||||||
|
|
@ -162,9 +159,10 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
sendVmInfos = true;
|
sendVmInfos = true;
|
||||||
}
|
}
|
||||||
if (sendVmInfos) {
|
if (sendVmInfos) {
|
||||||
for (var vmInfo : vmInfos.values()) {
|
for (var vmDef : channelManager.associated()) {
|
||||||
var def = JsonBeanDecoder.create(vmInfo.data().toString())
|
var def
|
||||||
.readObject();
|
= JsonBeanDecoder.create(vmDef.data().toString())
|
||||||
|
.readObject();
|
||||||
channel.respond(new NotifyConletView(type(),
|
channel.respond(new NotifyConletView(type(),
|
||||||
conletId, "updateVm", def));
|
conletId, "updateVm", def));
|
||||||
}
|
}
|
||||||
|
|
@ -187,9 +185,8 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
public void onVmDefChanged(VmDefChanged event, VmChannel channel)
|
public void onVmDefChanged(VmDefChanged event, VmChannel channel)
|
||||||
throws JsonDecodeException, IOException {
|
throws JsonDecodeException, IOException {
|
||||||
var vmName = event.vmDefinition().getMetadata().getName();
|
var vmName = event.vmDefinition().getMetadata().getName();
|
||||||
if (event.type() == Type.DELETED) {
|
if (event.type() == K8sObserver.ResponseType.DELETED) {
|
||||||
vmInfos.remove(vmName);
|
channelManager.remove(vmName);
|
||||||
vmChannels.remove(vmName);
|
|
||||||
for (var entry : conletIdsByConsoleConnection().entrySet()) {
|
for (var entry : conletIdsByConsoleConnection().entrySet()) {
|
||||||
for (String conletId : entry.getValue()) {
|
for (String conletId : entry.getValue()) {
|
||||||
entry.getKey().respond(new NotifyConletView(type(),
|
entry.getKey().respond(new NotifyConletView(type(),
|
||||||
|
|
@ -199,8 +196,7 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
} else {
|
} else {
|
||||||
var vmDef = new K8sDynamicModel(channel.client().getJSON()
|
var vmDef = new K8sDynamicModel(channel.client().getJSON()
|
||||||
.getGson(), convertQuantities(event.vmDefinition().data()));
|
.getGson(), convertQuantities(event.vmDefinition().data()));
|
||||||
vmInfos.put(vmName, vmDef);
|
channelManager.put(vmName, channel, vmDef);
|
||||||
vmChannels.put(vmName, channel);
|
|
||||||
var def = JsonBeanDecoder.create(vmDef.data().toString())
|
var def = JsonBeanDecoder.create(vmDef.data().toString())
|
||||||
.readObject();
|
.readObject();
|
||||||
for (var entry : conletIdsByConsoleConnection().entrySet()) {
|
for (var entry : conletIdsByConsoleConnection().entrySet()) {
|
||||||
|
|
@ -323,7 +319,7 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
return cachedSummary;
|
return cachedSummary;
|
||||||
}
|
}
|
||||||
Summary summary = new Summary();
|
Summary summary = new Summary();
|
||||||
for (var vmDef : vmInfos.values()) {
|
for (var vmDef : channelManager.associated()) {
|
||||||
summary.totalVms += 1;
|
summary.totalVms += 1;
|
||||||
var status = GsonPtr.to(vmDef.data()).to("status");
|
var status = GsonPtr.to(vmDef.data()).to("status");
|
||||||
summary.usedCpus += status.getAsInt("cpus").orElse(0);
|
summary.usedCpus += status.getAsInt("cpus").orElse(0);
|
||||||
|
|
@ -349,7 +345,7 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
throws Exception {
|
throws Exception {
|
||||||
event.stop();
|
event.stop();
|
||||||
var vmName = event.params().asString(0);
|
var vmName = event.params().asString(0);
|
||||||
var vmChannel = vmChannels.get(vmName);
|
var vmChannel = channelManager.channel(vmName).orElse(null);
|
||||||
if (vmChannel == null) {
|
if (vmChannel == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue