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.
|
description: Copied to cloud-init's network-config file.
|
||||||
type: object
|
type: object
|
||||||
x-kubernetes-preserve-unknown-fields: true
|
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:
|
vm:
|
||||||
type: object
|
type: object
|
||||||
description: Defines the VM.
|
description: Defines the VM.
|
||||||
|
|
@ -1395,6 +1419,9 @@ spec:
|
||||||
to the spice server. Defaults to the address
|
to the spice server. Defaults to the address
|
||||||
of the node that the VM is running on.
|
of the node that the VM is running on.
|
||||||
type: string
|
type: string
|
||||||
|
generateSecret:
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
proxyUrl:
|
proxyUrl:
|
||||||
description: >-
|
description: >-
|
||||||
If specified, is copied to the generated
|
If specified, is copied to the generated
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@
|
||||||
# User admin has role admin
|
# User admin has role admin
|
||||||
admin:
|
admin:
|
||||||
- admin
|
- admin
|
||||||
|
test:
|
||||||
|
- user
|
||||||
# All users have role other
|
# All users have role other
|
||||||
"*":
|
"*":
|
||||||
- other
|
- other
|
||||||
|
|
@ -37,11 +39,14 @@
|
||||||
# Admins can use all conlets
|
# Admins can use all conlets
|
||||||
admin:
|
admin:
|
||||||
- "*"
|
- "*"
|
||||||
|
user:
|
||||||
|
- org.jdrupes.vmoperator.vmviewer.VmViewer
|
||||||
# Others cannot use any conlet (except login conlet to log out)
|
# Others cannot use any conlet (except login conlet to log out)
|
||||||
other:
|
other:
|
||||||
- --org.jdrupes.vmoperator.vmconlet.VmConlet
|
|
||||||
- org.jgrapes.webconlet.oidclogin.LoginConlet
|
- org.jgrapes.webconlet.oidclogin.LoginConlet
|
||||||
"/ComponentCollector":
|
"/ComponentCollector":
|
||||||
"/VmViewer":
|
"/VmViewer":
|
||||||
displayResource:
|
displayResource:
|
||||||
preferredIpVersion: ipv4
|
preferredIpVersion: ipv4
|
||||||
|
syncPreviewsFor:
|
||||||
|
- role: user
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,8 @@ patches:
|
||||||
# User admin has role admin
|
# User admin has role admin
|
||||||
admin:
|
admin:
|
||||||
- admin
|
- admin
|
||||||
|
test:
|
||||||
|
- user
|
||||||
# All users have role other
|
# All users have role other
|
||||||
"*":
|
"*":
|
||||||
- other
|
- other
|
||||||
|
|
@ -63,6 +65,8 @@ patches:
|
||||||
# Admins can use all conlets
|
# Admins can use all conlets
|
||||||
admin:
|
admin:
|
||||||
- "*"
|
- "*"
|
||||||
|
user:
|
||||||
|
- org.jdrupes.vmoperator.vmviewer.VmViewer
|
||||||
# Others cannot use any conlet (except login conlet to log out)
|
# Others cannot use any conlet (except login conlet to log out)
|
||||||
other:
|
other:
|
||||||
- org.jgrapes.webconlet.locallogin.LoginConlet
|
- org.jgrapes.webconlet.locallogin.LoginConlet
|
||||||
|
|
@ -70,6 +74,8 @@ patches:
|
||||||
"/VmViewer":
|
"/VmViewer":
|
||||||
displayResource:
|
displayResource:
|
||||||
preferredIpVersion: ipv4
|
preferredIpVersion: ipv4
|
||||||
|
syncPreviewsFor:
|
||||||
|
- role: user
|
||||||
- target:
|
- target:
|
||||||
group: apps
|
group: apps
|
||||||
version: v1
|
version: v1
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,17 @@ spec:
|
||||||
image:
|
image:
|
||||||
repository: docker-registry.lan.mnl.de
|
repository: docker-registry.lan.mnl.de
|
||||||
path: vmoperator/org.jdrupes.vmoperator.runner.qemu-alpine
|
path: vmoperator/org.jdrupes.vmoperator.runner.qemu-alpine
|
||||||
|
version: latest
|
||||||
pullPolicy: Always
|
pullPolicy: Always
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
- user: admin
|
||||||
|
may:
|
||||||
|
- "*"
|
||||||
|
- user: test
|
||||||
|
may:
|
||||||
|
- "accessConsole"
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
cpu: 1
|
cpu: 1
|
||||||
|
|
@ -52,3 +61,4 @@ spec:
|
||||||
display:
|
display:
|
||||||
spice:
|
spice:
|
||||||
port: 5810
|
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;
|
package org.jdrupes.vmoperator.common;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.JsonElement;
|
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import io.kubernetes.client.common.KubernetesListObject;
|
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
|
* 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
|
* notably the metadata, is made available through the methods
|
||||||
* defined by {@link KubernetesListObject}.
|
* defined by {@link KubernetesListObject}.
|
||||||
*/
|
*/
|
||||||
public class K8sDynamicModels implements KubernetesListObject {
|
public class K8sDynamicModels extends K8sDynamicModelsBase<K8sDynamicModel> {
|
||||||
|
|
||||||
private final JsonObject data;
|
|
||||||
private final V1ListMeta metadata;
|
|
||||||
private final List<K8sDynamicModel> items;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the object list using the given JSON data.
|
* Initialize the object list using the given JSON data.
|
||||||
|
|
@ -48,116 +38,7 @@ public class K8sDynamicModels implements KubernetesListObject {
|
||||||
* @param data the data
|
* @param data the data
|
||||||
*/
|
*/
|
||||||
public K8sDynamicModels(Gson delegate, JsonObject data) {
|
public K8sDynamicModels(Gson delegate, JsonObject data) {
|
||||||
this.data = data;
|
super(K8sDynamicModel.class, delegate, 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()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@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;
|
package org.jdrupes.vmoperator.common;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import io.kubernetes.client.Discovery.APIResource;
|
import io.kubernetes.client.Discovery.APIResource;
|
||||||
import io.kubernetes.client.apimachinery.GroupVersionKind;
|
import io.kubernetes.client.apimachinery.GroupVersionKind;
|
||||||
import io.kubernetes.client.openapi.ApiClient;
|
|
||||||
import io.kubernetes.client.openapi.ApiException;
|
import io.kubernetes.client.openapi.ApiException;
|
||||||
import io.kubernetes.client.util.generic.options.ListOptions;
|
import io.kubernetes.client.util.generic.options.ListOptions;
|
||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
|
|
@ -35,40 +33,23 @@ import java.util.Collection;
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
|
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
|
||||||
public class K8sDynamicStub
|
public class K8sDynamicStub
|
||||||
extends K8sGenericStub<K8sDynamicModel, K8sDynamicModels> {
|
extends K8sDynamicStubBase<K8sDynamicModel, K8sDynamicModels> {
|
||||||
|
|
||||||
|
private static DynamicTypeAdapterFactory<K8sDynamicModel,
|
||||||
|
K8sDynamicModels> taf = new K8sDynamicModelTypeAdapterFactory();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiates a new dynamic stub.
|
* Instantiates a new dynamic stub.
|
||||||
*
|
*
|
||||||
* @param objectClass the object class
|
|
||||||
* @param objectListClass the object list class
|
|
||||||
* @param client the client
|
* @param client the client
|
||||||
* @param context the context
|
* @param context the context
|
||||||
* @param namespace the namespace
|
* @param namespace the namespace
|
||||||
* @param name the name
|
* @param name the name
|
||||||
*/
|
*/
|
||||||
public K8sDynamicStub(Class<K8sDynamicModel> objectClass,
|
public K8sDynamicStub(K8sClient client,
|
||||||
Class<K8sDynamicModels> objectListClass, K8sClient client,
|
|
||||||
APIResource context, String namespace, String name) {
|
APIResource context, String namespace, String name) {
|
||||||
super(objectClass, objectListClass, client, context, namespace, name);
|
super(K8sDynamicModel.class, K8sDynamicModels.class, taf, 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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -88,8 +69,8 @@ public class K8sDynamicStub
|
||||||
public static K8sDynamicStub get(K8sClient client,
|
public static K8sDynamicStub get(K8sClient client,
|
||||||
GroupVersionKind gvk, String namespace, String name)
|
GroupVersionKind gvk, String namespace, String name)
|
||||||
throws ApiException {
|
throws ApiException {
|
||||||
return K8sGenericStub.get(K8sDynamicModel.class, K8sDynamicModels.class,
|
return new K8sDynamicStub(client, apiResource(client, gvk), namespace,
|
||||||
client, gvk, namespace, name, K8sDynamicStub::new);
|
name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -106,8 +87,7 @@ public class K8sDynamicStub
|
||||||
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
|
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
|
||||||
public static K8sDynamicStub get(K8sClient client,
|
public static K8sDynamicStub get(K8sClient client,
|
||||||
APIResource context, String namespace, String name) {
|
APIResource context, String namespace, String name) {
|
||||||
return K8sGenericStub.get(K8sDynamicModel.class, K8sDynamicModels.class,
|
return new K8sDynamicStub(client, context, namespace, name);
|
||||||
client, context, namespace, name, K8sDynamicStub::new);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -125,7 +105,7 @@ public class K8sDynamicStub
|
||||||
K8s.yamlToJson(client, yaml));
|
K8s.yamlToJson(client, yaml));
|
||||||
return K8sGenericStub.create(K8sDynamicModel.class,
|
return K8sGenericStub.create(K8sDynamicModel.class,
|
||||||
K8sDynamicModels.class, client, context, model,
|
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 {
|
throws ApiException {
|
||||||
return K8sGenericStub.list(K8sDynamicModel.class,
|
return K8sGenericStub.list(K8sDynamicModel.class,
|
||||||
K8sDynamicModels.class, client, context, namespace, options,
|
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());
|
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.openapi.ApiException;
|
||||||
import io.kubernetes.client.util.Strings;
|
import io.kubernetes.client.util.Strings;
|
||||||
import io.kubernetes.client.util.generic.GenericKubernetesApi;
|
import io.kubernetes.client.util.generic.GenericKubernetesApi;
|
||||||
|
import io.kubernetes.client.util.generic.KubernetesApiResponse;
|
||||||
import io.kubernetes.client.util.generic.options.GetOptions;
|
import io.kubernetes.client.util.generic.options.GetOptions;
|
||||||
import io.kubernetes.client.util.generic.options.ListOptions;
|
import io.kubernetes.client.util.generic.options.ListOptions;
|
||||||
import io.kubernetes.client.util.generic.options.PatchOptions;
|
import io.kubernetes.client.util.generic.options.PatchOptions;
|
||||||
|
import io.kubernetes.client.util.generic.options.UpdateOptions;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
|
@ -228,7 +230,8 @@ public class K8sGenericStub<O extends KubernetesObject,
|
||||||
public Optional<O> patch(String patchType, V1Patch patch,
|
public Optional<O> patch(String patchType, V1Patch patch,
|
||||||
PatchOptions options) throws ApiException {
|
PatchOptions options) throws ApiException {
|
||||||
return K8s
|
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);
|
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.
|
* A supplier for generic stubs.
|
||||||
*
|
*
|
||||||
|
|
@ -258,17 +285,13 @@ public class K8sGenericStub<O extends KubernetesObject,
|
||||||
/**
|
/**
|
||||||
* Gets a new stub.
|
* Gets a new stub.
|
||||||
*
|
*
|
||||||
* @param objectClass the object class
|
|
||||||
* @param objectListClass the object list class
|
|
||||||
* @param client the client
|
* @param client the client
|
||||||
* @param context the API resource
|
|
||||||
* @param namespace the namespace
|
* @param namespace the namespace
|
||||||
* @param name the name
|
* @param name the name
|
||||||
* @return the result
|
* @return the result
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("PMD.UseObjectForClearerAPI")
|
@SuppressWarnings("PMD.UseObjectForClearerAPI")
|
||||||
R get(Class<O> objectClass, Class<L> objectListClass, K8sClient client,
|
R get(K8sClient client, String namespace, String name);
|
||||||
APIResource context, String namespace, String name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -278,68 +301,6 @@ public class K8sGenericStub<O extends KubernetesObject,
|
||||||
+ version().toUpperCase() + kind() + " " + namespace + ":" + name;
|
+ version().toUpperCase() + kind() + " " + namespace + ":" + name;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a namespaced object stub. If the version in parameter
|
|
||||||
* `gvk` is an empty string, the stub refers to the first object
|
|
||||||
* found with matching group and kind.
|
|
||||||
*
|
|
||||||
* @param <O> the object type
|
|
||||||
* @param <L> the object list type
|
|
||||||
* @param <R> the stub type
|
|
||||||
* @param objectClass the object class
|
|
||||||
* @param objectListClass the object list class
|
|
||||||
* @param client the client
|
|
||||||
* @param gvk the group, version and kind
|
|
||||||
* @param namespace the namespace
|
|
||||||
* @param name the name
|
|
||||||
* @param provider the provider
|
|
||||||
* @return the stub if the object exists
|
|
||||||
* @throws ApiException the api exception
|
|
||||||
*/
|
|
||||||
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop" })
|
|
||||||
public static <O extends KubernetesObject, L extends KubernetesListObject,
|
|
||||||
R extends K8sGenericStub<O, L>>
|
|
||||||
R get(Class<O> objectClass, Class<L> objectListClass,
|
|
||||||
K8sClient client, GroupVersionKind gvk, String namespace,
|
|
||||||
String name, GenericSupplier<O, L, R> provider)
|
|
||||||
throws ApiException {
|
|
||||||
var context = K8s.context(client, gvk.getGroup(), gvk.getVersion(),
|
|
||||||
gvk.getKind());
|
|
||||||
if (context.isEmpty()) {
|
|
||||||
throw new ApiException("No known API for " + gvk.getGroup()
|
|
||||||
+ "/" + gvk.getVersion() + " " + gvk.getKind());
|
|
||||||
}
|
|
||||||
return provider.get(objectClass, objectListClass, client, context.get(),
|
|
||||||
namespace, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a namespaced object stub.
|
|
||||||
*
|
|
||||||
* @param <O> the object type
|
|
||||||
* @param <L> the object list type
|
|
||||||
* @param <R> the stub type
|
|
||||||
* @param objectClass the object class
|
|
||||||
* @param objectListClass the object list class
|
|
||||||
* @param client the client
|
|
||||||
* @param context the context
|
|
||||||
* @param namespace the namespace
|
|
||||||
* @param name the name
|
|
||||||
* @param provider the provider
|
|
||||||
* @return the stub if the object exists
|
|
||||||
* @throws ApiException the api exception
|
|
||||||
*/
|
|
||||||
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
|
|
||||||
"PMD.UseObjectForClearerAPI" })
|
|
||||||
public static <O extends KubernetesObject, L extends KubernetesListObject,
|
|
||||||
R extends K8sGenericStub<O, L>>
|
|
||||||
R get(Class<O> objectClass, Class<L> objectListClass,
|
|
||||||
K8sClient client, APIResource context, String namespace,
|
|
||||||
String name, GenericSupplier<O, L, R> provider) {
|
|
||||||
return provider.get(objectClass, objectListClass, client,
|
|
||||||
context, namespace, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a namespaced object stub for a newly created object.
|
* 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.getGroup(), context.getPreferredVersion(),
|
||||||
context.getResourcePlural(), client);
|
context.getResourcePlural(), client);
|
||||||
api.create(model).throwsApiException();
|
api.create(model).throwsApiException();
|
||||||
return provider.get(objectClass, objectListClass, client,
|
return provider.get(client, model.getMetadata().getNamespace(),
|
||||||
context, model.getMetadata().getNamespace(),
|
|
||||||
model.getMetadata().getName());
|
model.getMetadata().getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -402,8 +362,8 @@ public class K8sGenericStub<O extends KubernetesObject,
|
||||||
client);
|
client);
|
||||||
var objs = api.list(namespace, options).throwsApiException();
|
var objs = api.list(namespace, options).throwsApiException();
|
||||||
for (var item : objs.getObject().getItems()) {
|
for (var item : objs.getObject().getItems()) {
|
||||||
result.add(provider.get(objectClass, objectListClass, client,
|
result.add(provider.get(client, namespace,
|
||||||
context, namespace, item.getMetadata().getName()));
|
item.getMetadata().getName()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -416,4 +376,23 @@ public class K8sGenericStub<O extends KubernetesObject,
|
||||||
return result;
|
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,
|
public static Collection<K8sV1PodStub> list(K8sClient client,
|
||||||
String namespace, ListOptions options) throws ApiException {
|
String namespace, ListOptions options) throws ApiException {
|
||||||
return K8sGenericStub.list(V1Pod.class, V1PodList.class, client,
|
return K8sGenericStub.list(V1Pod.class, V1PodList.class, client,
|
||||||
CONTEXT, namespace, options, K8sV1PodStub::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 io.kubernetes.client.util.generic.options.ListOptions;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.jdrupes.vmoperator.common.K8sGenericStub.GenericSupplier;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A stub for secrets (v1).
|
* A stub for secrets (v1).
|
||||||
|
|
@ -62,6 +61,20 @@ public class K8sV1SecretStub extends K8sGenericStub<V1Secret, V1SecretList> {
|
||||||
return new K8sV1SecretStub(client, namespace, name);
|
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
|
* Get the stubs for the objects in the given namespace that match
|
||||||
* the criteria from the given options.
|
* the criteria from the given options.
|
||||||
|
|
@ -75,18 +88,6 @@ public class K8sV1SecretStub extends K8sGenericStub<V1Secret, V1SecretList> {
|
||||||
public static Collection<K8sV1SecretStub> list(K8sClient client,
|
public static Collection<K8sV1SecretStub> list(K8sClient client,
|
||||||
String namespace, ListOptions options) throws ApiException {
|
String namespace, ListOptions options) throws ApiException {
|
||||||
return K8sGenericStub.list(V1Secret.class, V1SecretList.class, client,
|
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 io.kubernetes.client.util.generic.options.ListOptions;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.jdrupes.vmoperator.common.K8sGenericStub.GenericSupplier;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A stub for secrets (v1).
|
* A stub for secrets (v1).
|
||||||
|
|
@ -75,18 +74,7 @@ public class K8sV1ServiceStub extends K8sGenericStub<V1Service, V1ServiceList> {
|
||||||
public static Collection<K8sV1ServiceStub> list(K8sClient client,
|
public static Collection<K8sV1ServiceStub> list(K8sClient client,
|
||||||
String namespace, ListOptions options) throws ApiException {
|
String namespace, ListOptions options) throws ApiException {
|
||||||
return K8sGenericStub.list(V1Service.class, V1ServiceList.class, client,
|
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;
|
package org.jdrupes.vmoperator.manager.events;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import org.jdrupes.vmoperator.common.VmDefinitionModel;
|
||||||
import org.jgrapes.core.Event;
|
import org.jgrapes.core.Event;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the current display secret.
|
* Gets the current display secret and optionally updates it.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("PMD.DataClass")
|
@SuppressWarnings("PMD.DataClass")
|
||||||
public class GetDisplayPassword extends Event<String> {
|
public class GetDisplayPassword extends Event<String> {
|
||||||
|
|
||||||
private final String vmName;
|
private final VmDefinitionModel vmDef;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiates a new returns the display secret.
|
* Instantiates a new returns the display secret.
|
||||||
*
|
*
|
||||||
* @param vmName the vm name
|
* @param vmDef the vm name
|
||||||
*/
|
*/
|
||||||
public GetDisplayPassword(String vmName) {
|
public GetDisplayPassword(VmDefinitionModel vmDef) {
|
||||||
this.vmName = vmName;
|
this.vmDef = vmDef;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the vm name.
|
* Gets the vm definition.
|
||||||
*
|
*
|
||||||
* @return the vm name
|
* @return the vm definition
|
||||||
*/
|
*/
|
||||||
public String vmName() {
|
public VmDefinitionModel vmDefinition() {
|
||||||
return vmName;
|
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
|
* @return the optional
|
||||||
*/
|
*/
|
||||||
public Optional<String> password() {
|
public Optional<String> password() {
|
||||||
|
if (!isDone()) {
|
||||||
|
throw new IllegalStateException("Event is not done.");
|
||||||
|
}
|
||||||
return currentResults().stream().findFirst();
|
return currentResults().stream().findFirst();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
package org.jdrupes.vmoperator.manager.events;
|
package org.jdrupes.vmoperator.manager.events;
|
||||||
|
|
||||||
import org.jdrupes.vmoperator.common.K8sClient;
|
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.Channel;
|
||||||
import org.jgrapes.core.EventPipeline;
|
import org.jgrapes.core.EventPipeline;
|
||||||
import org.jgrapes.core.Subchannel.DefaultSubchannel;
|
import org.jgrapes.core.Subchannel.DefaultSubchannel;
|
||||||
|
|
@ -32,7 +32,7 @@ public class VmChannel extends DefaultSubchannel {
|
||||||
|
|
||||||
private final EventPipeline pipeline;
|
private final EventPipeline pipeline;
|
||||||
private final K8sClient client;
|
private final K8sClient client;
|
||||||
private K8sDynamicModel vmDefinition;
|
private VmDefinitionModel vmDefinition;
|
||||||
private long generation = -1;
|
private long generation = -1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -56,7 +56,7 @@ public class VmChannel extends DefaultSubchannel {
|
||||||
* @return the watch channel
|
* @return the watch channel
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("PMD.LinguisticNaming")
|
@SuppressWarnings("PMD.LinguisticNaming")
|
||||||
public VmChannel setVmDefinition(K8sDynamicModel definition) {
|
public VmChannel setVmDefinition(VmDefinitionModel definition) {
|
||||||
this.vmDefinition = definition;
|
this.vmDefinition = definition;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
@ -66,7 +66,7 @@ public class VmChannel extends DefaultSubchannel {
|
||||||
*
|
*
|
||||||
* @return the json object
|
* @return the json object
|
||||||
*/
|
*/
|
||||||
public K8sDynamicModel vmDefinition() {
|
public VmDefinitionModel vmDefinition() {
|
||||||
return vmDefinition;
|
return vmDefinition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@
|
||||||
|
|
||||||
package org.jdrupes.vmoperator.manager.events;
|
package org.jdrupes.vmoperator.manager.events;
|
||||||
|
|
||||||
import org.jdrupes.vmoperator.common.K8sDynamicModel;
|
|
||||||
import org.jdrupes.vmoperator.common.K8sObserver;
|
import org.jdrupes.vmoperator.common.K8sObserver;
|
||||||
|
import org.jdrupes.vmoperator.common.VmDefinitionModel;
|
||||||
import org.jgrapes.core.Channel;
|
import org.jgrapes.core.Channel;
|
||||||
import org.jgrapes.core.Components;
|
import org.jgrapes.core.Components;
|
||||||
import org.jgrapes.core.Event;
|
import org.jgrapes.core.Event;
|
||||||
|
|
@ -36,7 +36,7 @@ public class VmDefChanged extends Event<Void> {
|
||||||
|
|
||||||
private final K8sObserver.ResponseType type;
|
private final K8sObserver.ResponseType type;
|
||||||
private final boolean specChanged;
|
private final boolean specChanged;
|
||||||
private final K8sDynamicModel vmDef;
|
private final VmDefinitionModel vmDef;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiates a new VM changed event.
|
* Instantiates a new VM changed event.
|
||||||
|
|
@ -46,7 +46,7 @@ public class VmDefChanged extends Event<Void> {
|
||||||
* @param vmDefinition the VM definition
|
* @param vmDefinition the VM definition
|
||||||
*/
|
*/
|
||||||
public VmDefChanged(K8sObserver.ResponseType type, boolean specChanged,
|
public VmDefChanged(K8sObserver.ResponseType type, boolean specChanged,
|
||||||
K8sDynamicModel vmDefinition) {
|
VmDefinitionModel vmDefinition) {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.specChanged = specChanged;
|
this.specChanged = specChanged;
|
||||||
this.vmDef = vmDefinition;
|
this.vmDef = vmDefinition;
|
||||||
|
|
@ -73,7 +73,7 @@ public class VmDefChanged extends Event<Void> {
|
||||||
*
|
*
|
||||||
* @return the object.
|
* @return the object.
|
||||||
*/
|
*/
|
||||||
public K8sDynamicModel vmDefinition() {
|
public VmDefinitionModel vmDefinition() {
|
||||||
return vmDef;
|
return vmDef;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ dependencies {
|
||||||
implementation 'org.jgrapes:org.jgrapes.webconsole.vuejs:[1.5.0,2)'
|
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.webconsole.rbac:[1.3.0,2)'
|
||||||
implementation 'org.jgrapes:org.jgrapes.webconlet.oidclogin:[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.sysinfo:[1.4.0,2)'
|
||||||
runtimeOnly 'org.jgrapes:org.jgrapes.webconlet.logviewer:[0.2.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
|
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;
|
package org.jdrupes.vmoperator.manager;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
import java.io.IOException;
|
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.Channel;
|
||||||
import org.jgrapes.core.Component;
|
import org.jgrapes.core.Component;
|
||||||
import org.jgrapes.core.annotation.Handler;
|
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.ConsoleConnection;
|
||||||
import org.jgrapes.webconsole.base.events.AddConletRequest;
|
import org.jgrapes.webconsole.base.events.AddConletRequest;
|
||||||
import org.jgrapes.webconsole.base.events.ConsoleConfigured;
|
import org.jgrapes.webconsole.base.events.ConsoleConfigured;
|
||||||
|
|
@ -63,11 +69,14 @@ public class AvoidEmptyPolicy extends Component {
|
||||||
* @param event the event
|
* @param event the event
|
||||||
* @param connection the connection
|
* @param connection the connection
|
||||||
*/
|
*/
|
||||||
@Handler
|
@Handler(priority = 100)
|
||||||
public void onRenderConlet(RenderConlet event,
|
public void onRenderConlet(RenderConlet event,
|
||||||
ConsoleConnection connection) {
|
ConsoleConnection connection) {
|
||||||
|
if (event.renderAs().contains(RenderMode.Preview)
|
||||||
|
|| event.renderAs().contains(RenderMode.View)) {
|
||||||
connection.session().put(renderedFlagName, true);
|
connection.session().put(renderedFlagName, true);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* On console configured.
|
* On console configured.
|
||||||
|
|
@ -76,18 +85,42 @@ public class AvoidEmptyPolicy extends Component {
|
||||||
* @param connection the console connection
|
* @param connection the console connection
|
||||||
* @throws InterruptedException the interrupted exception
|
* @throws InterruptedException the interrupted exception
|
||||||
*/
|
*/
|
||||||
@Handler
|
@Handler(priority = -100)
|
||||||
public void onConsoleConfigured(ConsoleConfigured event,
|
public void onConsoleConfigured(ConsoleConfigured event,
|
||||||
ConsoleConnection connection) throws InterruptedException,
|
ConsoleConnection connection) throws InterruptedException,
|
||||||
IOException {
|
IOException {
|
||||||
if ((Boolean) connection.session().getOrDefault(
|
if ((Boolean) connection.session().getOrDefault(renderedFlagName,
|
||||||
renderedFlagName, false)) {
|
false)) {
|
||||||
return;
|
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(),
|
fire(new AddConletRequest(event.event().event().renderSupport(),
|
||||||
"org.jdrupes.vmoperator.vmconlet.VmConlet",
|
MarkdownDisplayConlet.class.getName(),
|
||||||
Conlet.RenderMode
|
RenderMode.asSet(RenderMode.Preview))
|
||||||
.asSet(Conlet.RenderMode.Preview, Conlet.RenderMode.View)),
|
.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);
|
connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,9 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
ListOptions listOpts = new ListOptions();
|
ListOptions listOpts = new ListOptions();
|
||||||
listOpts.setLabelSelector(
|
listOpts.setLabelSelector(
|
||||||
"app.kubernetes.io/managed-by=" + VM_OP_NAME + ","
|
"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
|
// Get pod, selected by label
|
||||||
var podApi = new DynamicKubernetesApi("", "v1", "pods", client);
|
var podApi = new DynamicKubernetesApi("", "v1", "pods", client);
|
||||||
var pods = podApi
|
var pods = podApi
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,12 @@ public class Constants extends org.jdrupes.vmoperator.common.Constants {
|
||||||
/** The Constant COMP_DISPLAY_SECRET. */
|
/** The Constant COMP_DISPLAY_SECRET. */
|
||||||
public static final String COMP_DISPLAY_SECRET = "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. */
|
/** The Constant STATE_RUNNING. */
|
||||||
public static final String STATE_RUNNING = "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 VmMonitor(channel()).channelManager(chanMgr));
|
||||||
attach(new DisplayPasswordMonitor(channel())
|
attach(new DisplaySecretMonitor(channel())
|
||||||
.channelManager(chanMgr.fixed()));
|
.channelManager(chanMgr.fixed()));
|
||||||
// Currently, we don't use the IP assigned by the load balancer
|
// Currently, we don't use the IP assigned by the load balancer
|
||||||
// to access the VM's console. Might change in the future.
|
// 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")
|
@SuppressWarnings("PMD.SingularField")
|
||||||
private final Configuration fmConfig;
|
private final Configuration fmConfig;
|
||||||
private final ConfigMapReconciler cmReconciler;
|
private final ConfigMapReconciler cmReconciler;
|
||||||
|
private final DisplaySecretReconciler dsReconciler;
|
||||||
private final StatefulSetReconciler stsReconciler;
|
private final StatefulSetReconciler stsReconciler;
|
||||||
private final LoadBalancerReconciler lbReconciler;
|
private final LoadBalancerReconciler lbReconciler;
|
||||||
@SuppressWarnings("PMD.UseConcurrentHashMap")
|
@SuppressWarnings("PMD.UseConcurrentHashMap")
|
||||||
|
|
@ -159,6 +160,7 @@ public class Reconciler extends Component {
|
||||||
fmConfig.setClassForTemplateLoading(Reconciler.class, "");
|
fmConfig.setClassForTemplateLoading(Reconciler.class, "");
|
||||||
|
|
||||||
cmReconciler = new ConfigMapReconciler(fmConfig);
|
cmReconciler = new ConfigMapReconciler(fmConfig);
|
||||||
|
dsReconciler = new DisplaySecretReconciler();
|
||||||
stsReconciler = new StatefulSetReconciler(fmConfig);
|
stsReconciler = new StatefulSetReconciler(fmConfig);
|
||||||
lbReconciler = new LoadBalancerReconciler(fmConfig);
|
lbReconciler = new LoadBalancerReconciler(fmConfig);
|
||||||
}
|
}
|
||||||
|
|
@ -209,6 +211,7 @@ public class Reconciler extends Component {
|
||||||
= prepareModel(channel.client(), patchCr(event.vmDefinition()));
|
= prepareModel(channel.client(), patchCr(event.vmDefinition()));
|
||||||
var configMap = cmReconciler.reconcile(event, model, channel);
|
var configMap = cmReconciler.reconcile(event, model, channel);
|
||||||
model.put("cm", configMap.getRaw());
|
model.put("cm", configMap.getRaw());
|
||||||
|
dsReconciler.reconcile(event, model, channel);
|
||||||
stsReconciler.reconcile(event, model, channel);
|
stsReconciler.reconcile(event, model, channel);
|
||||||
lbReconciler.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.K8s;
|
||||||
import org.jdrupes.vmoperator.common.K8sClient;
|
import org.jdrupes.vmoperator.common.K8sClient;
|
||||||
import org.jdrupes.vmoperator.common.K8sDynamicModel;
|
import org.jdrupes.vmoperator.common.K8sDynamicModel;
|
||||||
import org.jdrupes.vmoperator.common.K8sDynamicModels;
|
|
||||||
import org.jdrupes.vmoperator.common.K8sDynamicStub;
|
import org.jdrupes.vmoperator.common.K8sDynamicStub;
|
||||||
import org.jdrupes.vmoperator.common.K8sObserver.ResponseType;
|
import org.jdrupes.vmoperator.common.K8sObserver.ResponseType;
|
||||||
import org.jdrupes.vmoperator.common.K8sV1ConfigMapStub;
|
import org.jdrupes.vmoperator.common.K8sV1ConfigMapStub;
|
||||||
import org.jdrupes.vmoperator.common.K8sV1PodStub;
|
import org.jdrupes.vmoperator.common.K8sV1PodStub;
|
||||||
import org.jdrupes.vmoperator.common.K8sV1StatefulSetStub;
|
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.APP_NAME;
|
||||||
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_KIND_VM;
|
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_KIND_VM;
|
||||||
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME;
|
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.
|
* Watches for changes of VM definitions.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports" })
|
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports" })
|
||||||
public class VmMonitor
|
public class VmMonitor extends
|
||||||
extends AbstractMonitor<K8sDynamicModel, K8sDynamicModels, VmChannel> {
|
AbstractMonitor<VmDefinitionModel, VmDefinitionModels, VmChannel> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiates a new VM definition watcher.
|
* Instantiates a new VM definition watcher.
|
||||||
|
|
@ -59,7 +61,8 @@ public class VmMonitor
|
||||||
* @param componentChannel the component channel
|
* @param componentChannel the component channel
|
||||||
*/
|
*/
|
||||||
public VmMonitor(Channel componentChannel) {
|
public VmMonitor(Channel componentChannel) {
|
||||||
super(componentChannel, K8sDynamicModel.class, K8sDynamicModels.class);
|
super(componentChannel, VmDefinitionModel.class,
|
||||||
|
VmDefinitionModels.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -102,7 +105,7 @@ public class VmMonitor
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void handleChange(K8sClient client,
|
protected void handleChange(K8sClient client,
|
||||||
Watch.Response<K8sDynamicModel> response) {
|
Watch.Response<VmDefinitionModel> response) {
|
||||||
V1ObjectMeta metadata = response.object.getMetadata();
|
V1ObjectMeta metadata = response.object.getMetadata();
|
||||||
VmChannel channel = channel(metadata.getName()).orElse(null);
|
VmChannel channel = channel(metadata.getName()).orElse(null);
|
||||||
if (channel == null) {
|
if (channel == null) {
|
||||||
|
|
@ -138,9 +141,10 @@ public class VmMonitor
|
||||||
vmDef), channel);
|
vmDef), channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
private K8sDynamicModel getModel(K8sClient client, K8sDynamicModel vmDef) {
|
private VmDefinitionModel getModel(K8sClient client,
|
||||||
|
VmDefinitionModel vmDef) {
|
||||||
try {
|
try {
|
||||||
return K8sDynamicStub.get(client, context(), namespace(),
|
return VmDefinitionStub.get(client, context(), namespace(),
|
||||||
vmDef.metadata().getName()).model().orElse(null);
|
vmDef.metadata().getName()).model().orElse(null);
|
||||||
} catch (ApiException e) {
|
} catch (ApiException e) {
|
||||||
return null;
|
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.K8s;
|
||||||
import org.jdrupes.vmoperator.common.K8sClient;
|
import org.jdrupes.vmoperator.common.K8sClient;
|
||||||
import org.jdrupes.vmoperator.common.K8sDynamicModel;
|
import org.jdrupes.vmoperator.common.K8sDynamicModel;
|
||||||
import org.jdrupes.vmoperator.common.K8sDynamicStub;
|
import org.jdrupes.vmoperator.common.VmDefinitionModel;
|
||||||
|
import org.jdrupes.vmoperator.common.VmDefinitionStub;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.events.BalloonChangeEvent;
|
import org.jdrupes.vmoperator.runner.qemu.events.BalloonChangeEvent;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu;
|
import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.events.DisplayPasswordChanged;
|
import org.jdrupes.vmoperator.runner.qemu.events.DisplayPasswordChanged;
|
||||||
|
|
@ -73,7 +74,7 @@ public class StatusUpdater extends Component {
|
||||||
private long observedGeneration;
|
private long observedGeneration;
|
||||||
private boolean guestShutdownStops;
|
private boolean guestShutdownStops;
|
||||||
private boolean shutdownByGuest;
|
private boolean shutdownByGuest;
|
||||||
private K8sDynamicStub vmStub;
|
private VmDefinitionStub vmStub;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiates a new status updater.
|
* Instantiates a new status updater.
|
||||||
|
|
@ -158,7 +159,7 @@ public class StatusUpdater extends Component {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
vmStub = K8sDynamicStub.get(apiClient,
|
vmStub = VmDefinitionStub.get(apiClient,
|
||||||
new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM),
|
new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM),
|
||||||
namespace, vmName);
|
namespace, vmName);
|
||||||
vmStub.model().ifPresent(model -> {
|
vmStub.model().ifPresent(model -> {
|
||||||
|
|
@ -226,7 +227,7 @@ public class StatusUpdater extends Component {
|
||||||
"PMD.AvoidLiteralsInIfCondition" })
|
"PMD.AvoidLiteralsInIfCondition" })
|
||||||
public void onRunnerStateChanged(RunnerStateChange event)
|
public void onRunnerStateChanged(RunnerStateChange event)
|
||||||
throws ApiException {
|
throws ApiException {
|
||||||
K8sDynamicModel vmDef;
|
VmDefinitionModel vmDef;
|
||||||
if (vmStub == null || (vmDef = vmStub.model().orElse(null)) == null) {
|
if (vmStub == null || (vmDef = vmStub.model().orElse(null)) == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,8 @@ import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import org.jdrupes.json.JsonBeanDecoder;
|
import org.jdrupes.json.JsonBeanDecoder;
|
||||||
import org.jdrupes.json.JsonDecodeException;
|
import org.jdrupes.json.JsonDecodeException;
|
||||||
import org.jdrupes.vmoperator.common.K8sDynamicModel;
|
|
||||||
import org.jdrupes.vmoperator.common.K8sObserver;
|
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.ChannelCache;
|
||||||
import org.jdrupes.vmoperator.manager.events.ModifyVm;
|
import org.jdrupes.vmoperator.manager.events.ModifyVm;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
||||||
|
|
@ -69,7 +69,7 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
private static final Set<RenderMode> MODES = RenderMode.asSet(
|
private static final Set<RenderMode> MODES = RenderMode.asSet(
|
||||||
RenderMode.Preview, RenderMode.View);
|
RenderMode.Preview, RenderMode.View);
|
||||||
private final ChannelCache<String, VmChannel,
|
private final ChannelCache<String, VmChannel,
|
||||||
K8sDynamicModel> channelManager = new ChannelCache<>();
|
VmDefinitionModel> channelManager = new ChannelCache<>();
|
||||||
private final TimeSeries summarySeries = new TimeSeries(Duration.ofDays(1));
|
private final TimeSeries summarySeries = new TimeSeries(Duration.ofDays(1));
|
||||||
private Summary cachedSummary;
|
private Summary cachedSummary;
|
||||||
|
|
||||||
|
|
@ -196,8 +196,8 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var vmDef = new K8sDynamicModel(channel.client().getJSON()
|
var vmDef = new VmDefinitionModel(channel.client().getJSON()
|
||||||
.getGson(), convertQuantities(event.vmDefinition().data()));
|
.getGson(), cleanup(event.vmDefinition().data()));
|
||||||
channelManager.put(vmName, channel, vmDef);
|
channelManager.put(vmName, channel, vmDef);
|
||||||
var def = JsonBeanDecoder.create(vmDef.data().toString())
|
var def = JsonBeanDecoder.create(vmDef.data().toString())
|
||||||
.readObject();
|
.readObject();
|
||||||
|
|
@ -220,7 +220,7 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
|
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
|
||||||
private JsonObject convertQuantities(JsonObject vmDef) {
|
private JsonObject cleanup(JsonObject vmDef) {
|
||||||
// Clone and remove managed fields
|
// Clone and remove managed fields
|
||||||
var json = vmDef.deepCopy();
|
var json = vmDef.deepCopy();
|
||||||
GsonPtr.to(json).to("metadata").get(JsonObject.class)
|
GsonPtr.to(json).to("metadata").get(JsonObject.class)
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@
|
||||||
[role=button] {
|
[role=button] {
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
|
|
||||||
&:not([aria-disabled]):hover {
|
&:not([aria-disabled]):hover, &[aria-disabled='false']:hover {
|
||||||
box-shadow: var(--darkening);
|
box-shadow: var(--darkening);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,6 @@ conletName = VM-Konsole
|
||||||
okayLabel = Anwenden und Schließen
|
okayLabel = Anwenden und Schließen
|
||||||
Select\ VM = VM auswählen
|
Select\ VM = VM auswählen
|
||||||
|
|
||||||
Start\ VM = VM Starten
|
Start\ VM = VM starten
|
||||||
Stop\ VM = VM Anhalten
|
Stop\ VM = VM anhalten
|
||||||
|
Open\ console = Konsole anzeigen
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* VM-Operator
|
* VM-Operator
|
||||||
* Copyright (C) 2023 Michael N. Lipp
|
* Copyright (C) 2023,2024 Michael N. Lipp
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
|
@ -22,6 +22,7 @@ import com.fasterxml.jackson.annotation.JsonGetter;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
import com.google.gson.JsonPrimitive;
|
import com.google.gson.JsonPrimitive;
|
||||||
import freemarker.core.ParseException;
|
import freemarker.core.ParseException;
|
||||||
import freemarker.template.MalformedTemplateNameException;
|
import freemarker.template.MalformedTemplateNameException;
|
||||||
|
|
@ -33,18 +34,23 @@ import java.net.Inet4Address;
|
||||||
import java.net.Inet6Address;
|
import java.net.Inet6Address;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
|
import java.time.Duration;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.ResourceBundle;
|
import java.util.ResourceBundle;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
import org.bouncycastle.util.Objects;
|
||||||
import org.jdrupes.json.JsonBeanDecoder;
|
import org.jdrupes.json.JsonBeanDecoder;
|
||||||
import org.jdrupes.json.JsonDecodeException;
|
import org.jdrupes.json.JsonDecodeException;
|
||||||
import org.jdrupes.vmoperator.common.K8sDynamicModel;
|
import org.jdrupes.vmoperator.common.K8sDynamicModel;
|
||||||
import org.jdrupes.vmoperator.common.K8sObserver;
|
import org.jdrupes.vmoperator.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.ChannelCache;
|
||||||
import org.jdrupes.vmoperator.manager.events.GetDisplayPassword;
|
import org.jdrupes.vmoperator.manager.events.GetDisplayPassword;
|
||||||
import org.jdrupes.vmoperator.manager.events.ModifyVm;
|
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.manager.events.VmDefChanged;
|
||||||
import org.jdrupes.vmoperator.util.GsonPtr;
|
import org.jdrupes.vmoperator.util.GsonPtr;
|
||||||
import org.jgrapes.core.Channel;
|
import org.jgrapes.core.Channel;
|
||||||
|
import org.jgrapes.core.Components;
|
||||||
import org.jgrapes.core.Event;
|
import org.jgrapes.core.Event;
|
||||||
import org.jgrapes.core.Manager;
|
import org.jgrapes.core.Manager;
|
||||||
import org.jgrapes.core.annotation.Handler;
|
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.Conlet.RenderMode;
|
||||||
import org.jgrapes.webconsole.base.ConletBaseModel;
|
import org.jgrapes.webconsole.base.ConletBaseModel;
|
||||||
import org.jgrapes.webconsole.base.ConsoleConnection;
|
import org.jgrapes.webconsole.base.ConsoleConnection;
|
||||||
|
import org.jgrapes.webconsole.base.ConsoleRole;
|
||||||
import org.jgrapes.webconsole.base.ConsoleUser;
|
import org.jgrapes.webconsole.base.ConsoleUser;
|
||||||
import org.jgrapes.webconsole.base.WebConsoleUtils;
|
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.AddConletType;
|
||||||
import org.jgrapes.webconsole.base.events.AddPageResources.ScriptResource;
|
import org.jgrapes.webconsole.base.events.AddPageResources.ScriptResource;
|
||||||
import org.jgrapes.webconsole.base.events.ConletDeleted;
|
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.ConsoleReady;
|
||||||
import org.jgrapes.webconsole.base.events.DeleteConlet;
|
import org.jgrapes.webconsole.base.events.DeleteConlet;
|
||||||
import org.jgrapes.webconsole.base.events.NotifyConletModel;
|
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.RenderConlet;
|
||||||
import org.jgrapes.webconsole.base.events.RenderConletRequestBase;
|
import org.jgrapes.webconsole.base.events.RenderConletRequestBase;
|
||||||
import org.jgrapes.webconsole.base.events.SetLocale;
|
import org.jgrapes.webconsole.base.events.SetLocale;
|
||||||
|
import org.jgrapes.webconsole.base.events.UpdateConletType;
|
||||||
import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet;
|
import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Class VmConlet.
|
* The Class VmConlet.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports",
|
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports",
|
||||||
"PMD.CouplingBetweenObjects" })
|
"PMD.CouplingBetweenObjects", "PMD.GodClass" })
|
||||||
public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
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(
|
private static final Set<RenderMode> MODES = RenderMode.asSet(
|
||||||
RenderMode.Preview, RenderMode.Edit);
|
RenderMode.Preview, RenderMode.Edit);
|
||||||
|
private static final Set<RenderMode> MODES_FOR_GENERATED = RenderMode.asSet(
|
||||||
|
RenderMode.Preview, RenderMode.StickyPreview);
|
||||||
private final ChannelCache<String, VmChannel,
|
private final ChannelCache<String, VmChannel,
|
||||||
K8sDynamicModel> channelManager = new ChannelCache<>();
|
VmDefinitionModel> channelManager = new ChannelCache<>();
|
||||||
private static ObjectMapper objectMapper
|
private static ObjectMapper objectMapper
|
||||||
= new ObjectMapper().registerModule(new JavaTimeModule());
|
= new ObjectMapper().registerModule(new JavaTimeModule());
|
||||||
private Class<?> preferredIpVersion = Inet4Address.class;
|
private Class<?> preferredIpVersion = Inet4Address.class;
|
||||||
|
private final Set<String> syncUsers = new HashSet<>();
|
||||||
|
private final Set<String> syncRoles = new HashSet<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The periodically generated update event.
|
* The periodically generated update event.
|
||||||
|
|
@ -114,13 +135,15 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
*
|
*
|
||||||
* @param event the event
|
* @param event the event
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
@Handler
|
@Handler
|
||||||
public void onConfigurationUpdate(ConfigurationUpdate event) {
|
public void onConfigurationUpdate(ConfigurationUpdate event) {
|
||||||
event.structured(componentPath()).ifPresent(c -> {
|
event.structured(componentPath()).ifPresent(c -> {
|
||||||
@SuppressWarnings("unchecked")
|
try {
|
||||||
var dispRes = (Map<String, Object>) c
|
var dispRes = (Map<String, Object>) c
|
||||||
.getOrDefault("displayResource", Collections.emptyMap());
|
.getOrDefault("displayResource", Collections.emptyMap());
|
||||||
switch ((String) dispRes.getOrDefault("preferredIpVersion", "")) {
|
switch ((String) dispRes.getOrDefault("preferredIpVersion",
|
||||||
|
"")) {
|
||||||
case "ipv6":
|
case "ipv6":
|
||||||
preferredIpVersion = Inet6Address.class;
|
preferredIpVersion = Inet6Address.class;
|
||||||
break;
|
break;
|
||||||
|
|
@ -129,9 +152,30 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
preferredIpVersion = Inet4Address.class;
|
preferredIpVersion = Inet4Address.class;
|
||||||
break;
|
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}.
|
* On {@link ConsoleReady}, fire the {@link AddConletType}.
|
||||||
*
|
*
|
||||||
|
|
@ -155,6 +199,61 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
.addScript(new ScriptResource().setScriptType("module")
|
.addScript(new ScriptResource().setScriptType("module")
|
||||||
.setScriptUri(event.renderSupport().conletResource(
|
.setScriptUri(event.renderSupport().conletResource(
|
||||||
type(), "VmViewer-functions.js"))));
|
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) {
|
private String storagePath(Session session, String conletId) {
|
||||||
|
|
@ -163,6 +262,20 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
+ "/" + VmViewer.class.getName() + "/" + conletId;
|
+ "/" + 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
|
@Override
|
||||||
protected Optional<ViewerModel> createStateRepresentation(Event<?> event,
|
protected Optional<ViewerModel> createStateRepresentation(Event<?> event,
|
||||||
ConsoleConnection connection, String conletId) throws Exception {
|
ConsoleConnection connection, String conletId) throws Exception {
|
||||||
|
|
@ -197,32 +310,57 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
|
@SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops", "unchecked" })
|
||||||
protected Set<RenderMode> doRenderConlet(RenderConletRequestBase<?> event,
|
protected Set<RenderMode> doRenderConlet(RenderConletRequestBase<?> event,
|
||||||
ConsoleConnection channel, String conletId, ViewerModel conletState)
|
ConsoleConnection channel, String conletId, ViewerModel model)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
ResourceBundle resourceBundle = resourceBundle(channel.locale());
|
ResourceBundle resourceBundle = resourceBundle(channel.locale());
|
||||||
Set<RenderMode> renderedAs = new HashSet<>();
|
Set<RenderMode> renderedAs = new HashSet<>();
|
||||||
if (event.renderAs().contains(RenderMode.Preview)) {
|
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
|
Template tpl
|
||||||
= freemarkerConfig().getTemplate("VmViewer-preview.ftl.html");
|
= freemarkerConfig().getTemplate("VmViewer-preview.ftl.html");
|
||||||
channel.respond(new RenderConlet(type(), conletId,
|
channel.respond(new RenderConlet(type(), conletId,
|
||||||
processTemplate(event, tpl,
|
processTemplate(event, tpl,
|
||||||
fmModel(event, channel, conletId, conletState)))
|
fmModel(event, channel, conletId, model)))
|
||||||
.setRenderAs(
|
.setRenderAs(
|
||||||
RenderMode.Preview.addModifiers(event.renderAs()))
|
RenderMode.Preview.addModifiers(event.renderAs()))
|
||||||
.setSupportedModes(MODES));
|
.setSupportedModes(
|
||||||
|
model.isGenerated() ? MODES_FOR_GENERATED : MODES));
|
||||||
renderedAs.add(RenderMode.Preview);
|
renderedAs.add(RenderMode.Preview);
|
||||||
if (!Strings.isNullOrEmpty(conletState.vmName())) {
|
if (!Strings.isNullOrEmpty(model.vmName())) {
|
||||||
updateConfig(channel, conletState);
|
Optional.ofNullable(channel.session().get(RENDERED))
|
||||||
|
.ifPresent(s -> ((Set<String>) s).add(model.vmName()));
|
||||||
|
updateConfig(channel, model);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (event.renderAs().contains(RenderMode.Edit)) {
|
if (event.renderAs().contains(RenderMode.Edit)) {
|
||||||
Template tpl = freemarkerConfig()
|
Template tpl = freemarkerConfig()
|
||||||
.getTemplate("VmViewer-edit.ftl.html");
|
.getTemplate("VmViewer-edit.ftl.html");
|
||||||
var fmModel = fmModel(event, channel, conletId, conletState);
|
var fmModel = fmModel(event, channel, conletId, model);
|
||||||
fmModel.put("vmNames",
|
fmModel.put("vmNames", accessibleVms(channel));
|
||||||
channelManager.keys().stream().sorted().toList());
|
|
||||||
channel.respond(new OpenModalDialog(type(), conletId,
|
channel.respond(new OpenModalDialog(type(), conletId,
|
||||||
processTemplate(event, tpl, fmModel))
|
processTemplate(event, tpl, fmModel))
|
||||||
.addOption("cancelable", true)
|
.addOption("cancelable", true)
|
||||||
|
|
@ -232,6 +370,21 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
return renderedAs;
|
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) {
|
private void updateConfig(ConsoleConnection channel, ViewerModel model) {
|
||||||
channel.respond(new NotifyConletView(type(),
|
channel.respond(new NotifyConletView(type(),
|
||||||
model.getConletId(), "updateConfig", model.vmName()));
|
model.getConletId(), "updateConfig", model.vmName()));
|
||||||
|
|
@ -246,6 +399,9 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
try {
|
try {
|
||||||
var def = JsonBeanDecoder.create(vmDef.data().toString())
|
var def = JsonBeanDecoder.create(vmDef.data().toString())
|
||||||
.readObject();
|
.readObject();
|
||||||
|
def.setField("userPermissions",
|
||||||
|
permissions(vmDef, channel.session()).stream()
|
||||||
|
.map(Permission::toString).toList());
|
||||||
channel.respond(new NotifyConletView(type(),
|
channel.respond(new NotifyConletView(type(),
|
||||||
model.getConletId(), "updateVmDefinition", def));
|
model.getConletId(), "updateVmDefinition", def));
|
||||||
} catch (JsonDecodeException e) {
|
} catch (JsonDecodeException e) {
|
||||||
|
|
@ -279,8 +435,10 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
"PMD.ConfusingArgumentToVarargsMethod" })
|
"PMD.ConfusingArgumentToVarargsMethod" })
|
||||||
public void onVmDefChanged(VmDefChanged event, VmChannel channel)
|
public void onVmDefChanged(VmDefChanged event, VmChannel channel)
|
||||||
throws JsonDecodeException, IOException {
|
throws JsonDecodeException, IOException {
|
||||||
var vmDef = new K8sDynamicModel(channel.client().getJSON()
|
var vmDef = new VmDefinitionModel(channel.client().getJSON()
|
||||||
.getGson(), event.vmDefinition().data());
|
.getGson(), event.vmDefinition().data());
|
||||||
|
GsonPtr.to(vmDef.data()).to("metadata").get(JsonObject.class)
|
||||||
|
.remove("managedFields");
|
||||||
var vmName = vmDef.getMetadata().getName();
|
var vmName = vmDef.getMetadata().getName();
|
||||||
if (event.type() == K8sObserver.ResponseType.DELETED) {
|
if (event.type() == K8sObserver.ResponseType.DELETED) {
|
||||||
channelManager.remove(vmName);
|
channelManager.remove(vmName);
|
||||||
|
|
@ -291,7 +449,8 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
var connection = entry.getKey();
|
var connection = entry.getKey();
|
||||||
for (var conletId : entry.getValue()) {
|
for (var conletId : entry.getValue()) {
|
||||||
var model = stateFromSession(connection.session(), conletId);
|
var model = stateFromSession(connection.session(), conletId);
|
||||||
if (model.isEmpty() || !model.get().vmName().equals(vmName)) {
|
if (model.isEmpty()
|
||||||
|
|| !Objects.areEqual(model.get().vmName(), vmName)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (event.type() == K8sObserver.ResponseType.DELETED) {
|
if (event.type() == K8sObserver.ResponseType.DELETED) {
|
||||||
|
|
@ -311,11 +470,15 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
ConsoleConnection channel, ViewerModel model)
|
ConsoleConnection channel, ViewerModel model)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
event.stop();
|
event.stop();
|
||||||
var vmName = event.params().asString(0);
|
var both = Optional.ofNullable(event.params().asString(0))
|
||||||
var vmChannel = channelManager.channel(vmName).orElse(null);
|
.flatMap(vm -> channelManager.both(vm));
|
||||||
if (vmChannel == null) {
|
if (both.isEmpty()) {
|
||||||
return;
|
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()) {
|
switch (event.method()) {
|
||||||
case "selectedVm":
|
case "selectedVm":
|
||||||
model.setVmName(event.params().asString(0));
|
model.setVmName(event.params().asString(0));
|
||||||
|
|
@ -325,15 +488,22 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
updateConfig(channel, model);
|
updateConfig(channel, model);
|
||||||
break;
|
break;
|
||||||
case "start":
|
case "start":
|
||||||
|
if (perms.contains(Permission.START)) {
|
||||||
fire(new ModifyVm(vmName, "state", "Running", vmChannel));
|
fire(new ModifyVm(vmName, "state", "Running", vmChannel));
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "stop":
|
case "stop":
|
||||||
|
if (perms.contains(Permission.STOP)) {
|
||||||
fire(new ModifyVm(vmName, "state", "Stopped", vmChannel));
|
fire(new ModifyVm(vmName, "state", "Stopped", vmChannel));
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "openConsole":
|
case "openConsole":
|
||||||
channelManager.channel(vmName).ifPresent(
|
if (perms.contains(Permission.ACCESS_CONSOLE)) {
|
||||||
vc -> fire(Event.onCompletion(new GetDisplayPassword(vmName),
|
var pwQuery = Event.onCompletion(new GetDisplayPassword(vmDef),
|
||||||
ds -> openConsole(vmName, channel, model, ds)), vc));
|
e -> e.password().ifPresent(
|
||||||
|
pw -> openConsole(vmName, channel, model, pw)));
|
||||||
|
fire(pwQuery, vmChannel);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:// ignore
|
default:// ignore
|
||||||
break;
|
break;
|
||||||
|
|
@ -341,7 +511,7 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openConsole(String vmName, ConsoleConnection connection,
|
private void openConsole(String vmName, ConsoleConnection connection,
|
||||||
ViewerModel model, GetDisplayPassword pwQuery) {
|
ViewerModel model, String password) {
|
||||||
var vmDef = channelManager.associated(vmName).orElse(null);
|
var vmDef = channelManager.associated(vmName).orElse(null);
|
||||||
if (vmDef == null) {
|
if (vmDef == null) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -362,10 +532,8 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
StringBuffer data = new StringBuffer(100)
|
StringBuffer data = new StringBuffer(100)
|
||||||
.append("[virt-viewer]\ntype=spice\nhost=")
|
.append("[virt-viewer]\ntype=spice\nhost=")
|
||||||
.append(addr.get().getHostAddress()).append("\nport=")
|
.append(addr.get().getHostAddress()).append("\nport=")
|
||||||
.append(Integer.toString(port.get().getAsInt())).append('\n');
|
.append(Integer.toString(port.get().getAsInt()))
|
||||||
pwQuery.password().ifPresent(p -> {
|
.append("\npassword=").append(password).append('\n');
|
||||||
data.append("password=").append(p).append('\n');
|
|
||||||
});
|
|
||||||
proxyUrl.map(JsonPrimitive::getAsString).ifPresent(u -> {
|
proxyUrl.map(JsonPrimitive::getAsString).ifPresent(u -> {
|
||||||
if (!Strings.isNullOrEmpty(u)) {
|
if (!Strings.isNullOrEmpty(u)) {
|
||||||
data.append("proxy=").append(u).append('\n');
|
data.append("proxy=").append(u).append('\n');
|
||||||
|
|
@ -418,9 +586,11 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
/**
|
/**
|
||||||
* The Class VmsModel.
|
* The Class VmsModel.
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("PMD.DataClass")
|
||||||
public static class ViewerModel extends ConletBaseModel {
|
public static class ViewerModel extends ConletBaseModel {
|
||||||
|
|
||||||
private String vmName;
|
private String vmName;
|
||||||
|
private boolean generated;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiates a new vms model.
|
* Instantiates a new vms model.
|
||||||
|
|
@ -450,5 +620,23 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
this.vmName = vmName;
|
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>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td><img role=button
|
<td><img role=button
|
||||||
|
:aria-disabled="!vmDef.running || !vmDef.userPermissions
|
||||||
|
|| !vmDef.userPermissions.includes('accessConsole')"
|
||||||
v-on:click="vmAction(vmDef.name, 'openConsole')"
|
v-on:click="vmAction(vmDef.name, 'openConsole')"
|
||||||
:src="resourceBase + (vmDef.running
|
: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"
|
<td v-if="vmDef.spec"
|
||||||
class="jdrupes-vmoperator-vmviewer-preview-action-list">
|
class="jdrupes-vmoperator-vmviewer-preview-action-list">
|
||||||
<span role="button" v-if="vmDef.spec.vm.state != 'Running'"
|
<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')"
|
tabindex="0" class="fa fa-play" :title="localize('Start VM')"
|
||||||
v-on:click="vmAction(vmDef.name, 'start')"></span>
|
v-on:click="vmAction(vmDef.name, 'start')"></span>
|
||||||
<span role="button" v-else class="fa fa-play"
|
<span role="button" v-else class="fa fa-play"
|
||||||
aria-disabled="true" :title="localize('Start VM')"></span>
|
aria-disabled="true" :title="localize('Start VM')"></span>
|
||||||
<span role="button" v-if="vmDef.spec.vm.state != 'Stopped'"
|
<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')"
|
tabindex="0" class="fa fa-stop" :title="localize('Stop VM')"
|
||||||
v-on:click="vmAction(vmDef.name, 'stop')"></span>
|
v-on:click="vmAction(vmDef.name, 'stop')"></span>
|
||||||
<span role="button" v-else class="fa fa-stop"
|
<span role="button" v-else class="fa fa-stop"
|
||||||
|
|
|
||||||
|
|
@ -19,24 +19,27 @@
|
||||||
/*
|
/*
|
||||||
* Conlet specific styles.
|
* Conlet specific styles.
|
||||||
*/
|
*/
|
||||||
.jdrupes-vmoperator-vmviewer-preview img {
|
.jdrupes-vmoperator-vmviewer-preview {
|
||||||
|
|
||||||
|
[role=button] {
|
||||||
|
padding: 0.25rem;
|
||||||
|
|
||||||
|
&:not([aria-disabled]):hover, &[aria-disabled='false']:hover {
|
||||||
|
box-shadow: var(--darkening);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
height: 3em;
|
height: 3em;
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
|
|
||||||
&:hover {
|
&[aria-disabled=''], &[aria-disabled='true'] {
|
||||||
box-shadow: var(--darkening);
|
opacity: 0.4;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.jdrupes-vmoperator-vmviewer-preview-action-list {
|
.jdrupes-vmoperator-vmviewer-preview-action-list {
|
||||||
white-space: nowrap;
|
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