Viewer ACL (#26)
Some checks failed
Java CI with Gradle / build (push) Has been cancelled

Provide ACLs (together with general improvements) for the viewer conlet.
This commit is contained in:
Michael N. Lipp 2024-06-01 11:12:15 +02:00 committed by GitHub
parent a6525a2289
commit 659463b3b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 1664 additions and 679 deletions

View file

@ -990,6 +990,30 @@ spec:
description: Copied to cloud-init's network-config file.
type: object
x-kubernetes-preserve-unknown-fields: true
permissions:
type: array
description: >-
Defines permissions for accessing and manipulating the VM.
items:
type: object
description: >-
Permissions can be granted to a user or to a role.
oneOf:
- required:
- user
- required:
- role
properties:
user:
type: string
role:
type: string
may:
type: array
items:
type: string
enum: ["start", "stop", "accessConsole", "*"]
default: []
vm:
type: object
description: Defines the VM.
@ -1395,6 +1419,9 @@ spec:
to the spice server. Defaults to the address
of the node that the VM is running on.
type: string
generateSecret:
type: boolean
default: true
proxyUrl:
description: >-
If specified, is copied to the generated

View file

@ -28,6 +28,8 @@
# User admin has role admin
admin:
- admin
test:
- user
# All users have role other
"*":
- other
@ -37,11 +39,14 @@
# Admins can use all conlets
admin:
- "*"
user:
- org.jdrupes.vmoperator.vmviewer.VmViewer
# Others cannot use any conlet (except login conlet to log out)
other:
- --org.jdrupes.vmoperator.vmconlet.VmConlet
- org.jgrapes.webconlet.oidclogin.LoginConlet
"/ComponentCollector":
"/VmViewer":
displayResource:
preferredIpVersion: ipv4
syncPreviewsFor:
- role: user

View file

@ -54,6 +54,8 @@ patches:
# User admin has role admin
admin:
- admin
test:
- user
# All users have role other
"*":
- other
@ -63,6 +65,8 @@ patches:
# Admins can use all conlets
admin:
- "*"
user:
- org.jdrupes.vmoperator.vmviewer.VmViewer
# Others cannot use any conlet (except login conlet to log out)
other:
- org.jgrapes.webconlet.locallogin.LoginConlet
@ -70,6 +74,8 @@ patches:
"/VmViewer":
displayResource:
preferredIpVersion: ipv4
syncPreviewsFor:
- role: user
- target:
group: apps
version: v1

View file

@ -7,8 +7,17 @@ spec:
image:
repository: docker-registry.lan.mnl.de
path: vmoperator/org.jdrupes.vmoperator.runner.qemu-alpine
version: latest
pullPolicy: Always
permissions:
- user: admin
may:
- "*"
- user: test
may:
- "accessConsole"
resources:
requests:
cpu: 1
@ -52,3 +61,4 @@ spec:
display:
spice:
port: 5810
generateSecret: true

View file

@ -0,0 +1,197 @@
/*
* VM-Operator
* Copyright (C) 2024 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.common;
import com.google.gson.Gson;
import com.google.gson.InstanceCreator;
import com.google.gson.JsonObject;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import io.kubernetes.client.openapi.ApiClient;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Type;
/**
* A factory for creating objects.
*
* @param <O> the generic type
* @param <L> the generic type
*/
public class DynamicTypeAdapterFactory<O extends K8sDynamicModel,
L extends K8sDynamicModelsBase<O>> implements TypeAdapterFactory {
private final Class<O> objectClass;
private final Class<L> objectListClass;
/**
* Make sure that this adapter is registered.
*
* @param client the client
*/
public void register(ApiClient client) {
if (!ModelCreator.class
.equals(client.getJSON().getGson().getAdapter(objectClass)
.getClass())
|| !ModelsCreator.class.equals(client.getJSON().getGson()
.getAdapter(objectListClass).getClass())) {
Gson gson = client.getJSON().getGson();
client.getJSON().setGson(gson.newBuilder()
.registerTypeAdapterFactory(this).create());
}
}
/**
* Instantiates a new generic type adapter factory.
*
* @param objectClass the object class
* @param objectListClass the object list class
*/
public DynamicTypeAdapterFactory(Class<O> objectClass,
Class<L> objectListClass) {
this.objectClass = objectClass;
this.objectListClass = objectListClass;
}
/**
* Creates a type adapter for the given type.
*
* @param <T> the generic type
* @param gson the gson
* @param typeToken the type token
* @return the type adapter or null if the type is not handles by
* this factory
*/
@SuppressWarnings("unchecked")
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
if (TypeToken.get(objectClass).equals(typeToken)) {
return (TypeAdapter<T>) new ModelCreator(gson);
}
if (TypeToken.get(objectListClass).equals(typeToken)) {
return (TypeAdapter<T>) new ModelsCreator(gson);
}
return null;
}
/**
* The Class ModelCreator.
*/
private class ModelCreator extends TypeAdapter<O>
implements InstanceCreator<O> {
private final Gson delegate;
/**
* Instantiates a new object state creator.
*
* @param delegate the delegate
*/
public ModelCreator(Gson delegate) {
this.delegate = delegate;
}
@Override
public O createInstance(Type type) {
try {
return objectClass.getConstructor(Gson.class, JsonObject.class)
.newInstance(delegate, null);
} catch (InstantiationException | IllegalAccessException
| IllegalArgumentException | InvocationTargetException
| NoSuchMethodException | SecurityException e) {
return null;
}
}
@Override
public void write(JsonWriter jsonWriter, O state)
throws IOException {
jsonWriter.jsonValue(delegate.toJson(state.data()));
}
@Override
public O read(JsonReader jsonReader)
throws IOException {
try {
return objectClass.getConstructor(Gson.class, JsonObject.class)
.newInstance(delegate,
delegate.fromJson(jsonReader, JsonObject.class));
} catch (InstantiationException | IllegalAccessException
| IllegalArgumentException | InvocationTargetException
| NoSuchMethodException | SecurityException e) {
return null;
}
}
}
/**
* The Class ModelsCreator.
*/
private class ModelsCreator extends TypeAdapter<L>
implements InstanceCreator<L> {
private final Gson delegate;
/**
* Instantiates a new object states creator.
*
* @param delegate the delegate
*/
public ModelsCreator(Gson delegate) {
this.delegate = delegate;
}
@Override
public L createInstance(Type type) {
try {
return objectListClass
.getConstructor(Gson.class, JsonObject.class)
.newInstance(delegate, null);
} catch (InstantiationException | IllegalAccessException
| IllegalArgumentException | InvocationTargetException
| NoSuchMethodException | SecurityException e) {
return null;
}
}
@Override
public void write(JsonWriter jsonWriter, L states)
throws IOException {
jsonWriter.jsonValue(delegate.toJson(states.data()));
}
@Override
public L read(JsonReader jsonReader)
throws IOException {
try {
return objectListClass
.getConstructor(Gson.class, JsonObject.class)
.newInstance(delegate,
delegate.fromJson(jsonReader, JsonObject.class));
} catch (InstantiationException | IllegalAccessException
| IllegalArgumentException | InvocationTargetException
| NoSuchMethodException | SecurityException e) {
return null;
}
}
}
}

View file

@ -1,131 +0,0 @@
/*
* VM-Operator
* Copyright (C) 2024 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.common;
import com.google.gson.Gson;
import com.google.gson.InstanceCreator;
import com.google.gson.JsonObject;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.lang.reflect.Type;
/**
* A factory for creating K8sDynamicModel(s) objects.
*/
public class K8sDynamicModelTypeAdapterFactory implements TypeAdapterFactory {
/**
* Creates a type adapter for the given type.
*
* @param <T> the generic type
* @param gson the gson
* @param typeToken the type token
* @return the type adapter or null if the type is not handles by
* this factory
*/
@SuppressWarnings("unchecked")
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
if (TypeToken.get(K8sDynamicModel.class).equals(typeToken)) {
return (TypeAdapter<T>) new K8sDynamicModelCreator(gson);
}
if (TypeToken.get(K8sDynamicModels.class).equals(typeToken)) {
return (TypeAdapter<T>) new K8sDynamicModelsCreator(gson);
}
return null;
}
/**
* The Class K8sDynamicModelCreator.
*/
/* default */ class K8sDynamicModelCreator
extends TypeAdapter<K8sDynamicModel>
implements InstanceCreator<K8sDynamicModel> {
private final Gson delegate;
/**
* Instantiates a new object state creator.
*
* @param delegate the delegate
*/
public K8sDynamicModelCreator(Gson delegate) {
this.delegate = delegate;
}
@Override
public K8sDynamicModel createInstance(Type type) {
return new K8sDynamicModel(delegate, null);
}
@Override
public void write(JsonWriter jsonWriter, K8sDynamicModel state)
throws IOException {
jsonWriter.jsonValue(delegate.toJson(state.data()));
}
@Override
public K8sDynamicModel read(JsonReader jsonReader)
throws IOException {
return new K8sDynamicModel(delegate,
delegate.fromJson(jsonReader, JsonObject.class));
}
}
/**
* The Class K8sDynamicModelsCreator.
*/
/* default */class K8sDynamicModelsCreator
extends TypeAdapter<K8sDynamicModels>
implements InstanceCreator<K8sDynamicModels> {
private final Gson delegate;
/**
* Instantiates a new object states creator.
*
* @param delegate the delegate
*/
public K8sDynamicModelsCreator(Gson delegate) {
this.delegate = delegate;
}
@Override
public K8sDynamicModels createInstance(Type type) {
return new K8sDynamicModels(delegate, null);
}
@Override
public void write(JsonWriter jsonWriter, K8sDynamicModels states)
throws IOException {
jsonWriter.jsonValue(delegate.toJson(states.data()));
}
@Override
public K8sDynamicModels read(JsonReader jsonReader)
throws IOException {
return new K8sDynamicModels(delegate,
delegate.fromJson(jsonReader, JsonObject.class));
}
}
}

View file

@ -19,14 +19,8 @@
package org.jdrupes.vmoperator.common;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import io.kubernetes.client.common.KubernetesListObject;
import io.kubernetes.client.openapi.Configuration;
import io.kubernetes.client.openapi.models.V1ListMeta;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Represents a list of Kubernetes objects each of which is
@ -35,11 +29,7 @@ import java.util.Objects;
* notably the metadata, is made available through the methods
* defined by {@link KubernetesListObject}.
*/
public class K8sDynamicModels implements KubernetesListObject {
private final JsonObject data;
private final V1ListMeta metadata;
private final List<K8sDynamicModel> items;
public class K8sDynamicModels extends K8sDynamicModelsBase<K8sDynamicModel> {
/**
* Initialize the object list using the given JSON data.
@ -48,116 +38,7 @@ public class K8sDynamicModels implements KubernetesListObject {
* @param data the data
*/
public K8sDynamicModels(Gson delegate, JsonObject data) {
this.data = data;
metadata = delegate.fromJson(data.get("metadata"), V1ListMeta.class);
items = new ArrayList<>();
for (JsonElement e : data.get("items").getAsJsonArray()) {
items.add(new K8sDynamicModel(delegate, e.getAsJsonObject()));
}
super(K8sDynamicModel.class, delegate, data);
}
@Override
public String getApiVersion() {
return apiVersion();
}
/**
* Gets the API version. (Abbreviated method name for convenience.)
*
* @return the API version
*/
public String apiVersion() {
return data.get("apiVersion").getAsString();
}
@Override
public String getKind() {
return kind();
}
/**
* Gets the kind. (Abbreviated method name for convenience.)
*
* @return the kind
*/
public String kind() {
return data.get("kind").getAsString();
}
@Override
public V1ListMeta getMetadata() {
return metadata;
}
/**
* Gets the metadata. (Abbreviated method name for convenience.)
*
* @return the metadata
*/
public V1ListMeta metadata() {
return metadata;
}
/**
* Returns the JSON representation of this object.
*
* @return the JOSN representation
*/
public JsonObject data() {
return data;
}
@Override
public List<K8sDynamicModel> getItems() {
return items;
}
/**
* Sets the api version.
*
* @param apiVersion the new api version
*/
public void setApiVersion(String apiVersion) {
data.addProperty("apiVersion", apiVersion);
}
/**
* Sets the kind.
*
* @param kind the new kind
*/
public void setKind(String kind) {
data.addProperty("kind", kind);
}
/**
* Sets the metadata.
*
* @param objectMeta the new metadata
*/
public void setMetadata(V1ListMeta objectMeta) {
data.add("metadata",
Configuration.getDefaultApiClient().getJSON().getGson()
.toJsonTree(objectMeta));
}
@Override
public int hashCode() {
return Objects.hash(data);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
K8sDynamicModels other = (K8sDynamicModels) obj;
return Objects.equals(data, other.data);
}
}

View file

@ -0,0 +1,174 @@
/*
* VM-Operator
* Copyright (C) 2024 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.common;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import io.kubernetes.client.common.KubernetesListObject;
import io.kubernetes.client.openapi.Configuration;
import io.kubernetes.client.openapi.models.V1ListMeta;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Represents a list of Kubernetes objects each of which is
* represented using a JSON data structure.
* Some information that is common to all Kubernetes objects,
* notably the metadata, is made available through the methods
* defined by {@link KubernetesListObject}.
*/
public class K8sDynamicModelsBase<T extends K8sDynamicModel>
implements KubernetesListObject {
private final JsonObject data;
private final V1ListMeta metadata;
private final List<T> items;
/**
* Initialize the object list using the given JSON data.
*
* @param itemClass the item class
* @param delegate the gson instance to use for extracting structured data
* @param data the data
*/
public K8sDynamicModelsBase(Class<T> itemClass, Gson delegate,
JsonObject data) {
this.data = data;
metadata = delegate.fromJson(data.get("metadata"), V1ListMeta.class);
items = new ArrayList<>();
for (JsonElement e : data.get("items").getAsJsonArray()) {
try {
items.add(itemClass.getConstructor(Gson.class, JsonObject.class)
.newInstance(delegate, e.getAsJsonObject()));
} catch (InstantiationException | IllegalAccessException
| IllegalArgumentException | InvocationTargetException
| NoSuchMethodException | SecurityException exc) {
throw new IllegalArgumentException(exc);
}
}
}
@Override
public String getApiVersion() {
return apiVersion();
}
/**
* Gets the API version. (Abbreviated method name for convenience.)
*
* @return the API version
*/
public String apiVersion() {
return data.get("apiVersion").getAsString();
}
@Override
public String getKind() {
return kind();
}
/**
* Gets the kind. (Abbreviated method name for convenience.)
*
* @return the kind
*/
public String kind() {
return data.get("kind").getAsString();
}
@Override
public V1ListMeta getMetadata() {
return metadata;
}
/**
* Gets the metadata. (Abbreviated method name for convenience.)
*
* @return the metadata
*/
public V1ListMeta metadata() {
return metadata;
}
/**
* Returns the JSON representation of this object.
*
* @return the JOSN representation
*/
public JsonObject data() {
return data;
}
@Override
public List<T> getItems() {
return items;
}
/**
* Sets the api version.
*
* @param apiVersion the new api version
*/
public void setApiVersion(String apiVersion) {
data.addProperty("apiVersion", apiVersion);
}
/**
* Sets the kind.
*
* @param kind the new kind
*/
public void setKind(String kind) {
data.addProperty("kind", kind);
}
/**
* Sets the metadata.
*
* @param objectMeta the new metadata
*/
public void setMetadata(V1ListMeta objectMeta) {
data.add("metadata",
Configuration.getDefaultApiClient().getJSON().getGson()
.toJsonTree(objectMeta));
}
@Override
public int hashCode() {
return Objects.hash(data);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
K8sDynamicModelsBase<?> other = (K8sDynamicModelsBase<?>) obj;
return Objects.equals(data, other.data);
}
}

View file

@ -18,10 +18,8 @@
package org.jdrupes.vmoperator.common;
import com.google.gson.Gson;
import io.kubernetes.client.Discovery.APIResource;
import io.kubernetes.client.apimachinery.GroupVersionKind;
import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.util.generic.options.ListOptions;
import java.io.Reader;
@ -35,40 +33,23 @@ import java.util.Collection;
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class K8sDynamicStub
extends K8sGenericStub<K8sDynamicModel, K8sDynamicModels> {
extends K8sDynamicStubBase<K8sDynamicModel, K8sDynamicModels> {
private static DynamicTypeAdapterFactory<K8sDynamicModel,
K8sDynamicModels> taf = new K8sDynamicModelTypeAdapterFactory();
/**
* Instantiates a new dynamic stub.
*
* @param objectClass the object class
* @param objectListClass the object list class
* @param client the client
* @param context the context
* @param namespace the namespace
* @param name the name
*/
public K8sDynamicStub(Class<K8sDynamicModel> objectClass,
Class<K8sDynamicModels> objectListClass, K8sClient client,
public K8sDynamicStub(K8sClient client,
APIResource context, String namespace, String name) {
super(objectClass, objectListClass, client, context, namespace, name);
// Make sure that we have an adapter for our type
Gson gson = client.getJSON().getGson();
if (!checkAdapters(client)) {
client.getJSON().setGson(gson.newBuilder()
.registerTypeAdapterFactory(
new K8sDynamicModelTypeAdapterFactory())
.create());
}
}
private boolean checkAdapters(ApiClient client) {
return K8sDynamicModelTypeAdapterFactory.K8sDynamicModelCreator.class
.equals(client.getJSON().getGson().getAdapter(K8sDynamicModel.class)
.getClass())
&& K8sDynamicModelTypeAdapterFactory.K8sDynamicModelsCreator.class
.equals(client.getJSON().getGson()
.getAdapter(K8sDynamicModels.class).getClass());
super(K8sDynamicModel.class, K8sDynamicModels.class, taf, client,
context, namespace, name);
}
/**
@ -88,8 +69,8 @@ public class K8sDynamicStub
public static K8sDynamicStub get(K8sClient client,
GroupVersionKind gvk, String namespace, String name)
throws ApiException {
return K8sGenericStub.get(K8sDynamicModel.class, K8sDynamicModels.class,
client, gvk, namespace, name, K8sDynamicStub::new);
return new K8sDynamicStub(client, apiResource(client, gvk), namespace,
name);
}
/**
@ -106,8 +87,7 @@ public class K8sDynamicStub
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
public static K8sDynamicStub get(K8sClient client,
APIResource context, String namespace, String name) {
return K8sGenericStub.get(K8sDynamicModel.class, K8sDynamicModels.class,
client, context, namespace, name, K8sDynamicStub::new);
return new K8sDynamicStub(client, context, namespace, name);
}
/**
@ -125,7 +105,7 @@ public class K8sDynamicStub
K8s.yamlToJson(client, yaml));
return K8sGenericStub.create(K8sDynamicModel.class,
K8sDynamicModels.class, client, context, model,
K8sDynamicStub::new);
(c, ns, n) -> new K8sDynamicStub(c, context, ns, n));
}
/**
@ -143,7 +123,7 @@ public class K8sDynamicStub
throws ApiException {
return K8sGenericStub.list(K8sDynamicModel.class,
K8sDynamicModels.class, client, context, namespace, options,
K8sDynamicStub::new);
(c, ns, n) -> new K8sDynamicStub(c, context, ns, n));
}
/**
@ -160,4 +140,18 @@ public class K8sDynamicStub
return list(client, context, namespace, new ListOptions());
}
/**
* A factory for creating K8sDynamicModel(s) objects.
*/
public static class K8sDynamicModelTypeAdapterFactory extends
DynamicTypeAdapterFactory<K8sDynamicModel, K8sDynamicModels> {
/**
* Instantiates a new dynamic model type adapter factory.
*/
public K8sDynamicModelTypeAdapterFactory() {
super(K8sDynamicModel.class, K8sDynamicModels.class);
}
}
}

View file

@ -0,0 +1,51 @@
/*
* VM-Operator
* Copyright (C) 2024 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.common;
import io.kubernetes.client.Discovery.APIResource;
/**
* A stub for namespaced custom objects. It uses a dynamic model
* (see {@link K8sDynamicModel}) for representing the object's
* state and can therefore be used for any kind of object, especially
* custom objects.
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public abstract class K8sDynamicStubBase<O extends K8sDynamicModel,
L extends K8sDynamicModelsBase<O>> extends K8sGenericStub<O, L> {
/**
* Instantiates a new dynamic stub.
*
* @param objectClass the object class
* @param objectListClass the object list class
* @param client the client
* @param context the context
* @param namespace the namespace
* @param name the name
*/
@SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
public K8sDynamicStubBase(Class<O> objectClass,
Class<L> objectListClass, DynamicTypeAdapterFactory<O, L> taf,
K8sClient client, APIResource context, String namespace,
String name) {
super(objectClass, objectListClass, client, context, namespace, name);
taf.register(client);
}
}

View file

@ -26,9 +26,11 @@ import io.kubernetes.client.custom.V1Patch;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.util.Strings;
import io.kubernetes.client.util.generic.GenericKubernetesApi;
import io.kubernetes.client.util.generic.KubernetesApiResponse;
import io.kubernetes.client.util.generic.options.GetOptions;
import io.kubernetes.client.util.generic.options.ListOptions;
import io.kubernetes.client.util.generic.options.PatchOptions;
import io.kubernetes.client.util.generic.options.UpdateOptions;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.Collection;
@ -228,7 +230,8 @@ public class K8sGenericStub<O extends KubernetesObject,
public Optional<O> patch(String patchType, V1Patch patch,
PatchOptions options) throws ApiException {
return K8s
.optional(api.patch(namespace, name, patchType, patch, options));
.optional(api.patch(namespace, name, patchType, patch, options)
.throwsApiException());
}
/**
@ -245,6 +248,30 @@ public class K8sGenericStub<O extends KubernetesObject,
return patch(patchType, patch, opts);
}
/**
* Update the object.
*
* @param object the object
* @return the kubernetes api response
* @throws ApiException the api exception
*/
public KubernetesApiResponse<O> update(O object) throws ApiException {
return api.update(object).throwsApiException();
}
/**
* Update the object.
*
* @param object the object
* @param options the options
* @return the kubernetes api response
* @throws ApiException the api exception
*/
public KubernetesApiResponse<O> update(O object, UpdateOptions options)
throws ApiException {
return api.update(object, options).throwsApiException();
}
/**
* A supplier for generic stubs.
*
@ -258,17 +285,13 @@ public class K8sGenericStub<O extends KubernetesObject,
/**
* Gets a new stub.
*
* @param objectClass the object class
* @param objectListClass the object list class
* @param client the client
* @param context the API resource
* @param namespace the namespace
* @param name the name
* @return the result
*/
@SuppressWarnings("PMD.UseObjectForClearerAPI")
R get(Class<O> objectClass, Class<L> objectListClass, K8sClient client,
APIResource context, String namespace, String name);
R get(K8sClient client, String namespace, String name);
}
@Override
@ -278,68 +301,6 @@ public class K8sGenericStub<O extends KubernetesObject,
+ 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.
*
@ -366,8 +327,7 @@ public class K8sGenericStub<O extends KubernetesObject,
context.getGroup(), context.getPreferredVersion(),
context.getResourcePlural(), client);
api.create(model).throwsApiException();
return provider.get(objectClass, objectListClass, client,
context, model.getMetadata().getNamespace(),
return provider.get(client, model.getMetadata().getNamespace(),
model.getMetadata().getName());
}
@ -402,8 +362,8 @@ public class K8sGenericStub<O extends KubernetesObject,
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()));
result.add(provider.get(client, namespace,
item.getMetadata().getName()));
}
}
return result;
@ -416,4 +376,23 @@ public class K8sGenericStub<O extends KubernetesObject,
return result;
}
/**
* Api resource.
*
* @param client the client
* @param gvk the gvk
* @return the API resource
* @throws ApiException the api exception
*/
public static APIResource apiResource(K8sClient client,
GroupVersionKind gvk) 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 context.get();
}
}

View file

@ -73,18 +73,7 @@ public class K8sV1PodStub extends K8sGenericStub<V1Pod, V1PodList> {
public static Collection<K8sV1PodStub> list(K8sClient client,
String namespace, ListOptions options) throws ApiException {
return K8sGenericStub.list(V1Pod.class, V1PodList.class, client,
CONTEXT, namespace, options, K8sV1PodStub::getGeneric);
CONTEXT, namespace, options, (clnt, nscp,
name) -> new K8sV1PodStub(clnt, nscp, name));
}
/**
* Provide {@link GenericSupplier}.
*/
@SuppressWarnings({ "PMD.UnusedFormalParameter",
"PMD.UnusedPrivateMethod" })
private static K8sV1PodStub getGeneric(Class<V1Pod> objectClass,
Class<V1PodList> objectListClass, K8sClient client,
APIResource context, String namespace, String name) {
return new K8sV1PodStub(client, namespace, name);
}
}

View file

@ -25,7 +25,6 @@ import io.kubernetes.client.openapi.models.V1SecretList;
import io.kubernetes.client.util.generic.options.ListOptions;
import java.util.Collection;
import java.util.List;
import org.jdrupes.vmoperator.common.K8sGenericStub.GenericSupplier;
/**
* A stub for secrets (v1).
@ -62,6 +61,20 @@ public class K8sV1SecretStub extends K8sGenericStub<V1Secret, V1SecretList> {
return new K8sV1SecretStub(client, namespace, name);
}
/**
* Creates an object stub from a model.
*
* @param client the client
* @param model the model
* @return the k 8 s dynamic stub
* @throws ApiException the api exception
*/
public static K8sV1SecretStub create(K8sClient client, V1Secret model)
throws ApiException {
return K8sGenericStub.create(V1Secret.class,
V1SecretList.class, client, CONTEXT, model, K8sV1SecretStub::new);
}
/**
* Get the stubs for the objects in the given namespace that match
* the criteria from the given options.
@ -75,18 +88,6 @@ public class K8sV1SecretStub extends K8sGenericStub<V1Secret, V1SecretList> {
public static Collection<K8sV1SecretStub> list(K8sClient client,
String namespace, ListOptions options) throws ApiException {
return K8sGenericStub.list(V1Secret.class, V1SecretList.class, client,
CONTEXT, namespace, options, K8sV1SecretStub::getGeneric);
CONTEXT, namespace, options, K8sV1SecretStub::new);
}
/**
* Provide {@link GenericSupplier}.
*/
@SuppressWarnings({ "PMD.UnusedFormalParameter",
"PMD.UnusedPrivateMethod" })
private static K8sV1SecretStub getGeneric(Class<V1Secret> objectClass,
Class<V1SecretList> objectListClass, K8sClient client,
APIResource context, String namespace, String name) {
return new K8sV1SecretStub(client, namespace, name);
}
}

View file

@ -25,7 +25,6 @@ import io.kubernetes.client.openapi.models.V1ServiceList;
import io.kubernetes.client.util.generic.options.ListOptions;
import java.util.Collection;
import java.util.List;
import org.jdrupes.vmoperator.common.K8sGenericStub.GenericSupplier;
/**
* A stub for secrets (v1).
@ -75,18 +74,7 @@ public class K8sV1ServiceStub extends K8sGenericStub<V1Service, V1ServiceList> {
public static Collection<K8sV1ServiceStub> list(K8sClient client,
String namespace, ListOptions options) throws ApiException {
return K8sGenericStub.list(V1Service.class, V1ServiceList.class, client,
CONTEXT, namespace, options, K8sV1ServiceStub::getGeneric);
CONTEXT, namespace, options,
(clnt, nscp, name) -> new K8sV1ServiceStub(clnt, nscp, name));
}
/**
* Provide {@link GenericSupplier}.
*/
@SuppressWarnings({ "PMD.UnusedFormalParameter",
"PMD.UnusedPrivateMethod" })
private static K8sV1ServiceStub getGeneric(Class<V1Service> objectClass,
Class<V1ServiceList> objectListClass, K8sClient client,
APIResource context, String namespace, String name) {
return new K8sV1ServiceStub(client, namespace, name);
}
}

View file

@ -0,0 +1,123 @@
/*
* VM-Operator
* Copyright (C) 2024 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.common;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.jdrupes.vmoperator.util.GsonPtr;
/**
* Represents a VM definition.
*/
@SuppressWarnings("PMD.DataClass")
public class VmDefinitionModel extends K8sDynamicModel {
/**
* Permissions for accessing and manipulating the VM.
*/
public enum Permission {
START("start"), STOP("stop"), ACCESS_CONSOLE("accessConsole");
@SuppressWarnings("PMD.UseConcurrentHashMap")
private static Map<String, Permission> reprs = new HashMap<>();
static {
for (var value : EnumSet.allOf(Permission.class)) {
reprs.put(value.repr, value);
}
}
private final String repr;
Permission(String repr) {
this.repr = repr;
}
/**
* Create permission from representation in CRD.
*
* @param value the value
* @return the permission
*/
@SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
public static Set<Permission> parse(String value) {
if ("*".equals(value)) {
return EnumSet.allOf(Permission.class);
}
return Set.of(reprs.get(value));
}
@Override
public String toString() {
return repr;
}
}
/**
* Instantiates a new model from the JSON representation.
*
* @param delegate the gson instance to use for extracting structured data
* @param json the JSON
*/
public VmDefinitionModel(Gson delegate, JsonObject json) {
super(delegate, json);
}
/**
* Collect all permissions for the given user with the given roles.
*
* @param user the user
* @param roles the roles
* @return the sets the
*/
public Set<Permission> permissionsFor(String user,
Collection<String> roles) {
return GsonPtr.to(data())
.getAsListOf(JsonObject.class, "spec", "permissions")
.stream().filter(p -> GsonPtr.to(p).getAsString("user")
.map(u -> u.equals(user)).orElse(false)
|| GsonPtr.to(p).getAsString("role").map(roles::contains)
.orElse(false))
.map(p -> GsonPtr.to(p).getAsListOf(JsonPrimitive.class, "may")
.stream())
.flatMap(Function.identity()).map(p -> p.getAsString())
.map(Permission::parse).map(Set::stream)
.flatMap(Function.identity()).collect(Collectors.toSet());
}
/**
* Get the display password serial.
*
* @return the optional
*/
public Optional<Long> displayPasswordSerial() {
return GsonPtr.to(status())
.get(JsonPrimitive.class, "displayPasswordSerial")
.map(JsonPrimitive::getAsLong);
}
}

View file

@ -0,0 +1,39 @@
/*
* VM-Operator
* Copyright (C) 2024 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.common;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
/**
* Represents a list of {@link VmDefinitionModel}s.
*/
public class VmDefinitionModels
extends K8sDynamicModelsBase<VmDefinitionModel> {
/**
* Initialize the object list using the given JSON data.
*
* @param delegate the gson instance to use for extracting structured data
* @param data the data
*/
public VmDefinitionModels(Gson delegate, JsonObject data) {
super(VmDefinitionModel.class, delegate, data);
}
}

View file

@ -0,0 +1,157 @@
/*
* VM-Operator
* Copyright (C) 2024 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.common;
import io.kubernetes.client.Discovery.APIResource;
import io.kubernetes.client.apimachinery.GroupVersionKind;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.util.generic.options.ListOptions;
import java.io.Reader;
import java.util.Collection;
/**
* A stub for namespaced custom objects. It uses a dynamic model
* (see {@link K8sDynamicModel}) for representing the object's
* state and can therefore be used for any kind of object, especially
* custom objects.
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class VmDefinitionStub
extends K8sDynamicStubBase<VmDefinitionModel, VmDefinitionModels> {
private static DynamicTypeAdapterFactory<VmDefinitionModel,
VmDefinitionModels> taf = new VmDefintionModelTypeAdapterFactory();
/**
* Instantiates a new stub for VM defintions.
*
* @param client the client
* @param context the context
* @param namespace the namespace
* @param name the name
*/
public VmDefinitionStub(K8sClient client, APIResource context,
String namespace, String name) {
super(VmDefinitionModel.class, VmDefinitionModels.class, taf, client,
context, namespace, name);
}
/**
* Get a dynamic object stub. If the version in parameter
* `gvk` is an empty string, the stub refers to the first object with
* matching group and kind.
*
* @param client the client
* @param gvk the group, version and kind
* @param namespace the namespace
* @param name the name
* @return the stub if the object exists
* @throws ApiException the api exception
*/
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
public static VmDefinitionStub get(K8sClient client,
GroupVersionKind gvk, String namespace, String name)
throws ApiException {
return new VmDefinitionStub(client, apiResource(client, gvk), namespace,
name);
}
/**
* Get a dynamic object stub.
*
* @param client the client
* @param context the context
* @param namespace the namespace
* @param name the name
* @return the stub if the object exists
* @throws ApiException the api exception
*/
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
public static VmDefinitionStub get(K8sClient client,
APIResource context, String namespace, String name) {
return new VmDefinitionStub(client, context, namespace, name);
}
/**
* Creates a stub from yaml.
*
* @param client the client
* @param context the context
* @param yaml the yaml
* @return the k 8 s dynamic stub
* @throws ApiException the api exception
*/
public static VmDefinitionStub createFromYaml(K8sClient client,
APIResource context, Reader yaml) throws ApiException {
var model = new VmDefinitionModel(client.getJSON().getGson(),
K8s.yamlToJson(client, yaml));
return K8sGenericStub.create(VmDefinitionModel.class,
VmDefinitionModels.class, client, context, model,
(c, ns, n) -> new VmDefinitionStub(c, context, ns, n));
}
/**
* Get the stubs for the objects in the given namespace that match
* the criteria from the given options.
*
* @param client the client
* @param namespace the namespace
* @param options the options
* @return the collection
* @throws ApiException the api exception
*/
public static Collection<VmDefinitionStub> list(K8sClient client,
APIResource context, String namespace, ListOptions options)
throws ApiException {
return K8sGenericStub.list(VmDefinitionModel.class,
VmDefinitionModels.class, client, context, namespace, options,
(c, ns, n) -> new VmDefinitionStub(c, context, ns, n));
}
/**
* Get the stubs for the objects in the given namespace.
*
* @param client the client
* @param namespace the namespace
* @return the collection
* @throws ApiException the api exception
*/
public static Collection<VmDefinitionStub> list(K8sClient client,
APIResource context, String namespace)
throws ApiException {
return list(client, context, namespace, new ListOptions());
}
/**
* A factory for creating VmDefinitionModel(s) objects.
*/
public static class VmDefintionModelTypeAdapterFactory extends
DynamicTypeAdapterFactory<VmDefinitionModel, VmDefinitionModels> {
/**
* Instantiates a new dynamic model type adapter factory.
*/
public VmDefintionModelTypeAdapterFactory() {
super(VmDefinitionModel.class, VmDefinitionModels.class);
}
}
}

View file

@ -1,76 +0,0 @@
/*
* VM-Operator
* Copyright (C) 2024 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <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 DisplayPasswordChanged 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 DisplayPasswordChanged(ResponseType type, V1Secret secret) {
this.type = type;
this.secret = secret;
}
/**
* Returns the type.
*
* @return the type
*/
public ResponseType type() {
return type;
}
/**
* Gets the secret.
*
* @return the secret
*/
public V1Secret secret() {
return secret;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append(Components.objectName(this)).append(" [")
.append(secret.getMetadata().getName()).append(' ').append(type);
if (channels() != null) {
builder.append(", channels=").append(Channel.toString(channels()));
}
builder.append(']');
return builder.toString();
}
}

View file

@ -19,40 +19,44 @@
package org.jdrupes.vmoperator.manager.events;
import java.util.Optional;
import org.jdrupes.vmoperator.common.VmDefinitionModel;
import org.jgrapes.core.Event;
/**
* Gets the current display secret.
* Gets the current display secret and optionally updates it.
*/
@SuppressWarnings("PMD.DataClass")
public class GetDisplayPassword extends Event<String> {
private final String vmName;
private final VmDefinitionModel vmDef;
/**
* Instantiates a new returns the display secret.
*
* @param vmName the vm name
* @param vmDef the vm name
*/
public GetDisplayPassword(String vmName) {
this.vmName = vmName;
public GetDisplayPassword(VmDefinitionModel vmDef) {
this.vmDef = vmDef;
}
/**
* Gets the vm name.
* Gets the vm definition.
*
* @return the vm name
* @return the vm definition
*/
public String vmName() {
return vmName;
public VmDefinitionModel vmDefinition() {
return vmDef;
}
/**
* Return the password. Should only be called when the event is completed.
* Return the password. May only be called when the event is completed.
*
* @return the optional
*/
public Optional<String> password() {
if (!isDone()) {
throw new IllegalStateException("Event is not done.");
}
return currentResults().stream().findFirst();
}
}

View file

@ -19,7 +19,7 @@
package org.jdrupes.vmoperator.manager.events;
import org.jdrupes.vmoperator.common.K8sClient;
import org.jdrupes.vmoperator.common.K8sDynamicModel;
import org.jdrupes.vmoperator.common.VmDefinitionModel;
import org.jgrapes.core.Channel;
import org.jgrapes.core.EventPipeline;
import org.jgrapes.core.Subchannel.DefaultSubchannel;
@ -32,7 +32,7 @@ public class VmChannel extends DefaultSubchannel {
private final EventPipeline pipeline;
private final K8sClient client;
private K8sDynamicModel vmDefinition;
private VmDefinitionModel vmDefinition;
private long generation = -1;
/**
@ -56,7 +56,7 @@ public class VmChannel extends DefaultSubchannel {
* @return the watch channel
*/
@SuppressWarnings("PMD.LinguisticNaming")
public VmChannel setVmDefinition(K8sDynamicModel definition) {
public VmChannel setVmDefinition(VmDefinitionModel definition) {
this.vmDefinition = definition;
return this;
}
@ -66,7 +66,7 @@ public class VmChannel extends DefaultSubchannel {
*
* @return the json object
*/
public K8sDynamicModel vmDefinition() {
public VmDefinitionModel vmDefinition() {
return vmDefinition;
}

View file

@ -18,8 +18,8 @@
package org.jdrupes.vmoperator.manager.events;
import org.jdrupes.vmoperator.common.K8sDynamicModel;
import org.jdrupes.vmoperator.common.K8sObserver;
import org.jdrupes.vmoperator.common.VmDefinitionModel;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Components;
import org.jgrapes.core.Event;
@ -36,7 +36,7 @@ public class VmDefChanged extends Event<Void> {
private final K8sObserver.ResponseType type;
private final boolean specChanged;
private final K8sDynamicModel vmDef;
private final VmDefinitionModel vmDef;
/**
* Instantiates a new VM changed event.
@ -46,7 +46,7 @@ public class VmDefChanged extends Event<Void> {
* @param vmDefinition the VM definition
*/
public VmDefChanged(K8sObserver.ResponseType type, boolean specChanged,
K8sDynamicModel vmDefinition) {
VmDefinitionModel vmDefinition) {
this.type = type;
this.specChanged = specChanged;
this.vmDef = vmDefinition;
@ -73,7 +73,7 @@ public class VmDefChanged extends Event<Void> {
*
* @return the object.
*/
public K8sDynamicModel vmDefinition() {
public VmDefinitionModel vmDefinition() {
return vmDef;
}

View file

@ -22,6 +22,7 @@ dependencies {
implementation 'org.jgrapes:org.jgrapes.webconsole.vuejs:[1.5.0,2)'
implementation 'org.jgrapes:org.jgrapes.webconsole.rbac:[1.3.0,2)'
implementation 'org.jgrapes:org.jgrapes.webconlet.oidclogin:[1.3.0,2)'
implementation 'org.jgrapes:org.jgrapes.webconlet.markdowndisplay:[1.2.0,2)'
runtimeOnly 'org.jgrapes:org.jgrapes.webconlet.sysinfo:[1.4.0,2)'
runtimeOnly 'org.jgrapes:org.jgrapes.webconlet.logviewer:[0.2.0,2)'

View file

@ -0,0 +1,5 @@
You can use the "puzzle piece" icon on the top right corner of the
page to add display widgets (conlets) to the overview tab.
Use the "full screen" icon on the top right corner of any
conlet (if available) to get a detailed view.

View file

@ -0,0 +1,6 @@
Verwenden Sie das "Puzzle"-Icon auf der rechten oberen Ecke
der Seite, um Anzeige-Widgets (Conlets) hinzuzufügen.
Wenn sich in der rechten oberen Ecke eines Conlets ein Vollbild-Icon
befindet, können Sie es verwenden, um eine Detailansicht in einem neuen
Register anzufordern.

View file

@ -17,3 +17,4 @@
#
consoleTitle = VM-Operator
introTitle = Usage

View file

@ -0,0 +1,19 @@
#
# VM-Operator
# Copyright (C) 2024 Michael N. Lipp
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
# for more details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program; if not, see <http://www.gnu.org/licenses/>.
#
introTitle = Benutzung

View file

@ -18,11 +18,17 @@
package org.jdrupes.vmoperator.manager;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Collections;
import java.util.ResourceBundle;
import java.util.stream.Collectors;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Component;
import org.jgrapes.core.annotation.Handler;
import org.jgrapes.webconsole.base.Conlet;
import org.jgrapes.webconlet.markdowndisplay.MarkdownDisplayConlet;
import org.jgrapes.webconsole.base.Conlet.RenderMode;
import org.jgrapes.webconsole.base.ConsoleConnection;
import org.jgrapes.webconsole.base.events.AddConletRequest;
import org.jgrapes.webconsole.base.events.ConsoleConfigured;
@ -63,10 +69,13 @@ public class AvoidEmptyPolicy extends Component {
* @param event the event
* @param connection the connection
*/
@Handler
@Handler(priority = 100)
public void onRenderConlet(RenderConlet event,
ConsoleConnection connection) {
connection.session().put(renderedFlagName, true);
if (event.renderAs().contains(RenderMode.Preview)
|| event.renderAs().contains(RenderMode.View)) {
connection.session().put(renderedFlagName, true);
}
}
/**
@ -76,18 +85,42 @@ public class AvoidEmptyPolicy extends Component {
* @param connection the console connection
* @throws InterruptedException the interrupted exception
*/
@Handler
@Handler(priority = -100)
public void onConsoleConfigured(ConsoleConfigured event,
ConsoleConnection connection) throws InterruptedException,
IOException {
if ((Boolean) connection.session().getOrDefault(
renderedFlagName, false)) {
if ((Boolean) connection.session().getOrDefault(renderedFlagName,
false)) {
return;
}
var resourceBundle = ResourceBundle.getBundle(
getClass().getPackage().getName() + ".l10n", connection.locale(),
getClass().getClassLoader(),
ResourceBundle.Control.getNoFallbackControl(
ResourceBundle.Control.FORMAT_DEFAULT));
var locale = resourceBundle.getLocale().toString();
String shortDesc;
try (BufferedReader shortDescReader
= new BufferedReader(new InputStreamReader(
AvoidEmptyPolicy.class.getResourceAsStream(
"ManagerIntro-Preview" + (locale.isEmpty() ? ""
: "_" + locale) + ".md"),
"utf-8"))) {
shortDesc
= shortDescReader.lines().collect(Collectors.joining("\n"));
}
fire(new AddConletRequest(event.event().event().renderSupport(),
"org.jdrupes.vmoperator.vmconlet.VmConlet",
Conlet.RenderMode
.asSet(Conlet.RenderMode.Preview, Conlet.RenderMode.View)),
MarkdownDisplayConlet.class.getName(),
RenderMode.asSet(RenderMode.Preview))
.addProperty(MarkdownDisplayConlet.CONLET_ID,
getClass().getName())
.addProperty(MarkdownDisplayConlet.TITLE,
resourceBundle.getString("consoleTitle"))
.addProperty(MarkdownDisplayConlet.PREVIEW_SOURCE,
shortDesc)
.addProperty(MarkdownDisplayConlet.DELETABLE, true)
.addProperty(MarkdownDisplayConlet.EDITABLE_BY,
Collections.EMPTY_SET),
connection);
}

View file

@ -104,7 +104,9 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
ListOptions listOpts = new ListOptions();
listOpts.setLabelSelector(
"app.kubernetes.io/managed-by=" + VM_OP_NAME + ","
+ "app.kubernetes.io/name=" + APP_NAME);
+ "app.kubernetes.io/name=" + APP_NAME + ","
+ "app.kubernetes.io/instance=" + newCm.getMetadata()
.getLabels().get("app.kubernetes.io/instance"));
// Get pod, selected by label
var podApi = new DynamicKubernetesApi("", "v1", "pods", client);
var pods = podApi

View file

@ -27,6 +27,12 @@ public class Constants extends org.jdrupes.vmoperator.common.Constants {
/** The Constant COMP_DISPLAY_SECRET. */
public static final String COMP_DISPLAY_SECRET = "display-secret";
/** The Constant DATA_DISPLAY_PASSWORD. */
public static final String DATA_DISPLAY_PASSWORD = "display-password";
/** The Constant DATA_PASSWORD_EXPIRY. */
public static final String DATA_PASSWORD_EXPIRY = "password-expiry";
/** The Constant STATE_RUNNING. */
public static final String STATE_RUNNING = "Running";

View file

@ -101,7 +101,7 @@ public class Controller extends Component {
}
});
attach(new VmMonitor(channel()).channelManager(chanMgr));
attach(new DisplayPasswordMonitor(channel())
attach(new DisplaySecretMonitor(channel())
.channelManager(chanMgr.fixed()));
// Currently, we don't use the IP assigned by the load balancer
// to access the VM's console. Might change in the future.

View file

@ -1,102 +0,0 @@
/*
* VM-Operator
* Copyright (C) 2024 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <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.DisplayPasswordChanged;
import org.jdrupes.vmoperator.manager.events.GetDisplayPassword;
import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jgrapes.core.Channel;
import org.jgrapes.core.annotation.Handler;
/**
* Watches for changes of display secrets.
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class DisplayPasswordMonitor
extends AbstractMonitor<V1Secret, V1SecretList, VmChannel> {
/**
* Instantiates a new display secrets monitor.
*
* @param componentChannel the component channel
*/
public DisplayPasswordMonitor(Channel componentChannel) {
super(componentChannel, V1Secret.class, V1SecretList.class);
context(K8sV1SecretStub.CONTEXT);
ListOptions options = new ListOptions();
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
+ "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET);
options(options);
}
@Override
protected void prepareMonitoring() throws IOException, ApiException {
client(new K8sClient());
}
@Override
protected void handleChange(K8sClient client, Response<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 DisplayPasswordChanged(
ResponseType.valueOf(change.type), change.object), channel);
}
/**
* On get display secrets.
*
* @param event the event
* @param channel the channel
* @throws ApiException the api exception
*/
@Handler
@SuppressWarnings("PMD.StringInstantiation")
public void onGetDisplaySecrets(GetDisplayPassword event, VmChannel channel)
throws ApiException {
ListOptions options = new ListOptions();
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
+ "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET + ","
+ "app.kubernetes.io/instance=" + event.vmName());
var stubs = K8sV1SecretStub.list(client(), namespace(), options);
if (stubs.isEmpty()) {
return;
}
stubs.iterator().next().model().map(m -> m.getData())
.map(m -> m.get("display-password"))
.ifPresent(p -> event.setResult(new String(p)));
}
}

View file

@ -0,0 +1,285 @@
/*
* VM-Operator
* Copyright (C) 2024 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.manager;
import io.kubernetes.client.custom.V1Patch;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.models.V1Secret;
import io.kubernetes.client.openapi.models.V1SecretList;
import io.kubernetes.client.util.Watch.Response;
import io.kubernetes.client.util.generic.options.ListOptions;
import io.kubernetes.client.util.generic.options.PatchOptions;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.time.Instant;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.logging.Level;
import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_NAME;
import org.jdrupes.vmoperator.common.K8sClient;
import org.jdrupes.vmoperator.common.K8sV1PodStub;
import org.jdrupes.vmoperator.common.K8sV1SecretStub;
import static org.jdrupes.vmoperator.manager.Constants.COMP_DISPLAY_SECRET;
import static org.jdrupes.vmoperator.manager.Constants.DATA_DISPLAY_PASSWORD;
import static org.jdrupes.vmoperator.manager.Constants.DATA_PASSWORD_EXPIRY;
import org.jdrupes.vmoperator.manager.events.GetDisplayPassword;
import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jgrapes.core.Channel;
import org.jgrapes.core.CompletionLock;
import org.jgrapes.core.Event;
import org.jgrapes.core.annotation.Handler;
import org.jgrapes.util.events.ConfigurationUpdate;
import org.jose4j.base64url.Base64;
/**
* Watches for changes of display secrets.
*/
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.TooManyStaticImports" })
public class DisplaySecretMonitor
extends AbstractMonitor<V1Secret, V1SecretList, VmChannel> {
private int passwordValidity = 10;
private final List<PendingGet> pendingGets
= Collections.synchronizedList(new LinkedList<>());
/**
* Instantiates a new display secrets monitor.
*
* @param componentChannel the component channel
*/
public DisplaySecretMonitor(Channel componentChannel) {
super(componentChannel, V1Secret.class, V1SecretList.class);
context(K8sV1SecretStub.CONTEXT);
ListOptions options = new ListOptions();
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
+ "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET);
options(options);
}
/**
* On configuration update.
*
* @param event the event
*/
@Handler
@Override
public void onConfigurationUpdate(ConfigurationUpdate event) {
super.onConfigurationUpdate(event);
event.structured(componentPath()).ifPresent(c -> {
try {
if (c.containsKey("passwordValidity")) {
passwordValidity = Integer
.parseInt((String) c.get("passwordValidity"));
}
} catch (ClassCastException e) {
logger.config("Malformed configuration: " + e.getMessage());
}
});
}
@Override
protected void prepareMonitoring() throws IOException, ApiException {
client(new K8sClient());
}
@Override
protected void handleChange(K8sClient client, Response<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;
}
try {
patchPod(client, change);
} catch (ApiException e) {
logger.log(Level.WARNING, e,
() -> "Cannot patch pod annotations: " + e.getMessage());
}
}
private void patchPod(K8sClient client, Response<V1Secret> change)
throws ApiException {
// Force update for pod
ListOptions listOpts = new ListOptions();
listOpts.setLabelSelector(
"app.kubernetes.io/managed-by=" + VM_OP_NAME + ","
+ "app.kubernetes.io/name=" + APP_NAME + ","
+ "app.kubernetes.io/instance=" + change.object.getMetadata()
.getLabels().get("app.kubernetes.io/instance"));
// Get pod, selected by label
var pods = K8sV1PodStub.list(client, namespace(), listOpts);
// If the VM is being created, the pod may not exist yet.
if (pods.isEmpty()) {
return;
}
var pod = pods.iterator().next();
// Patch pod annotation
PatchOptions patchOpts = new PatchOptions();
patchOpts.setFieldManager("kubernetes-java-kubectl-apply");
pod.patch(V1Patch.PATCH_FORMAT_JSON_PATCH,
new V1Patch("[{\"op\": \"replace\", \"path\": "
+ "\"/metadata/annotations/vmrunner.jdrupes.org~1dpVersion\", "
+ "\"value\": \""
+ change.object.getMetadata().getResourceVersion()
+ "\"}]"),
patchOpts);
}
/**
* On get display secrets.
*
* @param event the event
* @param channel the channel
* @throws ApiException the api exception
*/
@Handler
@SuppressWarnings("PMD.StringInstantiation")
public void onGetDisplaySecrets(GetDisplayPassword event, VmChannel channel)
throws ApiException {
ListOptions options = new ListOptions();
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
+ "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET + ","
+ "app.kubernetes.io/instance="
+ event.vmDefinition().metadata().getName());
var stubs = K8sV1SecretStub.list(client(),
event.vmDefinition().metadata().getNamespace(), options);
if (stubs.isEmpty()) {
return;
}
var stub = stubs.iterator().next();
// Check validity
var model = stub.model().get();
@SuppressWarnings("PMD.StringInstantiation")
var expiry = new String(model.getData().get(DATA_PASSWORD_EXPIRY));
if (model.getData().get(DATA_DISPLAY_PASSWORD) != null
&& stillValid(expiry)) {
event.setResult(
new String(model.getData().get(DATA_DISPLAY_PASSWORD)));
return;
}
updatePassword(stub, event);
}
@SuppressWarnings("PMD.StringInstantiation")
private void updatePassword(K8sV1SecretStub stub, GetDisplayPassword event)
throws ApiException {
SecureRandom random = null;
try {
random = SecureRandom.getInstanceStrong();
} catch (NoSuchAlgorithmException e) { // NOPMD
// "Every implementation of the Java platform is required
// to support at least one strong SecureRandom implementation."
}
byte[] bytes = new byte[16];
random.nextBytes(bytes);
var password = Base64.encode(bytes);
var model = stub.model().get();
model.setStringData(Map.of(DATA_DISPLAY_PASSWORD, password,
DATA_PASSWORD_EXPIRY,
Long.toString(Instant.now().getEpochSecond() + passwordValidity)));
event.setResult(password);
// Prepare wait for confirmation (by VM status change)
var pending = new PendingGet(event,
event.vmDefinition().displayPasswordSerial().orElse(0L) + 1,
new CompletionLock(event, 1500));
pendingGets.add(pending);
Event.onCompletion(event, e -> {
pendingGets.remove(pending);
});
// Update, will (eventually) trigger confirmation
stub.update(model).getObject();
}
private boolean stillValid(String expiry) {
if (expiry == null || "never".equals(expiry)) {
return true;
}
@SuppressWarnings({ "PMD.CloseResource", "resource" })
var scanner = new Scanner(expiry);
if (!scanner.hasNextLong()) {
return false;
}
long expTime = scanner.nextLong();
return expTime > Instant.now().getEpochSecond() + passwordValidity;
}
/**
* On vm def changed.
*
* @param event the event
* @param channel the channel
*/
@Handler
public void onVmDefChanged(VmDefChanged event, Channel channel) {
synchronized (pendingGets) {
String vmName = event.vmDefinition().metadata().getName();
for (var pending : pendingGets) {
if (pending.event.vmDefinition().metadata().getName()
.equals(vmName)
&& event.vmDefinition().displayPasswordSerial()
.map(s -> s >= pending.expectedSerial).orElse(false)) {
pending.lock.remove();
// pending will be removed from pendingGest by
// waiting thread, see updatePassword
continue;
}
}
}
}
/**
* The Class PendingGet.
*/
@SuppressWarnings("PMD.DataClass")
private static class PendingGet {
public final GetDisplayPassword event;
public final long expectedSerial;
public final CompletionLock lock;
/**
* Instantiates a new pending get.
*
* @param event the event
* @param expectedSerial the expected serial
*/
public PendingGet(GetDisplayPassword event, long expectedSerial,
CompletionLock lock) {
super();
this.event = event;
this.expectedSerial = expectedSerial;
this.lock = lock;
}
}
}

View file

@ -0,0 +1,106 @@
/*
* VM-Operator
* Copyright (C) 2023 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.manager;
import com.google.gson.JsonPrimitive;
import freemarker.template.TemplateException;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.models.V1ObjectMeta;
import io.kubernetes.client.openapi.models.V1Secret;
import io.kubernetes.client.util.generic.options.ListOptions;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Map;
import java.util.logging.Logger;
import org.jdrupes.vmoperator.common.K8sV1SecretStub;
import static org.jdrupes.vmoperator.manager.Constants.APP_NAME;
import static org.jdrupes.vmoperator.manager.Constants.COMP_DISPLAY_SECRET;
import static org.jdrupes.vmoperator.manager.Constants.DATA_DISPLAY_PASSWORD;
import static org.jdrupes.vmoperator.manager.Constants.DATA_PASSWORD_EXPIRY;
import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jdrupes.vmoperator.util.GsonPtr;
import org.jose4j.base64url.Base64;
/**
* Delegee for reconciling the display secret
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
/* default */ class DisplaySecretReconciler {
protected final Logger logger = Logger.getLogger(getClass().getName());
/**
* Reconcile.
*
* @param event the event
* @param model the model
* @param channel the channel
* @throws IOException Signals that an I/O exception has occurred.
* @throws TemplateException the template exception
* @throws ApiException the api exception
*/
public void reconcile(VmDefChanged event,
Map<String, Object> model, VmChannel channel)
throws IOException, TemplateException, ApiException {
// Secret needed at all?
var display = GsonPtr.to(event.vmDefinition().data()).to("spec", "vm",
"display");
if (!display.get(JsonPrimitive.class, "spice", "generateSecret")
.map(JsonPrimitive::getAsBoolean).orElse(false)) {
return;
}
// Check if exists
var metadata = event.vmDefinition().getMetadata();
ListOptions options = new ListOptions();
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
+ "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET + ","
+ "app.kubernetes.io/instance=" + metadata.getName());
var stubs = K8sV1SecretStub.list(channel.client(),
metadata.getNamespace(), options);
if (!stubs.isEmpty()) {
return;
}
// Create secret
var secret = new V1Secret();
secret.setMetadata(new V1ObjectMeta().namespace(metadata.getNamespace())
.name(metadata.getName() + "-" + COMP_DISPLAY_SECRET)
.putLabelsItem("app.kubernetes.io/name", APP_NAME)
.putLabelsItem("app.kubernetes.io/component", COMP_DISPLAY_SECRET)
.putLabelsItem("app.kubernetes.io/instance", metadata.getName()));
secret.setType("Opaque");
SecureRandom random = null;
try {
random = SecureRandom.getInstanceStrong();
} catch (NoSuchAlgorithmException e) { // NOPMD
// "Every implementation of the Java platform is required
// to support at least one strong SecureRandom implementation."
}
byte[] bytes = new byte[16];
random.nextBytes(bytes);
var password = Base64.encode(bytes);
secret.setStringData(Map.of(DATA_DISPLAY_PASSWORD, password,
DATA_PASSWORD_EXPIRY, "now"));
K8sV1SecretStub.create(channel.client(), secret);
}
}

View file

@ -135,6 +135,7 @@ public class Reconciler extends Component {
@SuppressWarnings("PMD.SingularField")
private final Configuration fmConfig;
private final ConfigMapReconciler cmReconciler;
private final DisplaySecretReconciler dsReconciler;
private final StatefulSetReconciler stsReconciler;
private final LoadBalancerReconciler lbReconciler;
@SuppressWarnings("PMD.UseConcurrentHashMap")
@ -159,6 +160,7 @@ public class Reconciler extends Component {
fmConfig.setClassForTemplateLoading(Reconciler.class, "");
cmReconciler = new ConfigMapReconciler(fmConfig);
dsReconciler = new DisplaySecretReconciler();
stsReconciler = new StatefulSetReconciler(fmConfig);
lbReconciler = new LoadBalancerReconciler(fmConfig);
}
@ -209,6 +211,7 @@ public class Reconciler extends Component {
= prepareModel(channel.client(), patchCr(event.vmDefinition()));
var configMap = cmReconciler.reconcile(event, model, channel);
model.put("cm", configMap.getRaw());
dsReconciler.reconcile(event, model, channel);
stsReconciler.reconcile(event, model, channel);
lbReconciler.reconcile(event, model, channel);
}

View file

@ -32,12 +32,14 @@ import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP;
import org.jdrupes.vmoperator.common.K8s;
import org.jdrupes.vmoperator.common.K8sClient;
import org.jdrupes.vmoperator.common.K8sDynamicModel;
import org.jdrupes.vmoperator.common.K8sDynamicModels;
import org.jdrupes.vmoperator.common.K8sDynamicStub;
import org.jdrupes.vmoperator.common.K8sObserver.ResponseType;
import org.jdrupes.vmoperator.common.K8sV1ConfigMapStub;
import org.jdrupes.vmoperator.common.K8sV1PodStub;
import org.jdrupes.vmoperator.common.K8sV1StatefulSetStub;
import org.jdrupes.vmoperator.common.VmDefinitionModel;
import org.jdrupes.vmoperator.common.VmDefinitionModels;
import org.jdrupes.vmoperator.common.VmDefinitionStub;
import static org.jdrupes.vmoperator.manager.Constants.APP_NAME;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_KIND_VM;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME;
@ -50,8 +52,8 @@ import org.jgrapes.core.Channel;
* Watches for changes of VM definitions.
*/
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports" })
public class VmMonitor
extends AbstractMonitor<K8sDynamicModel, K8sDynamicModels, VmChannel> {
public class VmMonitor extends
AbstractMonitor<VmDefinitionModel, VmDefinitionModels, VmChannel> {
/**
* Instantiates a new VM definition watcher.
@ -59,7 +61,8 @@ public class VmMonitor
* @param componentChannel the component channel
*/
public VmMonitor(Channel componentChannel) {
super(componentChannel, K8sDynamicModel.class, K8sDynamicModels.class);
super(componentChannel, VmDefinitionModel.class,
VmDefinitionModels.class);
}
@Override
@ -102,7 +105,7 @@ public class VmMonitor
@Override
protected void handleChange(K8sClient client,
Watch.Response<K8sDynamicModel> response) {
Watch.Response<VmDefinitionModel> response) {
V1ObjectMeta metadata = response.object.getMetadata();
VmChannel channel = channel(metadata.getName()).orElse(null);
if (channel == null) {
@ -138,9 +141,10 @@ public class VmMonitor
vmDef), channel);
}
private K8sDynamicModel getModel(K8sClient client, K8sDynamicModel vmDef) {
private VmDefinitionModel getModel(K8sClient client,
VmDefinitionModel vmDef) {
try {
return K8sDynamicStub.get(client, context(), namespace(),
return VmDefinitionStub.get(client, context(), namespace(),
vmDef.metadata().getName()).model().orElse(null);
} catch (ApiException e) {
return null;

View file

@ -40,7 +40,8 @@ import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM;
import org.jdrupes.vmoperator.common.K8s;
import org.jdrupes.vmoperator.common.K8sClient;
import org.jdrupes.vmoperator.common.K8sDynamicModel;
import org.jdrupes.vmoperator.common.K8sDynamicStub;
import org.jdrupes.vmoperator.common.VmDefinitionModel;
import org.jdrupes.vmoperator.common.VmDefinitionStub;
import org.jdrupes.vmoperator.runner.qemu.events.BalloonChangeEvent;
import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu;
import org.jdrupes.vmoperator.runner.qemu.events.DisplayPasswordChanged;
@ -73,7 +74,7 @@ public class StatusUpdater extends Component {
private long observedGeneration;
private boolean guestShutdownStops;
private boolean shutdownByGuest;
private K8sDynamicStub vmStub;
private VmDefinitionStub vmStub;
/**
* Instantiates a new status updater.
@ -158,7 +159,7 @@ public class StatusUpdater extends Component {
return;
}
try {
vmStub = K8sDynamicStub.get(apiClient,
vmStub = VmDefinitionStub.get(apiClient,
new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM),
namespace, vmName);
vmStub.model().ifPresent(model -> {
@ -226,7 +227,7 @@ public class StatusUpdater extends Component {
"PMD.AvoidLiteralsInIfCondition" })
public void onRunnerStateChanged(RunnerStateChange event)
throws ApiException {
K8sDynamicModel vmDef;
VmDefinitionModel vmDef;
if (vmStub == null || (vmDef = vmStub.model().orElse(null)) == null) {
return;
}

View file

@ -35,8 +35,8 @@ import java.util.Optional;
import java.util.Set;
import org.jdrupes.json.JsonBeanDecoder;
import org.jdrupes.json.JsonDecodeException;
import org.jdrupes.vmoperator.common.K8sDynamicModel;
import org.jdrupes.vmoperator.common.K8sObserver;
import org.jdrupes.vmoperator.common.VmDefinitionModel;
import org.jdrupes.vmoperator.manager.events.ChannelCache;
import org.jdrupes.vmoperator.manager.events.ModifyVm;
import org.jdrupes.vmoperator.manager.events.VmChannel;
@ -69,7 +69,7 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
private static final Set<RenderMode> MODES = RenderMode.asSet(
RenderMode.Preview, RenderMode.View);
private final ChannelCache<String, VmChannel,
K8sDynamicModel> channelManager = new ChannelCache<>();
VmDefinitionModel> channelManager = new ChannelCache<>();
private final TimeSeries summarySeries = new TimeSeries(Duration.ofDays(1));
private Summary cachedSummary;
@ -196,8 +196,8 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
}
}
} else {
var vmDef = new K8sDynamicModel(channel.client().getJSON()
.getGson(), convertQuantities(event.vmDefinition().data()));
var vmDef = new VmDefinitionModel(channel.client().getJSON()
.getGson(), cleanup(event.vmDefinition().data()));
channelManager.put(vmName, channel, vmDef);
var def = JsonBeanDecoder.create(vmDef.data().toString())
.readObject();
@ -220,7 +220,7 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
}
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
private JsonObject convertQuantities(JsonObject vmDef) {
private JsonObject cleanup(JsonObject vmDef) {
// Clone and remove managed fields
var json = vmDef.deepCopy();
GsonPtr.to(json).to("metadata").get(JsonObject.class)

View file

@ -100,7 +100,7 @@
[role=button] {
padding: 0.25rem;
&:not([aria-disabled]):hover {
&:not([aria-disabled]):hover, &[aria-disabled='false']:hover {
box-shadow: var(--darkening);
}
}

View file

@ -3,5 +3,6 @@ conletName = VM-Konsole
okayLabel = Anwenden und Schließen
Select\ VM = VM auswählen
Start\ VM = VM Starten
Stop\ VM = VM Anhalten
Start\ VM = VM starten
Stop\ VM = VM anhalten
Open\ console = Konsole anzeigen

View file

@ -1,6 +1,6 @@
/*
* VM-Operator
* Copyright (C) 2023 Michael N. Lipp
* Copyright (C) 2023,2024 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@ -22,6 +22,7 @@ import com.fasterxml.jackson.annotation.JsonGetter;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import freemarker.core.ParseException;
import freemarker.template.MalformedTemplateNameException;
@ -33,18 +34,23 @@ import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.time.Duration;
import java.util.Base64;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.logging.Level;
import org.bouncycastle.util.Objects;
import org.jdrupes.json.JsonBeanDecoder;
import org.jdrupes.json.JsonDecodeException;
import org.jdrupes.vmoperator.common.K8sDynamicModel;
import org.jdrupes.vmoperator.common.K8sObserver;
import org.jdrupes.vmoperator.common.VmDefinitionModel;
import org.jdrupes.vmoperator.common.VmDefinitionModel.Permission;
import org.jdrupes.vmoperator.manager.events.ChannelCache;
import org.jdrupes.vmoperator.manager.events.GetDisplayPassword;
import org.jdrupes.vmoperator.manager.events.ModifyVm;
@ -52,6 +58,7 @@ import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jdrupes.vmoperator.util.GsonPtr;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Components;
import org.jgrapes.core.Event;
import org.jgrapes.core.Manager;
import org.jgrapes.core.annotation.Handler;
@ -62,11 +69,15 @@ import org.jgrapes.util.events.KeyValueStoreUpdate;
import org.jgrapes.webconsole.base.Conlet.RenderMode;
import org.jgrapes.webconsole.base.ConletBaseModel;
import org.jgrapes.webconsole.base.ConsoleConnection;
import org.jgrapes.webconsole.base.ConsoleRole;
import org.jgrapes.webconsole.base.ConsoleUser;
import org.jgrapes.webconsole.base.WebConsoleUtils;
import org.jgrapes.webconsole.base.events.AddConletRequest;
import org.jgrapes.webconsole.base.events.AddConletType;
import org.jgrapes.webconsole.base.events.AddPageResources.ScriptResource;
import org.jgrapes.webconsole.base.events.ConletDeleted;
import org.jgrapes.webconsole.base.events.ConsoleConfigured;
import org.jgrapes.webconsole.base.events.ConsolePrepared;
import org.jgrapes.webconsole.base.events.ConsoleReady;
import org.jgrapes.webconsole.base.events.DeleteConlet;
import org.jgrapes.webconsole.base.events.NotifyConletModel;
@ -75,22 +86,32 @@ import org.jgrapes.webconsole.base.events.OpenModalDialog;
import org.jgrapes.webconsole.base.events.RenderConlet;
import org.jgrapes.webconsole.base.events.RenderConletRequestBase;
import org.jgrapes.webconsole.base.events.SetLocale;
import org.jgrapes.webconsole.base.events.UpdateConletType;
import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet;
/**
* The Class VmConlet.
*/
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports",
"PMD.CouplingBetweenObjects" })
"PMD.CouplingBetweenObjects", "PMD.GodClass" })
public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
private static final String VM_NAME_PROPERTY = "vmName";
private static final String RENDERED
= VmViewer.class.getName() + ".rendered";
private static final String PENDING
= VmViewer.class.getName() + ".pending";
private static final Set<RenderMode> MODES = RenderMode.asSet(
RenderMode.Preview, RenderMode.Edit);
private static final Set<RenderMode> MODES_FOR_GENERATED = RenderMode.asSet(
RenderMode.Preview, RenderMode.StickyPreview);
private final ChannelCache<String, VmChannel,
K8sDynamicModel> channelManager = new ChannelCache<>();
VmDefinitionModel> channelManager = new ChannelCache<>();
private static ObjectMapper objectMapper
= new ObjectMapper().registerModule(new JavaTimeModule());
private Class<?> preferredIpVersion = Inet4Address.class;
private final Set<String> syncUsers = new HashSet<>();
private final Set<String> syncRoles = new HashSet<>();
/**
* The periodically generated update event.
@ -114,24 +135,47 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
*
* @param event the event
*/
@SuppressWarnings("unchecked")
@Handler
public void onConfigurationUpdate(ConfigurationUpdate event) {
event.structured(componentPath()).ifPresent(c -> {
@SuppressWarnings("unchecked")
var dispRes = (Map<String, Object>) c
.getOrDefault("displayResource", Collections.emptyMap());
switch ((String) dispRes.getOrDefault("preferredIpVersion", "")) {
case "ipv6":
preferredIpVersion = Inet6Address.class;
break;
case "ipv4":
default:
preferredIpVersion = Inet4Address.class;
break;
try {
var dispRes = (Map<String, Object>) c
.getOrDefault("displayResource", Collections.emptyMap());
switch ((String) dispRes.getOrDefault("preferredIpVersion",
"")) {
case "ipv6":
preferredIpVersion = Inet6Address.class;
break;
case "ipv4":
default:
preferredIpVersion = Inet4Address.class;
break;
}
// Sync
for (var entry : (List<Map<String, String>>) c.getOrDefault(
"syncPreviewsFor", Collections.emptyList())) {
if (entry.containsKey("user")) {
syncUsers.add(entry.get("user"));
} else if (entry.containsKey("role")) {
syncRoles.add(entry.get("role"));
}
}
} catch (ClassCastException e) {
logger.config("Malformed configuration: " + e.getMessage());
}
});
}
private boolean syncPreviews(Session session) {
return WebConsoleUtils.userFromSession(session)
.filter(u -> syncUsers.contains(u.getName())).isPresent()
|| WebConsoleUtils.rolesFromSession(session).stream()
.filter(cr -> syncRoles.contains(cr.getName())).findAny()
.isPresent();
}
/**
* On {@link ConsoleReady}, fire the {@link AddConletType}.
*
@ -155,6 +199,61 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
.addScript(new ScriptResource().setScriptType("module")
.setScriptUri(event.renderSupport().conletResource(
type(), "VmViewer-functions.js"))));
channel.session().put(RENDERED, new HashSet<>());
}
/**
* On console configured.
*
* @param event the event
* @param connection the console connection
* @throws InterruptedException the interrupted exception
*/
@Handler
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
public void onConsoleConfigured(ConsoleConfigured event,
ConsoleConnection connection) throws InterruptedException,
IOException {
@SuppressWarnings("unchecked")
final var rendered = (Set<String>) connection.session().get(RENDERED);
connection.session().remove(RENDERED);
if (!syncPreviews(connection.session())) {
return;
}
boolean foundMissing = false;
for (var vmName : accessibleVms(connection)) {
if (rendered.contains(vmName)) {
continue;
}
if (!foundMissing) {
// Suspending to allow rendering of conlets to be noticed
var failSafe = Components.schedule(t -> event.resumeHandling(),
Duration.ofSeconds(1));
event.suspendHandling(failSafe::cancel);
connection.setAssociated(PENDING, event);
foundMissing = true;
}
fire(new AddConletRequest(event.event().event().renderSupport(),
VmViewer.class.getName(),
RenderMode.asSet(RenderMode.Preview))
.addProperty(VM_NAME_PROPERTY, vmName),
connection);
}
}
/**
* On console prepared.
*
* @param event the event
* @param connection the connection
*/
@Handler
public void onConsolePrepared(ConsolePrepared event,
ConsoleConnection connection) {
if (syncPreviews(connection.session())) {
connection.respond(new UpdateConletType(type()));
}
}
private String storagePath(Session session, String conletId) {
@ -163,6 +262,20 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
+ "/" + VmViewer.class.getName() + "/" + conletId;
}
@Override
protected Optional<ViewerModel> createNewState(AddConletRequest event,
ConsoleConnection connection, String conletId) throws Exception {
var model = new ViewerModel(conletId);
model.vmName = (String) event.properties().get(VM_NAME_PROPERTY);
if (model.vmName != null) {
model.setGenerated(true);
}
String jsonState = objectMapper.writeValueAsString(model);
connection.respond(new KeyValueStoreUpdate().update(
storagePath(connection.session(), model.getConletId()), jsonState));
return Optional.of(model);
}
@Override
protected Optional<ViewerModel> createStateRepresentation(Event<?> event,
ConsoleConnection connection, String conletId) throws Exception {
@ -197,32 +310,57 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
}
@Override
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
@SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops", "unchecked" })
protected Set<RenderMode> doRenderConlet(RenderConletRequestBase<?> event,
ConsoleConnection channel, String conletId, ViewerModel conletState)
ConsoleConnection channel, String conletId, ViewerModel model)
throws Exception {
ResourceBundle resourceBundle = resourceBundle(channel.locale());
Set<RenderMode> renderedAs = new HashSet<>();
if (event.renderAs().contains(RenderMode.Preview)) {
channel.associated(PENDING, Event.class)
.ifPresent(e -> {
e.resumeHandling();
channel.setAssociated(PENDING, null);
});
// Remove conlet if definition has been removed
if (model.vmName() != null
&& !channelManager.associated(model.vmName()).isPresent()) {
channel.respond(
new DeleteConlet(conletId, Collections.emptySet()));
return Collections.emptySet();
}
// Don't render if user has not at least one permission
if (model.vmName() != null
&& channelManager.associated(model.vmName())
.map(d -> permissions(d, channel.session()).isEmpty())
.orElse(true)) {
return Collections.emptySet();
}
// Render
Template tpl
= freemarkerConfig().getTemplate("VmViewer-preview.ftl.html");
channel.respond(new RenderConlet(type(), conletId,
processTemplate(event, tpl,
fmModel(event, channel, conletId, conletState)))
fmModel(event, channel, conletId, model)))
.setRenderAs(
RenderMode.Preview.addModifiers(event.renderAs()))
.setSupportedModes(MODES));
.setSupportedModes(
model.isGenerated() ? MODES_FOR_GENERATED : MODES));
renderedAs.add(RenderMode.Preview);
if (!Strings.isNullOrEmpty(conletState.vmName())) {
updateConfig(channel, conletState);
if (!Strings.isNullOrEmpty(model.vmName())) {
Optional.ofNullable(channel.session().get(RENDERED))
.ifPresent(s -> ((Set<String>) s).add(model.vmName()));
updateConfig(channel, model);
}
}
if (event.renderAs().contains(RenderMode.Edit)) {
Template tpl = freemarkerConfig()
.getTemplate("VmViewer-edit.ftl.html");
var fmModel = fmModel(event, channel, conletId, conletState);
fmModel.put("vmNames",
channelManager.keys().stream().sorted().toList());
var fmModel = fmModel(event, channel, conletId, model);
fmModel.put("vmNames", accessibleVms(channel));
channel.respond(new OpenModalDialog(type(), conletId,
processTemplate(event, tpl, fmModel))
.addOption("cancelable", true)
@ -232,6 +370,21 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
return renderedAs;
}
private List<String> accessibleVms(ConsoleConnection channel) {
return channelManager.associated().stream()
.filter(d -> !permissions(d, channel.session()).isEmpty())
.map(d -> d.getMetadata().getName()).sorted().toList();
}
private Set<Permission> permissions(VmDefinitionModel vmDef,
Session session) {
var user = WebConsoleUtils.userFromSession(session)
.map(ConsoleUser::getName).orElse(null);
var roles = WebConsoleUtils.rolesFromSession(session)
.stream().map(ConsoleRole::getName).toList();
return vmDef.permissionsFor(user, roles);
}
private void updateConfig(ConsoleConnection channel, ViewerModel model) {
channel.respond(new NotifyConletView(type(),
model.getConletId(), "updateConfig", model.vmName()));
@ -246,6 +399,9 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
try {
var def = JsonBeanDecoder.create(vmDef.data().toString())
.readObject();
def.setField("userPermissions",
permissions(vmDef, channel.session()).stream()
.map(Permission::toString).toList());
channel.respond(new NotifyConletView(type(),
model.getConletId(), "updateVmDefinition", def));
} catch (JsonDecodeException e) {
@ -279,8 +435,10 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
"PMD.ConfusingArgumentToVarargsMethod" })
public void onVmDefChanged(VmDefChanged event, VmChannel channel)
throws JsonDecodeException, IOException {
var vmDef = new K8sDynamicModel(channel.client().getJSON()
var vmDef = new VmDefinitionModel(channel.client().getJSON()
.getGson(), event.vmDefinition().data());
GsonPtr.to(vmDef.data()).to("metadata").get(JsonObject.class)
.remove("managedFields");
var vmName = vmDef.getMetadata().getName();
if (event.type() == K8sObserver.ResponseType.DELETED) {
channelManager.remove(vmName);
@ -291,7 +449,8 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
var connection = entry.getKey();
for (var conletId : entry.getValue()) {
var model = stateFromSession(connection.session(), conletId);
if (model.isEmpty() || !model.get().vmName().equals(vmName)) {
if (model.isEmpty()
|| !Objects.areEqual(model.get().vmName(), vmName)) {
continue;
}
if (event.type() == K8sObserver.ResponseType.DELETED) {
@ -311,11 +470,15 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
ConsoleConnection channel, ViewerModel model)
throws Exception {
event.stop();
var vmName = event.params().asString(0);
var vmChannel = channelManager.channel(vmName).orElse(null);
if (vmChannel == null) {
var both = Optional.ofNullable(event.params().asString(0))
.flatMap(vm -> channelManager.both(vm));
if (both.isEmpty()) {
return;
}
var vmChannel = both.get().channel;
var vmDef = both.get().associated;
var vmName = vmDef.metadata().getName();
var perms = permissions(vmDef, channel.session());
switch (event.method()) {
case "selectedVm":
model.setVmName(event.params().asString(0));
@ -325,15 +488,22 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
updateConfig(channel, model);
break;
case "start":
fire(new ModifyVm(vmName, "state", "Running", vmChannel));
if (perms.contains(Permission.START)) {
fire(new ModifyVm(vmName, "state", "Running", vmChannel));
}
break;
case "stop":
fire(new ModifyVm(vmName, "state", "Stopped", vmChannel));
if (perms.contains(Permission.STOP)) {
fire(new ModifyVm(vmName, "state", "Stopped", vmChannel));
}
break;
case "openConsole":
channelManager.channel(vmName).ifPresent(
vc -> fire(Event.onCompletion(new GetDisplayPassword(vmName),
ds -> openConsole(vmName, channel, model, ds)), vc));
if (perms.contains(Permission.ACCESS_CONSOLE)) {
var pwQuery = Event.onCompletion(new GetDisplayPassword(vmDef),
e -> e.password().ifPresent(
pw -> openConsole(vmName, channel, model, pw)));
fire(pwQuery, vmChannel);
}
break;
default:// ignore
break;
@ -341,7 +511,7 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
}
private void openConsole(String vmName, ConsoleConnection connection,
ViewerModel model, GetDisplayPassword pwQuery) {
ViewerModel model, String password) {
var vmDef = channelManager.associated(vmName).orElse(null);
if (vmDef == null) {
return;
@ -362,10 +532,8 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
StringBuffer data = new StringBuffer(100)
.append("[virt-viewer]\ntype=spice\nhost=")
.append(addr.get().getHostAddress()).append("\nport=")
.append(Integer.toString(port.get().getAsInt())).append('\n');
pwQuery.password().ifPresent(p -> {
data.append("password=").append(p).append('\n');
});
.append(Integer.toString(port.get().getAsInt()))
.append("\npassword=").append(password).append('\n');
proxyUrl.map(JsonPrimitive::getAsString).ifPresent(u -> {
if (!Strings.isNullOrEmpty(u)) {
data.append("proxy=").append(u).append('\n');
@ -418,9 +586,11 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
/**
* The Class VmsModel.
*/
@SuppressWarnings("PMD.DataClass")
public static class ViewerModel extends ConletBaseModel {
private String vmName;
private boolean generated;
/**
* Instantiates a new vms model.
@ -450,5 +620,23 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
this.vmName = vmName;
}
/**
* Checks if is generated.
*
* @return the generated
*/
public boolean isGenerated() {
return generated;
}
/**
* Sets the generated.
*
* @param generated the generated to set
*/
public void setGenerated(boolean generated) {
this.generated = generated;
}
}
}

View file

@ -84,17 +84,22 @@ window.orgJDrupesVmOperatorVmViewer.initPreview = (previewDom: HTMLElement,
<tbody>
<tr>
<td><img role=button
:aria-disabled="!vmDef.running || !vmDef.userPermissions
|| !vmDef.userPermissions.includes('accessConsole')"
v-on:click="vmAction(vmDef.name, 'openConsole')"
:src="resourceBase + (vmDef.running
? 'computer.svg' : 'computer-off.svg')"></td>
? 'computer.svg' : 'computer-off.svg')"
:title="localize('Open console')"></td>
<td v-if="vmDef.spec"
class="jdrupes-vmoperator-vmviewer-preview-action-list">
<span role="button" v-if="vmDef.spec.vm.state != 'Running'"
tabindex="0" class="fa fa-play" :title="localize('Start VM')"
<span role="button" v-if="vmDef.spec.vm.state != 'Running'"
:aria-disabled="!vmDef.userPermissions.includes('start')"
tabindex="0" class="fa fa-play" :title="localize('Start VM')"
v-on:click="vmAction(vmDef.name, 'start')"></span>
<span role="button" v-else class="fa fa-play"
aria-disabled="true" :title="localize('Start VM')"></span>
<span role="button" v-if="vmDef.spec.vm.state != 'Stopped'"
:aria-disabled="!vmDef.userPermissions.includes('stop')"
tabindex="0" class="fa fa-stop" :title="localize('Stop VM')"
v-on:click="vmAction(vmDef.name, 'stop')"></span>
<span role="button" v-else class="fa fa-stop"

View file

@ -19,24 +19,27 @@
/*
* Conlet specific styles.
*/
.jdrupes-vmoperator-vmviewer-preview img {
height: 3em;
padding: 0.25rem;
&:hover {
box-shadow: var(--darkening);
.jdrupes-vmoperator-vmviewer-preview {
[role=button] {
padding: 0.25rem;
&:not([aria-disabled]):hover, &[aria-disabled='false']:hover {
box-shadow: var(--darkening);
}
}
img {
height: 3em;
padding: 0.25rem;
&[aria-disabled=''], &[aria-disabled='true'] {
opacity: 0.4;
}
}
}
.jdrupes-vmoperator-vmviewer-preview-action-list {
white-space: nowrap;
[role=button] {
padding: 0.25rem;
&:not([aria-disabled]):hover {
box-shadow: var(--darkening);
}
}
}