Provide ACLs (together with general improvements) for the viewer conlet.
This commit is contained in:
parent
a6525a2289
commit
659463b3b4
42 changed files with 1664 additions and 679 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)'
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -17,3 +17,4 @@
|
|||
#
|
||||
|
||||
consoleTitle = VM-Operator
|
||||
introTitle = Usage
|
||||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue