Add viewer conlet (#25)
Some checks failed
Java CI with Gradle / build (push) Has been cancelled

This commit is contained in:
Michael N. Lipp 2024-05-27 12:57:01 +02:00 committed by GitHub
parent b6f0299932
commit a6525a2289
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
77 changed files with 2642 additions and 250 deletions

View file

@ -32,6 +32,7 @@ dependencies {
runtimeOnly 'org.apache.logging.log4j:log4j-to-jul:2.20.0'
runtimeOnly project(':org.jdrupes.vmoperator.vmconlet')
runtimeOnly project(':org.jdrupes.vmoperator.vmviewer')
}
application {

View file

@ -202,7 +202,7 @@ data:
ticket: "${ cr.spec.vm.display.spice.ticket.asString }"
</#if>
<#if cr.spec.vm.display.spice.streamingVideo??>
ticket: "${ cr.spec.vm.display.spice.streamingVideo.asString }"
streaming-video: "${ cr.spec.vm.display.spice.streamingVideo.asString }"
</#if>
usbRedirects: ${ cr.spec.vm.display.spice.usbRedirects.asInt?c }
</#if>

View file

@ -111,7 +111,7 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
.list(newCm.getMetadata().getNamespace(), listOpts).getObject();
// If the VM is being created, the pod may not exist yet.
if (pods == null || pods.getItems().size() == 0) {
if (pods == null || pods.getItems().isEmpty()) {
return;
}
var pod = pods.getItems().get(0);

View file

@ -21,6 +21,7 @@ package org.jdrupes.vmoperator.manager;
/**
* Some constants.
*/
@SuppressWarnings("PMD.DataClass")
public class Constants extends org.jdrupes.vmoperator.common.Constants {
/** The Constant COMP_DISPLAY_SECRET. */

View file

@ -85,6 +85,7 @@ public class Controller extends Component {
/**
* Creates a new instance.
*/
@SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
public Controller(Channel componentChannel) {
super(componentChannel);
// Prepare component tree
@ -100,8 +101,11 @@ public class Controller extends Component {
}
});
attach(new VmMonitor(channel()).channelManager(chanMgr));
attach(new DisplaySecretsMonitor(channel())
attach(new DisplayPasswordMonitor(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.
// attach(new ServiceMonitor(channel()).channelManager(chanMgr));
attach(new Reconciler(channel()));
}

View file

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

View file

@ -224,6 +224,7 @@ public class Reconciler extends Component {
return new DynamicKubernetesObject(json);
}
@SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
private void adjustCdRomPaths(JsonObject json) {
var disks
= GsonPtr.to(json).to("spec", "vm", "disks").get(JsonArray.class);

View file

@ -19,38 +19,36 @@
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.openapi.models.V1Service;
import io.kubernetes.client.openapi.models.V1ServiceList;
import io.kubernetes.client.util.Watch.Response;
import io.kubernetes.client.util.generic.options.ListOptions;
import java.io.IOException;
import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
import org.jdrupes.vmoperator.common.K8sClient;
import org.jdrupes.vmoperator.common.K8sObserver.ResponseType;
import org.jdrupes.vmoperator.common.K8sV1SecretStub;
import static org.jdrupes.vmoperator.manager.Constants.COMP_DISPLAY_SECRET;
import org.jdrupes.vmoperator.manager.events.DisplaySecretChanged;
import org.jdrupes.vmoperator.common.K8sV1ServiceStub;
import org.jdrupes.vmoperator.manager.events.ServiceChanged;
import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jgrapes.core.Channel;
/**
* Watches for changes of display secrets.
* Watches for changes of services.
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class DisplaySecretsMonitor
extends AbstractMonitor<V1Secret, V1SecretList, VmChannel> {
public class ServiceMonitor
extends AbstractMonitor<V1Service, V1ServiceList, VmChannel> {
/**
* Instantiates a new display secrets monitor.
*
* @param componentChannel the component channel
*/
public DisplaySecretsMonitor(Channel componentChannel) {
super(componentChannel, V1Secret.class, V1SecretList.class);
context(K8sV1SecretStub.CONTEXT);
public ServiceMonitor(Channel componentChannel) {
super(componentChannel, V1Service.class, V1ServiceList.class);
context(K8sV1ServiceStub.CONTEXT);
ListOptions options = new ListOptions();
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
+ "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET);
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME);
options(options);
}
@ -60,7 +58,7 @@ public class DisplaySecretsMonitor
}
@Override
protected void handleChange(K8sClient client, Response<V1Secret> change) {
protected void handleChange(K8sClient client, Response<V1Service> change) {
String vmName = change.object.getMetadata().getLabels()
.get("app.kubernetes.io/instance");
if (vmName == null) {
@ -70,8 +68,7 @@ public class DisplaySecretsMonitor
if (channel == null || channel.vmDefinition() == null) {
return;
}
channel.pipeline().fire(new DisplaySecretChanged(
channel.pipeline().fire(new ServiceChanged(
ResponseType.valueOf(change.type), change.object), channel);
}
}

View file

@ -82,14 +82,15 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
// or not running.
var stsStub = K8sV1StatefulSetStub.get(channel.client(),
metadata.getNamespace(), metadata.getName());
stsStub.model().ifPresent(sts -> {
var current = sts.getSpec().getReplicas();
var stsModel = stsStub.model().orElse(null);
if (stsModel != null) {
var current = stsModel.getSpec().getReplicas();
var desired = GsonPtr.to(stsDef.getRaw())
.to("spec").getAsInt("replicas").orElse(1);
if (current == 1 && desired == 1) {
return;
}
});
}
// Do apply changes
PatchOptions opts = new PatchOptions();

View file

@ -150,6 +150,7 @@ public class VmMonitor
private void addDynamicData(K8sClient client, K8sDynamicModel vmState) {
var rootNode = GsonPtr.to(vmState.data()).get(JsonObject.class);
rootNode.addProperty("nodeName", "");
rootNode.addProperty("nodeAddress", "");
// VM definition status changes before the pod terminates.
// This results in pod information being shown for a stopped
@ -172,8 +173,17 @@ public class VmMonitor
var podList
= K8sV1PodStub.list(client, namespace(), podSearch);
for (var podStub : podList) {
rootNode.addProperty("nodeName",
podStub.model().get().getSpec().getNodeName());
var nodeName = podStub.model().get().getSpec().getNodeName();
rootNode.addProperty("nodeName", nodeName);
logger.fine(() -> "Added node name " + nodeName
+ " to VM info for " + vmState.getMetadata().getName());
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
var addrs = new JsonArray();
podStub.model().get().getStatus().getPodIPs().stream()
.map(ip -> ip.getIp()).forEach(addrs::add);
rootNode.add("nodeAddresses", addrs);
logger.fine(() -> "Added node addresses " + addrs
+ " to VM info for " + vmState.getMetadata().getName());
}
} catch (ApiException e) {
logger.log(Level.WARNING, e,