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
|
|
@ -22,6 +22,7 @@ dependencies {
|
|||
implementation 'org.jgrapes:org.jgrapes.webconsole.vuejs:[1.5.0,2)'
|
||||
implementation 'org.jgrapes:org.jgrapes.webconsole.rbac:[1.3.0,2)'
|
||||
implementation 'org.jgrapes:org.jgrapes.webconlet.oidclogin:[1.3.0,2)'
|
||||
implementation 'org.jgrapes:org.jgrapes.webconlet.markdowndisplay:[1.2.0,2)'
|
||||
|
||||
runtimeOnly 'org.jgrapes:org.jgrapes.webconlet.sysinfo:[1.4.0,2)'
|
||||
runtimeOnly 'org.jgrapes:org.jgrapes.webconlet.logviewer:[0.2.0,2)'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
You can use the "puzzle piece" icon on the top right corner of the
|
||||
page to add display widgets (conlets) to the overview tab.
|
||||
|
||||
Use the "full screen" icon on the top right corner of any
|
||||
conlet (if available) to get a detailed view.
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
Verwenden Sie das "Puzzle"-Icon auf der rechten oberen Ecke
|
||||
der Seite, um Anzeige-Widgets (Conlets) hinzuzufügen.
|
||||
|
||||
Wenn sich in der rechten oberen Ecke eines Conlets ein Vollbild-Icon
|
||||
befindet, können Sie es verwenden, um eine Detailansicht in einem neuen
|
||||
Register anzufordern.
|
||||
|
|
@ -17,3 +17,4 @@
|
|||
#
|
||||
|
||||
consoleTitle = VM-Operator
|
||||
introTitle = Usage
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
#
|
||||
# VM-Operator
|
||||
# Copyright (C) 2024 Michael N. Lipp
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
|
||||
# for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along
|
||||
# with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
introTitle = Benutzung
|
||||
|
|
@ -18,11 +18,17 @@
|
|||
|
||||
package org.jdrupes.vmoperator.manager;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.Collections;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.stream.Collectors;
|
||||
import org.jgrapes.core.Channel;
|
||||
import org.jgrapes.core.Component;
|
||||
import org.jgrapes.core.annotation.Handler;
|
||||
import org.jgrapes.webconsole.base.Conlet;
|
||||
import org.jgrapes.webconlet.markdowndisplay.MarkdownDisplayConlet;
|
||||
import org.jgrapes.webconsole.base.Conlet.RenderMode;
|
||||
import org.jgrapes.webconsole.base.ConsoleConnection;
|
||||
import org.jgrapes.webconsole.base.events.AddConletRequest;
|
||||
import org.jgrapes.webconsole.base.events.ConsoleConfigured;
|
||||
|
|
@ -63,10 +69,13 @@ public class AvoidEmptyPolicy extends Component {
|
|||
* @param event the event
|
||||
* @param connection the connection
|
||||
*/
|
||||
@Handler
|
||||
@Handler(priority = 100)
|
||||
public void onRenderConlet(RenderConlet event,
|
||||
ConsoleConnection connection) {
|
||||
connection.session().put(renderedFlagName, true);
|
||||
if (event.renderAs().contains(RenderMode.Preview)
|
||||
|| event.renderAs().contains(RenderMode.View)) {
|
||||
connection.session().put(renderedFlagName, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -76,18 +85,42 @@ public class AvoidEmptyPolicy extends Component {
|
|||
* @param connection the console connection
|
||||
* @throws InterruptedException the interrupted exception
|
||||
*/
|
||||
@Handler
|
||||
@Handler(priority = -100)
|
||||
public void onConsoleConfigured(ConsoleConfigured event,
|
||||
ConsoleConnection connection) throws InterruptedException,
|
||||
IOException {
|
||||
if ((Boolean) connection.session().getOrDefault(
|
||||
renderedFlagName, false)) {
|
||||
if ((Boolean) connection.session().getOrDefault(renderedFlagName,
|
||||
false)) {
|
||||
return;
|
||||
}
|
||||
var resourceBundle = ResourceBundle.getBundle(
|
||||
getClass().getPackage().getName() + ".l10n", connection.locale(),
|
||||
getClass().getClassLoader(),
|
||||
ResourceBundle.Control.getNoFallbackControl(
|
||||
ResourceBundle.Control.FORMAT_DEFAULT));
|
||||
var locale = resourceBundle.getLocale().toString();
|
||||
String shortDesc;
|
||||
try (BufferedReader shortDescReader
|
||||
= new BufferedReader(new InputStreamReader(
|
||||
AvoidEmptyPolicy.class.getResourceAsStream(
|
||||
"ManagerIntro-Preview" + (locale.isEmpty() ? ""
|
||||
: "_" + locale) + ".md"),
|
||||
"utf-8"))) {
|
||||
shortDesc
|
||||
= shortDescReader.lines().collect(Collectors.joining("\n"));
|
||||
}
|
||||
fire(new AddConletRequest(event.event().event().renderSupport(),
|
||||
"org.jdrupes.vmoperator.vmconlet.VmConlet",
|
||||
Conlet.RenderMode
|
||||
.asSet(Conlet.RenderMode.Preview, Conlet.RenderMode.View)),
|
||||
MarkdownDisplayConlet.class.getName(),
|
||||
RenderMode.asSet(RenderMode.Preview))
|
||||
.addProperty(MarkdownDisplayConlet.CONLET_ID,
|
||||
getClass().getName())
|
||||
.addProperty(MarkdownDisplayConlet.TITLE,
|
||||
resourceBundle.getString("consoleTitle"))
|
||||
.addProperty(MarkdownDisplayConlet.PREVIEW_SOURCE,
|
||||
shortDesc)
|
||||
.addProperty(MarkdownDisplayConlet.DELETABLE, true)
|
||||
.addProperty(MarkdownDisplayConlet.EDITABLE_BY,
|
||||
Collections.EMPTY_SET),
|
||||
connection);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -104,7 +104,9 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
|
|||
ListOptions listOpts = new ListOptions();
|
||||
listOpts.setLabelSelector(
|
||||
"app.kubernetes.io/managed-by=" + VM_OP_NAME + ","
|
||||
+ "app.kubernetes.io/name=" + APP_NAME);
|
||||
+ "app.kubernetes.io/name=" + APP_NAME + ","
|
||||
+ "app.kubernetes.io/instance=" + newCm.getMetadata()
|
||||
.getLabels().get("app.kubernetes.io/instance"));
|
||||
// Get pod, selected by label
|
||||
var podApi = new DynamicKubernetesApi("", "v1", "pods", client);
|
||||
var pods = podApi
|
||||
|
|
|
|||
|
|
@ -27,6 +27,12 @@ public class Constants extends org.jdrupes.vmoperator.common.Constants {
|
|||
/** The Constant COMP_DISPLAY_SECRET. */
|
||||
public static final String COMP_DISPLAY_SECRET = "display-secret";
|
||||
|
||||
/** The Constant DATA_DISPLAY_PASSWORD. */
|
||||
public static final String DATA_DISPLAY_PASSWORD = "display-password";
|
||||
|
||||
/** The Constant DATA_PASSWORD_EXPIRY. */
|
||||
public static final String DATA_PASSWORD_EXPIRY = "password-expiry";
|
||||
|
||||
/** The Constant STATE_RUNNING. */
|
||||
public static final String STATE_RUNNING = "Running";
|
||||
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ public class Controller extends Component {
|
|||
}
|
||||
});
|
||||
attach(new VmMonitor(channel()).channelManager(chanMgr));
|
||||
attach(new DisplayPasswordMonitor(channel())
|
||||
attach(new DisplaySecretMonitor(channel())
|
||||
.channelManager(chanMgr.fixed()));
|
||||
// Currently, we don't use the IP assigned by the load balancer
|
||||
// to access the VM's console. Might change in the future.
|
||||
|
|
|
|||
|
|
@ -1,102 +0,0 @@
|
|||
/*
|
||||
* VM-Operator
|
||||
* Copyright (C) 2024 Michael N. Lipp
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.jdrupes.vmoperator.manager;
|
||||
|
||||
import io.kubernetes.client.openapi.ApiException;
|
||||
import io.kubernetes.client.openapi.models.V1Secret;
|
||||
import io.kubernetes.client.openapi.models.V1SecretList;
|
||||
import io.kubernetes.client.util.Watch.Response;
|
||||
import io.kubernetes.client.util.generic.options.ListOptions;
|
||||
import java.io.IOException;
|
||||
import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
|
||||
import org.jdrupes.vmoperator.common.K8sClient;
|
||||
import org.jdrupes.vmoperator.common.K8sObserver.ResponseType;
|
||||
import org.jdrupes.vmoperator.common.K8sV1SecretStub;
|
||||
import static org.jdrupes.vmoperator.manager.Constants.COMP_DISPLAY_SECRET;
|
||||
import org.jdrupes.vmoperator.manager.events.DisplayPasswordChanged;
|
||||
import org.jdrupes.vmoperator.manager.events.GetDisplayPassword;
|
||||
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
||||
import org.jgrapes.core.Channel;
|
||||
import org.jgrapes.core.annotation.Handler;
|
||||
|
||||
/**
|
||||
* Watches for changes of display secrets.
|
||||
*/
|
||||
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
|
||||
public class DisplayPasswordMonitor
|
||||
extends AbstractMonitor<V1Secret, V1SecretList, VmChannel> {
|
||||
|
||||
/**
|
||||
* Instantiates a new display secrets monitor.
|
||||
*
|
||||
* @param componentChannel the component channel
|
||||
*/
|
||||
public DisplayPasswordMonitor(Channel componentChannel) {
|
||||
super(componentChannel, V1Secret.class, V1SecretList.class);
|
||||
context(K8sV1SecretStub.CONTEXT);
|
||||
ListOptions options = new ListOptions();
|
||||
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
|
||||
+ "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET);
|
||||
options(options);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void prepareMonitoring() throws IOException, ApiException {
|
||||
client(new K8sClient());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleChange(K8sClient client, Response<V1Secret> change) {
|
||||
String vmName = change.object.getMetadata().getLabels()
|
||||
.get("app.kubernetes.io/instance");
|
||||
if (vmName == null) {
|
||||
return;
|
||||
}
|
||||
var channel = channel(vmName).orElse(null);
|
||||
if (channel == null || channel.vmDefinition() == null) {
|
||||
return;
|
||||
}
|
||||
channel.pipeline().fire(new DisplayPasswordChanged(
|
||||
ResponseType.valueOf(change.type), change.object), channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* On get display secrets.
|
||||
*
|
||||
* @param event the event
|
||||
* @param channel the channel
|
||||
* @throws ApiException the api exception
|
||||
*/
|
||||
@Handler
|
||||
@SuppressWarnings("PMD.StringInstantiation")
|
||||
public void onGetDisplaySecrets(GetDisplayPassword event, VmChannel channel)
|
||||
throws ApiException {
|
||||
ListOptions options = new ListOptions();
|
||||
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
|
||||
+ "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET + ","
|
||||
+ "app.kubernetes.io/instance=" + event.vmName());
|
||||
var stubs = K8sV1SecretStub.list(client(), namespace(), options);
|
||||
if (stubs.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
stubs.iterator().next().model().map(m -> m.getData())
|
||||
.map(m -> m.get("display-password"))
|
||||
.ifPresent(p -> event.setResult(new String(p)));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,285 @@
|
|||
/*
|
||||
* VM-Operator
|
||||
* Copyright (C) 2024 Michael N. Lipp
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.jdrupes.vmoperator.manager;
|
||||
|
||||
import io.kubernetes.client.custom.V1Patch;
|
||||
import io.kubernetes.client.openapi.ApiException;
|
||||
import io.kubernetes.client.openapi.models.V1Secret;
|
||||
import io.kubernetes.client.openapi.models.V1SecretList;
|
||||
import io.kubernetes.client.util.Watch.Response;
|
||||
import io.kubernetes.client.util.generic.options.ListOptions;
|
||||
import io.kubernetes.client.util.generic.options.PatchOptions;
|
||||
import java.io.IOException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Scanner;
|
||||
import java.util.logging.Level;
|
||||
import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
|
||||
import static org.jdrupes.vmoperator.common.Constants.VM_OP_NAME;
|
||||
import org.jdrupes.vmoperator.common.K8sClient;
|
||||
import org.jdrupes.vmoperator.common.K8sV1PodStub;
|
||||
import org.jdrupes.vmoperator.common.K8sV1SecretStub;
|
||||
import static org.jdrupes.vmoperator.manager.Constants.COMP_DISPLAY_SECRET;
|
||||
import static org.jdrupes.vmoperator.manager.Constants.DATA_DISPLAY_PASSWORD;
|
||||
import static org.jdrupes.vmoperator.manager.Constants.DATA_PASSWORD_EXPIRY;
|
||||
import org.jdrupes.vmoperator.manager.events.GetDisplayPassword;
|
||||
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
||||
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
||||
import org.jgrapes.core.Channel;
|
||||
import org.jgrapes.core.CompletionLock;
|
||||
import org.jgrapes.core.Event;
|
||||
import org.jgrapes.core.annotation.Handler;
|
||||
import org.jgrapes.util.events.ConfigurationUpdate;
|
||||
import org.jose4j.base64url.Base64;
|
||||
|
||||
/**
|
||||
* Watches for changes of display secrets.
|
||||
*/
|
||||
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.TooManyStaticImports" })
|
||||
public class DisplaySecretMonitor
|
||||
extends AbstractMonitor<V1Secret, V1SecretList, VmChannel> {
|
||||
|
||||
private int passwordValidity = 10;
|
||||
private final List<PendingGet> pendingGets
|
||||
= Collections.synchronizedList(new LinkedList<>());
|
||||
|
||||
/**
|
||||
* Instantiates a new display secrets monitor.
|
||||
*
|
||||
* @param componentChannel the component channel
|
||||
*/
|
||||
public DisplaySecretMonitor(Channel componentChannel) {
|
||||
super(componentChannel, V1Secret.class, V1SecretList.class);
|
||||
context(K8sV1SecretStub.CONTEXT);
|
||||
ListOptions options = new ListOptions();
|
||||
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
|
||||
+ "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET);
|
||||
options(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* On configuration update.
|
||||
*
|
||||
* @param event the event
|
||||
*/
|
||||
@Handler
|
||||
@Override
|
||||
public void onConfigurationUpdate(ConfigurationUpdate event) {
|
||||
super.onConfigurationUpdate(event);
|
||||
event.structured(componentPath()).ifPresent(c -> {
|
||||
try {
|
||||
if (c.containsKey("passwordValidity")) {
|
||||
passwordValidity = Integer
|
||||
.parseInt((String) c.get("passwordValidity"));
|
||||
}
|
||||
} catch (ClassCastException e) {
|
||||
logger.config("Malformed configuration: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void prepareMonitoring() throws IOException, ApiException {
|
||||
client(new K8sClient());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleChange(K8sClient client, Response<V1Secret> change) {
|
||||
String vmName = change.object.getMetadata().getLabels()
|
||||
.get("app.kubernetes.io/instance");
|
||||
if (vmName == null) {
|
||||
return;
|
||||
}
|
||||
var channel = channel(vmName).orElse(null);
|
||||
if (channel == null || channel.vmDefinition() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
patchPod(client, change);
|
||||
} catch (ApiException e) {
|
||||
logger.log(Level.WARNING, e,
|
||||
() -> "Cannot patch pod annotations: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void patchPod(K8sClient client, Response<V1Secret> change)
|
||||
throws ApiException {
|
||||
// Force update for pod
|
||||
ListOptions listOpts = new ListOptions();
|
||||
listOpts.setLabelSelector(
|
||||
"app.kubernetes.io/managed-by=" + VM_OP_NAME + ","
|
||||
+ "app.kubernetes.io/name=" + APP_NAME + ","
|
||||
+ "app.kubernetes.io/instance=" + change.object.getMetadata()
|
||||
.getLabels().get("app.kubernetes.io/instance"));
|
||||
// Get pod, selected by label
|
||||
var pods = K8sV1PodStub.list(client, namespace(), listOpts);
|
||||
|
||||
// If the VM is being created, the pod may not exist yet.
|
||||
if (pods.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
var pod = pods.iterator().next();
|
||||
|
||||
// Patch pod annotation
|
||||
PatchOptions patchOpts = new PatchOptions();
|
||||
patchOpts.setFieldManager("kubernetes-java-kubectl-apply");
|
||||
pod.patch(V1Patch.PATCH_FORMAT_JSON_PATCH,
|
||||
new V1Patch("[{\"op\": \"replace\", \"path\": "
|
||||
+ "\"/metadata/annotations/vmrunner.jdrupes.org~1dpVersion\", "
|
||||
+ "\"value\": \""
|
||||
+ change.object.getMetadata().getResourceVersion()
|
||||
+ "\"}]"),
|
||||
patchOpts);
|
||||
}
|
||||
|
||||
/**
|
||||
* On get display secrets.
|
||||
*
|
||||
* @param event the event
|
||||
* @param channel the channel
|
||||
* @throws ApiException the api exception
|
||||
*/
|
||||
@Handler
|
||||
@SuppressWarnings("PMD.StringInstantiation")
|
||||
public void onGetDisplaySecrets(GetDisplayPassword event, VmChannel channel)
|
||||
throws ApiException {
|
||||
ListOptions options = new ListOptions();
|
||||
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
|
||||
+ "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET + ","
|
||||
+ "app.kubernetes.io/instance="
|
||||
+ event.vmDefinition().metadata().getName());
|
||||
var stubs = K8sV1SecretStub.list(client(),
|
||||
event.vmDefinition().metadata().getNamespace(), options);
|
||||
if (stubs.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
var stub = stubs.iterator().next();
|
||||
|
||||
// Check validity
|
||||
var model = stub.model().get();
|
||||
@SuppressWarnings("PMD.StringInstantiation")
|
||||
var expiry = new String(model.getData().get(DATA_PASSWORD_EXPIRY));
|
||||
if (model.getData().get(DATA_DISPLAY_PASSWORD) != null
|
||||
&& stillValid(expiry)) {
|
||||
event.setResult(
|
||||
new String(model.getData().get(DATA_DISPLAY_PASSWORD)));
|
||||
return;
|
||||
}
|
||||
updatePassword(stub, event);
|
||||
}
|
||||
|
||||
@SuppressWarnings("PMD.StringInstantiation")
|
||||
private void updatePassword(K8sV1SecretStub stub, GetDisplayPassword event)
|
||||
throws ApiException {
|
||||
SecureRandom random = null;
|
||||
try {
|
||||
random = SecureRandom.getInstanceStrong();
|
||||
} catch (NoSuchAlgorithmException e) { // NOPMD
|
||||
// "Every implementation of the Java platform is required
|
||||
// to support at least one strong SecureRandom implementation."
|
||||
}
|
||||
byte[] bytes = new byte[16];
|
||||
random.nextBytes(bytes);
|
||||
var password = Base64.encode(bytes);
|
||||
var model = stub.model().get();
|
||||
model.setStringData(Map.of(DATA_DISPLAY_PASSWORD, password,
|
||||
DATA_PASSWORD_EXPIRY,
|
||||
Long.toString(Instant.now().getEpochSecond() + passwordValidity)));
|
||||
event.setResult(password);
|
||||
|
||||
// Prepare wait for confirmation (by VM status change)
|
||||
var pending = new PendingGet(event,
|
||||
event.vmDefinition().displayPasswordSerial().orElse(0L) + 1,
|
||||
new CompletionLock(event, 1500));
|
||||
pendingGets.add(pending);
|
||||
Event.onCompletion(event, e -> {
|
||||
pendingGets.remove(pending);
|
||||
});
|
||||
|
||||
// Update, will (eventually) trigger confirmation
|
||||
stub.update(model).getObject();
|
||||
}
|
||||
|
||||
private boolean stillValid(String expiry) {
|
||||
if (expiry == null || "never".equals(expiry)) {
|
||||
return true;
|
||||
}
|
||||
@SuppressWarnings({ "PMD.CloseResource", "resource" })
|
||||
var scanner = new Scanner(expiry);
|
||||
if (!scanner.hasNextLong()) {
|
||||
return false;
|
||||
}
|
||||
long expTime = scanner.nextLong();
|
||||
return expTime > Instant.now().getEpochSecond() + passwordValidity;
|
||||
}
|
||||
|
||||
/**
|
||||
* On vm def changed.
|
||||
*
|
||||
* @param event the event
|
||||
* @param channel the channel
|
||||
*/
|
||||
@Handler
|
||||
public void onVmDefChanged(VmDefChanged event, Channel channel) {
|
||||
synchronized (pendingGets) {
|
||||
String vmName = event.vmDefinition().metadata().getName();
|
||||
for (var pending : pendingGets) {
|
||||
if (pending.event.vmDefinition().metadata().getName()
|
||||
.equals(vmName)
|
||||
&& event.vmDefinition().displayPasswordSerial()
|
||||
.map(s -> s >= pending.expectedSerial).orElse(false)) {
|
||||
pending.lock.remove();
|
||||
// pending will be removed from pendingGest by
|
||||
// waiting thread, see updatePassword
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The Class PendingGet.
|
||||
*/
|
||||
@SuppressWarnings("PMD.DataClass")
|
||||
private static class PendingGet {
|
||||
public final GetDisplayPassword event;
|
||||
public final long expectedSerial;
|
||||
public final CompletionLock lock;
|
||||
|
||||
/**
|
||||
* Instantiates a new pending get.
|
||||
*
|
||||
* @param event the event
|
||||
* @param expectedSerial the expected serial
|
||||
*/
|
||||
public PendingGet(GetDisplayPassword event, long expectedSerial,
|
||||
CompletionLock lock) {
|
||||
super();
|
||||
this.event = event;
|
||||
this.expectedSerial = expectedSerial;
|
||||
this.lock = lock;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* VM-Operator
|
||||
* Copyright (C) 2023 Michael N. Lipp
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.jdrupes.vmoperator.manager;
|
||||
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import freemarker.template.TemplateException;
|
||||
import io.kubernetes.client.openapi.ApiException;
|
||||
import io.kubernetes.client.openapi.models.V1ObjectMeta;
|
||||
import io.kubernetes.client.openapi.models.V1Secret;
|
||||
import io.kubernetes.client.util.generic.options.ListOptions;
|
||||
import java.io.IOException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Logger;
|
||||
import org.jdrupes.vmoperator.common.K8sV1SecretStub;
|
||||
import static org.jdrupes.vmoperator.manager.Constants.APP_NAME;
|
||||
import static org.jdrupes.vmoperator.manager.Constants.COMP_DISPLAY_SECRET;
|
||||
import static org.jdrupes.vmoperator.manager.Constants.DATA_DISPLAY_PASSWORD;
|
||||
import static org.jdrupes.vmoperator.manager.Constants.DATA_PASSWORD_EXPIRY;
|
||||
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
||||
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
||||
import org.jdrupes.vmoperator.util.GsonPtr;
|
||||
import org.jose4j.base64url.Base64;
|
||||
|
||||
/**
|
||||
* Delegee for reconciling the display secret
|
||||
*/
|
||||
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
|
||||
/* default */ class DisplaySecretReconciler {
|
||||
|
||||
protected final Logger logger = Logger.getLogger(getClass().getName());
|
||||
|
||||
/**
|
||||
* Reconcile.
|
||||
*
|
||||
* @param event the event
|
||||
* @param model the model
|
||||
* @param channel the channel
|
||||
* @throws IOException Signals that an I/O exception has occurred.
|
||||
* @throws TemplateException the template exception
|
||||
* @throws ApiException the api exception
|
||||
*/
|
||||
public void reconcile(VmDefChanged event,
|
||||
Map<String, Object> model, VmChannel channel)
|
||||
throws IOException, TemplateException, ApiException {
|
||||
// Secret needed at all?
|
||||
var display = GsonPtr.to(event.vmDefinition().data()).to("spec", "vm",
|
||||
"display");
|
||||
if (!display.get(JsonPrimitive.class, "spice", "generateSecret")
|
||||
.map(JsonPrimitive::getAsBoolean).orElse(false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if exists
|
||||
var metadata = event.vmDefinition().getMetadata();
|
||||
ListOptions options = new ListOptions();
|
||||
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
|
||||
+ "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET + ","
|
||||
+ "app.kubernetes.io/instance=" + metadata.getName());
|
||||
var stubs = K8sV1SecretStub.list(channel.client(),
|
||||
metadata.getNamespace(), options);
|
||||
if (!stubs.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create secret
|
||||
var secret = new V1Secret();
|
||||
secret.setMetadata(new V1ObjectMeta().namespace(metadata.getNamespace())
|
||||
.name(metadata.getName() + "-" + COMP_DISPLAY_SECRET)
|
||||
.putLabelsItem("app.kubernetes.io/name", APP_NAME)
|
||||
.putLabelsItem("app.kubernetes.io/component", COMP_DISPLAY_SECRET)
|
||||
.putLabelsItem("app.kubernetes.io/instance", metadata.getName()));
|
||||
secret.setType("Opaque");
|
||||
SecureRandom random = null;
|
||||
try {
|
||||
random = SecureRandom.getInstanceStrong();
|
||||
} catch (NoSuchAlgorithmException e) { // NOPMD
|
||||
// "Every implementation of the Java platform is required
|
||||
// to support at least one strong SecureRandom implementation."
|
||||
}
|
||||
byte[] bytes = new byte[16];
|
||||
random.nextBytes(bytes);
|
||||
var password = Base64.encode(bytes);
|
||||
secret.setStringData(Map.of(DATA_DISPLAY_PASSWORD, password,
|
||||
DATA_PASSWORD_EXPIRY, "now"));
|
||||
K8sV1SecretStub.create(channel.client(), secret);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -135,6 +135,7 @@ public class Reconciler extends Component {
|
|||
@SuppressWarnings("PMD.SingularField")
|
||||
private final Configuration fmConfig;
|
||||
private final ConfigMapReconciler cmReconciler;
|
||||
private final DisplaySecretReconciler dsReconciler;
|
||||
private final StatefulSetReconciler stsReconciler;
|
||||
private final LoadBalancerReconciler lbReconciler;
|
||||
@SuppressWarnings("PMD.UseConcurrentHashMap")
|
||||
|
|
@ -159,6 +160,7 @@ public class Reconciler extends Component {
|
|||
fmConfig.setClassForTemplateLoading(Reconciler.class, "");
|
||||
|
||||
cmReconciler = new ConfigMapReconciler(fmConfig);
|
||||
dsReconciler = new DisplaySecretReconciler();
|
||||
stsReconciler = new StatefulSetReconciler(fmConfig);
|
||||
lbReconciler = new LoadBalancerReconciler(fmConfig);
|
||||
}
|
||||
|
|
@ -209,6 +211,7 @@ public class Reconciler extends Component {
|
|||
= prepareModel(channel.client(), patchCr(event.vmDefinition()));
|
||||
var configMap = cmReconciler.reconcile(event, model, channel);
|
||||
model.put("cm", configMap.getRaw());
|
||||
dsReconciler.reconcile(event, model, channel);
|
||||
stsReconciler.reconcile(event, model, channel);
|
||||
lbReconciler.reconcile(event, model, channel);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,12 +32,14 @@ import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP;
|
|||
import org.jdrupes.vmoperator.common.K8s;
|
||||
import org.jdrupes.vmoperator.common.K8sClient;
|
||||
import org.jdrupes.vmoperator.common.K8sDynamicModel;
|
||||
import org.jdrupes.vmoperator.common.K8sDynamicModels;
|
||||
import org.jdrupes.vmoperator.common.K8sDynamicStub;
|
||||
import org.jdrupes.vmoperator.common.K8sObserver.ResponseType;
|
||||
import org.jdrupes.vmoperator.common.K8sV1ConfigMapStub;
|
||||
import org.jdrupes.vmoperator.common.K8sV1PodStub;
|
||||
import org.jdrupes.vmoperator.common.K8sV1StatefulSetStub;
|
||||
import org.jdrupes.vmoperator.common.VmDefinitionModel;
|
||||
import org.jdrupes.vmoperator.common.VmDefinitionModels;
|
||||
import org.jdrupes.vmoperator.common.VmDefinitionStub;
|
||||
import static org.jdrupes.vmoperator.manager.Constants.APP_NAME;
|
||||
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_KIND_VM;
|
||||
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME;
|
||||
|
|
@ -50,8 +52,8 @@ import org.jgrapes.core.Channel;
|
|||
* Watches for changes of VM definitions.
|
||||
*/
|
||||
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports" })
|
||||
public class VmMonitor
|
||||
extends AbstractMonitor<K8sDynamicModel, K8sDynamicModels, VmChannel> {
|
||||
public class VmMonitor extends
|
||||
AbstractMonitor<VmDefinitionModel, VmDefinitionModels, VmChannel> {
|
||||
|
||||
/**
|
||||
* Instantiates a new VM definition watcher.
|
||||
|
|
@ -59,7 +61,8 @@ public class VmMonitor
|
|||
* @param componentChannel the component channel
|
||||
*/
|
||||
public VmMonitor(Channel componentChannel) {
|
||||
super(componentChannel, K8sDynamicModel.class, K8sDynamicModels.class);
|
||||
super(componentChannel, VmDefinitionModel.class,
|
||||
VmDefinitionModels.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -102,7 +105,7 @@ public class VmMonitor
|
|||
|
||||
@Override
|
||||
protected void handleChange(K8sClient client,
|
||||
Watch.Response<K8sDynamicModel> response) {
|
||||
Watch.Response<VmDefinitionModel> response) {
|
||||
V1ObjectMeta metadata = response.object.getMetadata();
|
||||
VmChannel channel = channel(metadata.getName()).orElse(null);
|
||||
if (channel == null) {
|
||||
|
|
@ -138,9 +141,10 @@ public class VmMonitor
|
|||
vmDef), channel);
|
||||
}
|
||||
|
||||
private K8sDynamicModel getModel(K8sClient client, K8sDynamicModel vmDef) {
|
||||
private VmDefinitionModel getModel(K8sClient client,
|
||||
VmDefinitionModel vmDef) {
|
||||
try {
|
||||
return K8sDynamicStub.get(client, context(), namespace(),
|
||||
return VmDefinitionStub.get(client, context(), namespace(),
|
||||
vmDef.metadata().getName()).model().orElse(null);
|
||||
} catch (ApiException e) {
|
||||
return null;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue