Merge branch 'main' into testing

This commit is contained in:
Michael Lipp 2025-03-29 15:10:52 +01:00
commit e314e8f63e
18 changed files with 152 additions and 262 deletions

View file

@ -300,8 +300,8 @@ public class VmDefinition extends K8sDynamicModel {
* *
* @return the data * @return the data
*/ */
public Optional<VmExtraData> extra() { public VmExtraData extra() {
return Optional.ofNullable(extraData); return extraData;
} }
/** /**

View file

@ -112,7 +112,7 @@ public class VmExtraData {
* @param deleteConnectionFile the delete connection file * @param deleteConnectionFile the delete connection file
* @return the string * @return the string
*/ */
public String connectionFile(String password, public Optional<String> connectionFile(String password,
Class<?> preferredIpVersion, boolean deleteConnectionFile) { Class<?> preferredIpVersion, boolean deleteConnectionFile) {
var addr = displayIp(preferredIpVersion); var addr = displayIp(preferredIpVersion);
if (addr.isEmpty()) { if (addr.isEmpty()) {
@ -144,7 +144,7 @@ public class VmExtraData {
if (deleteConnectionFile) { if (deleteConnectionFile) {
data.append("delete-this-file=1\n"); data.append("delete-this-file=1\n");
} }
return data.toString(); return Optional.of(data.toString());
} }
private Optional<InetAddress> displayIp(Class<?> preferredIpVersion) { private Optional<InetAddress> displayIp(Class<?> preferredIpVersion) {

View file

@ -177,7 +177,7 @@ public class VmPool {
} }
// Additional check in case lastUsed has not been updated // Additional check in case lastUsed has not been updated
// by PoolMonitor#onVmDefChanged() yet ("race condition") // by PoolMonitor#onVmResourceChanged() yet ("race condition")
if (vmDef.condition("ConsoleConnected") if (vmDef.condition("ConsoleConnected")
.map(cc -> cc.getLastTransitionTime().toInstant()) .map(cc -> cc.getLastTransitionTime().toInstant())
.map(this::retainUntil) .map(this::retainUntil)

View file

@ -25,31 +25,35 @@ import org.jgrapes.core.Components;
import org.jgrapes.core.Event; import org.jgrapes.core.Event;
/** /**
* Indicates a change in a VM definition. Note that the definition * Indicates a change in a VM "resource". Note that the resource
* consists of the metadata (mostly immutable), the "spec" and the * combines the VM CR's metadata (mostly immutable), the VM CR's
* "status" parts. Consumers that are only interested in "spec" * "spec" part, the VM CR's "status" subresource and state information
* changes should check {@link #specChanged()} before processing * from the pod. Consumers that are only interested in "spec" changes
* the event any further. * should check {@link #specChanged()} before processing the event any
* further.
*/ */
@SuppressWarnings("PMD.DataClass") @SuppressWarnings("PMD.DataClass")
public class VmDefChanged extends Event<Void> { public class VmResourceChanged extends Event<Void> {
private final K8sObserver.ResponseType type; private final K8sObserver.ResponseType type;
private final boolean specChanged;
private final VmDefinition vmDefinition; private final VmDefinition vmDefinition;
private final boolean specChanged;
private final boolean podChanged;
/** /**
* Instantiates a new VM changed event. * Instantiates a new VM changed event.
* *
* @param type the type * @param type the type
* @param specChanged the spec part changed
* @param vmDefinition the VM definition * @param vmDefinition the VM definition
* @param specChanged the spec part changed
*/ */
public VmDefChanged(K8sObserver.ResponseType type, boolean specChanged, public VmResourceChanged(K8sObserver.ResponseType type,
VmDefinition vmDefinition) { VmDefinition vmDefinition, boolean specChanged,
boolean podChanged) {
this.type = type; this.type = type;
this.specChanged = specChanged;
this.vmDefinition = vmDefinition; this.vmDefinition = vmDefinition;
this.specChanged = specChanged;
this.podChanged = podChanged;
} }
/** /**
@ -61,6 +65,15 @@ public class VmDefChanged extends Event<Void> {
return type; return type;
} }
/**
* Return the VM definition.
*
* @return the VM definition
*/
public VmDefinition vmDefinition() {
return vmDefinition;
}
/** /**
* Indicates if the "spec" part changed. * Indicates if the "spec" part changed.
*/ */
@ -69,12 +82,10 @@ public class VmDefChanged extends Event<Void> {
} }
/** /**
* Return the VM definition. * Indicates if the pod status changed.
*
* @return the VM definition
*/ */
public VmDefinition vmDefinition() { public boolean podChanged() {
return vmDefinition; return podChanged;
} }
@Override @Override

View file

@ -53,7 +53,7 @@ data:
# i.e. if you start the VM without a value for this property, and # i.e. if you start the VM without a value for this property, and
# decide to trigger a reset later, you have to first set the value # decide to trigger a reset later, you have to first set the value
# and then inrement it. # and then inrement it.
resetCounter: ${ cr.extra().get().resetCount()?c } resetCounter: ${ cr.extra().resetCount()?c }
# Forward the cloud-init data if provided # Forward the cloud-init data if provided
<#if spec.cloudInit??> <#if spec.cloudInit??>

View file

@ -1,6 +1,6 @@
/* /*
* VM-Operator * VM-Operator
* Copyright (C) 2023 Michael N. Lipp * Copyright (C) 2023, 2025 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
@ -46,8 +46,8 @@ import org.jdrupes.vmoperator.manager.events.ModifyVm;
import org.jdrupes.vmoperator.manager.events.PodChanged; import org.jdrupes.vmoperator.manager.events.PodChanged;
import org.jdrupes.vmoperator.manager.events.UpdateAssignment; import org.jdrupes.vmoperator.manager.events.UpdateAssignment;
import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jdrupes.vmoperator.manager.events.VmPoolChanged; import org.jdrupes.vmoperator.manager.events.VmPoolChanged;
import org.jdrupes.vmoperator.manager.events.VmResourceChanged;
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;
@ -61,7 +61,7 @@ import org.jgrapes.util.events.ConfigurationUpdate;
* *
* The implementation splits the controller in two components. The * The implementation splits the controller in two components. The
* {@link VmMonitor} and the {@link Reconciler}. The former watches * {@link VmMonitor} and the {@link Reconciler}. The former watches
* the VM definitions (CRs) and generates {@link VmDefChanged} events * the VM definitions (CRs) and generates {@link VmResourceChanged} events
* when they change. The latter handles the changes and reconciles the * when they change. The latter handles the changes and reconciles the
* resources in the cluster. * resources in the cluster.
* *

View file

@ -45,7 +45,7 @@ import org.jdrupes.vmoperator.common.VmDefinition;
import org.jdrupes.vmoperator.common.VmDefinitionStub; import org.jdrupes.vmoperator.common.VmDefinitionStub;
import org.jdrupes.vmoperator.manager.events.GetDisplaySecret; import org.jdrupes.vmoperator.manager.events.GetDisplaySecret;
import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged; import org.jdrupes.vmoperator.manager.events.VmResourceChanged;
import org.jdrupes.vmoperator.util.DataPath; import org.jdrupes.vmoperator.util.DataPath;
import org.jgrapes.core.Channel; import org.jgrapes.core.Channel;
import org.jgrapes.core.CompletionLock; import org.jgrapes.core.CompletionLock;
@ -123,13 +123,19 @@ public class DisplaySecretReconciler extends Component {
* @param vmDef the VM definition * @param vmDef the VM definition
* @param model the model * @param model the model
* @param channel the channel * @param channel the channel
* @param specChanged the spec changed
* @throws IOException Signals that an I/O exception has occurred. * @throws IOException Signals that an I/O exception has occurred.
* @throws TemplateException the template exception * @throws TemplateException the template exception
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
public void reconcile(VmDefinition vmDef, Map<String, Object> model, public void reconcile(VmDefinition vmDef, Map<String, Object> model,
VmChannel channel) VmChannel channel, boolean specChanged)
throws IOException, TemplateException, ApiException { throws IOException, TemplateException, ApiException {
// Nothing to do unless spec changed
if (!specChanged) {
return;
}
// Secret needed at all? // Secret needed at all?
var display = vmDef.fromVm("display").get(); var display = vmDef.fromVm("display").get();
if (!DataPath.<Boolean> get(display, "spice", "generateSecret") if (!DataPath.<Boolean> get(display, "spice", "generateSecret")
@ -292,7 +298,7 @@ public class DisplaySecretReconciler extends Component {
*/ */
@Handler @Handler
@SuppressWarnings("PMD.AvoidSynchronizedStatement") @SuppressWarnings("PMD.AvoidSynchronizedStatement")
public void onVmDefChanged(VmDefChanged event, Channel channel) { public void onVmResourceChanged(VmResourceChanged event, Channel channel) {
synchronized (pendingPrepares) { synchronized (pendingPrepares) {
String vmName = event.vmDefinition().name(); String vmName = event.vmDefinition().name();
for (var pending : pendingPrepares) { for (var pending : pendingPrepares) {

View file

@ -71,13 +71,19 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
* @param vmDef the VM definition * @param vmDef the VM definition
* @param model the model * @param model the model
* @param channel the channel * @param channel the channel
* @param specChanged the spec changed
* @throws IOException Signals that an I/O exception has occurred. * @throws IOException Signals that an I/O exception has occurred.
* @throws TemplateException the template exception * @throws TemplateException the template exception
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
public void reconcile(VmDefinition vmDef, public void reconcile(VmDefinition vmDef, Map<String, Object> model,
Map<String, Object> model, VmChannel channel) VmChannel channel, boolean specChanged)
throws IOException, TemplateException, ApiException { throws IOException, TemplateException, ApiException {
// Nothing to do unless spec changed
if (!specChanged) {
return;
}
// Check if to be generated // Check if to be generated
@SuppressWarnings({ "PMD.AvoidDuplicateLiterals", "unchecked" }) @SuppressWarnings({ "PMD.AvoidDuplicateLiterals", "unchecked" })
var lbsDef = Optional.of(model) var lbsDef = Optional.of(model)

View file

@ -38,7 +38,7 @@ import org.jdrupes.vmoperator.common.K8sV1PodStub;
import org.jdrupes.vmoperator.manager.events.ChannelDictionary; import org.jdrupes.vmoperator.manager.events.ChannelDictionary;
import org.jdrupes.vmoperator.manager.events.PodChanged; import org.jdrupes.vmoperator.manager.events.PodChanged;
import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged; import org.jdrupes.vmoperator.manager.events.VmResourceChanged;
import org.jgrapes.core.Channel; import org.jgrapes.core.Channel;
import org.jgrapes.core.annotation.Handler; import org.jgrapes.core.annotation.Handler;
@ -118,7 +118,8 @@ public class PodMonitor extends AbstractMonitor<V1Pod, V1PodList, VmChannel> {
* @param channel the channel * @param channel the channel
*/ */
@Handler @Handler
public void onVmDefChanged(VmDefChanged event, VmChannel channel) { public void onVmResourceChanged(VmResourceChanged event,
VmChannel channel) {
Optional.ofNullable(pendingChanges.remove(event.vmDefinition().name())) Optional.ofNullable(pendingChanges.remove(event.vmDefinition().name()))
.map(PendingChange::change).ifPresent(change -> { .map(PendingChange::change).ifPresent(change -> {
logger.finer(() -> "Firing pending pod change for " logger.finer(() -> "Firing pending pod change for "

View file

@ -64,18 +64,14 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
* @param vmDef the vm def * @param vmDef the vm def
* @param model the model * @param model the model
* @param channel the channel * @param channel the channel
* @param specChanged the spec changed
* @throws IOException Signals that an I/O exception has occurred. * @throws IOException Signals that an I/O exception has occurred.
* @throws TemplateException the template exception * @throws TemplateException the template exception
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
public void reconcile(VmDefinition vmDef, Map<String, Object> model, public void reconcile(VmDefinition vmDef, Map<String, Object> model,
VmChannel channel) VmChannel channel, boolean specChanged)
throws IOException, TemplateException, ApiException { throws IOException, TemplateException, ApiException {
// Don't do anything if stateful set is still in use (pre v3.4)
if ((Boolean) model.get("usingSts")) {
return;
}
// Get pod stub. // Get pod stub.
var podStub = K8sV1PodStub.get(channel.client(), vmDef.namespace(), var podStub = K8sV1PodStub.get(channel.client(), vmDef.namespace(),
vmDef.name()); vmDef.name());

View file

@ -40,8 +40,8 @@ import org.jdrupes.vmoperator.common.VmDefinition.Assignment;
import org.jdrupes.vmoperator.common.VmDefinitionStub; import org.jdrupes.vmoperator.common.VmDefinitionStub;
import org.jdrupes.vmoperator.common.VmPool; import org.jdrupes.vmoperator.common.VmPool;
import org.jdrupes.vmoperator.manager.events.GetPools; import org.jdrupes.vmoperator.manager.events.GetPools;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jdrupes.vmoperator.manager.events.VmPoolChanged; import org.jdrupes.vmoperator.manager.events.VmPoolChanged;
import org.jdrupes.vmoperator.manager.events.VmResourceChanged;
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.EventPipeline; import org.jgrapes.core.EventPipeline;
@ -142,7 +142,8 @@ public class PoolMonitor extends
* @throws ApiException * @throws ApiException
*/ */
@Handler @Handler
public void onVmDefChanged(VmDefChanged event) throws ApiException { public void onVmResourceChanged(VmResourceChanged event)
throws ApiException {
final var vmDef = event.vmDefinition(); final var vmDef = event.vmDefinition();
final String vmName = vmDef.name(); final String vmName = vmDef.name();
switch (event.type()) { switch (event.type()) {

View file

@ -67,30 +67,35 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
/** /**
* Reconcile the PVCs. * Reconcile the PVCs.
* *
* @param vmDef the vm def * @param vmDef the VM definition
* @param model the model * @param model the model
* @param channel the channel * @param channel the channel
* @param specChanged the spec changed
* @throws IOException Signals that an I/O exception has occurred. * @throws IOException Signals that an I/O exception has occurred.
* @throws TemplateException the template exception * @throws TemplateException the template exception
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
@SuppressWarnings("PMD.AvoidDuplicateLiterals") @SuppressWarnings({ "PMD.AvoidDuplicateLiterals", "unchecked" })
public void reconcile(VmDefinition vmDef, Map<String, Object> model, public void reconcile(VmDefinition vmDef, Map<String, Object> model,
VmChannel channel) VmChannel channel, boolean specChanged)
throws IOException, TemplateException, ApiException { throws IOException, TemplateException, ApiException {
// Existing disks Set<String> knownPvcs;
if (!specChanged && channel.associated(this, Set.class).isPresent()) {
knownPvcs = (Set<String>) channel.associated(this, Set.class).get();
} else {
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=" + vmDef.name()); + "app.kubernetes.io/instance=" + vmDef.name());
var knownDisks = K8sV1PvcStub.list(channel.client(), knownPvcs = K8sV1PvcStub.list(channel.client(),
vmDef.namespace(), listOpts); vmDef.namespace(), listOpts).stream().map(K8sV1PvcStub::name)
var knownPvcs = knownDisks.stream().map(K8sV1PvcStub::name)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
channel.setAssociated(this, knownPvcs);
}
// Reconcile runner data pvc // Reconcile runner data pvc
reconcileRunnerDataPvc(vmDef, model, channel, knownPvcs); reconcileRunnerDataPvc(vmDef, model, channel, knownPvcs, specChanged);
// Reconcile pvcs for defined disks // Reconcile pvcs for defined disks
var diskDefs = vmDef.<List<Map<String, Object>>> fromVm("disks") var diskDefs = vmDef.<List<Map<String, Object>>> fromVm("disks")
@ -114,15 +119,13 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
} }
// Update PVC // Update PVC
model.put("disk", diskDef); reconcileRunnerDiskPvc(vmDef, model, channel, specChanged, diskDef);
reconcileRunnerDiskPvc(vmDef, model, channel);
} }
model.remove("disk");
} }
private void reconcileRunnerDataPvc(VmDefinition vmDef, private void reconcileRunnerDataPvc(VmDefinition vmDef,
Map<String, Object> model, VmChannel channel, Map<String, Object> model, VmChannel channel,
Set<String> knownPvcs) Set<String> knownPvcs, boolean specChanged)
throws TemplateNotFoundException, MalformedTemplateNameException, throws TemplateNotFoundException, MalformedTemplateNameException,
ParseException, IOException, TemplateException, ApiException { ParseException, IOException, TemplateException, ApiException {
@ -135,7 +138,12 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
} }
// Generate PVC // Generate PVC
model.put("runnerDataPvcName", vmDef.name() + "-runner-data"); var runnerDataPvcName = vmDef.name() + "-runner-data";
model.put("runnerDataPvcName", runnerDataPvcName);
if (!specChanged) {
// Augmenting the model is all we have to do
return;
}
var fmTemplate = fmConfig.getTemplate("runnerDataPvc.ftl.yaml"); var fmTemplate = fmConfig.getTemplate("runnerDataPvc.ftl.yaml");
StringWriter out = new StringWriter(); StringWriter out = new StringWriter();
fmTemplate.process(model, out); fmTemplate.process(model, out);
@ -159,17 +167,24 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
} }
private void reconcileRunnerDiskPvc(VmDefinition vmDef, private void reconcileRunnerDiskPvc(VmDefinition vmDef,
Map<String, Object> model, VmChannel channel) Map<String, Object> model, VmChannel channel, boolean specChanged,
Map<String, Object> diskDef)
throws TemplateNotFoundException, MalformedTemplateNameException, throws TemplateNotFoundException, MalformedTemplateNameException,
ParseException, IOException, TemplateException, ApiException { ParseException, IOException, TemplateException, ApiException {
// Generate PVC // Generate PVC
@SuppressWarnings("unchecked")
var diskDef = (Map<String, Object>) model.get("disk");
var pvcName = vmDef.name() + "-" + diskDef.get("generatedDiskName"); var pvcName = vmDef.name() + "-" + diskDef.get("generatedDiskName");
diskDef.put("generatedPvcName", pvcName); diskDef.put("generatedPvcName", pvcName);
if (!specChanged) {
// Augmenting the model is all we have to do
return;
}
// Generate PVC
model.put("disk", diskDef);
var fmTemplate = fmConfig.getTemplate("runnerDiskPvc.ftl.yaml"); var fmTemplate = fmConfig.getTemplate("runnerDiskPvc.ftl.yaml");
StringWriter out = new StringWriter(); StringWriter out = new StringWriter();
fmTemplate.process(model, out); fmTemplate.process(model, out);
model.remove("disk");
// Avoid Yaml.load due to // Avoid Yaml.load due to
// https://github.com/kubernetes-client/java/issues/2741 // https://github.com/kubernetes-client/java/issues/2741
var pvcDef = Dynamics.newFromYaml( var pvcDef = Dynamics.newFromYaml(

View file

@ -44,15 +44,13 @@ import java.util.Optional;
import java.util.logging.Level; import java.util.logging.Level;
import org.jdrupes.vmoperator.common.Convertions; import org.jdrupes.vmoperator.common.Convertions;
import org.jdrupes.vmoperator.common.K8sObserver; import org.jdrupes.vmoperator.common.K8sObserver;
import org.jdrupes.vmoperator.common.K8sObserver.ResponseType;
import org.jdrupes.vmoperator.common.VmDefinition; import org.jdrupes.vmoperator.common.VmDefinition;
import org.jdrupes.vmoperator.common.VmDefinition.Assignment; import org.jdrupes.vmoperator.common.VmDefinition.Assignment;
import org.jdrupes.vmoperator.common.VmPool; import org.jdrupes.vmoperator.common.VmPool;
import org.jdrupes.vmoperator.manager.events.GetPools; import org.jdrupes.vmoperator.manager.events.GetPools;
import org.jdrupes.vmoperator.manager.events.PodChanged;
import org.jdrupes.vmoperator.manager.events.ResetVm; import org.jdrupes.vmoperator.manager.events.ResetVm;
import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged; import org.jdrupes.vmoperator.manager.events.VmResourceChanged;
import org.jdrupes.vmoperator.util.ExtendedObjectWrapper; import org.jdrupes.vmoperator.util.ExtendedObjectWrapper;
import org.jgrapes.core.Channel; import org.jgrapes.core.Channel;
import org.jgrapes.core.Component; import org.jgrapes.core.Component;
@ -151,7 +149,6 @@ public class Reconciler extends Component {
private final Configuration fmConfig; private final Configuration fmConfig;
private final ConfigMapReconciler cmReconciler; private final ConfigMapReconciler cmReconciler;
private final DisplaySecretReconciler dsReconciler; private final DisplaySecretReconciler dsReconciler;
private final StatefulSetReconciler stsReconciler;
private final PvcReconciler pvcReconciler; private final PvcReconciler pvcReconciler;
private final PodReconciler podReconciler; private final PodReconciler podReconciler;
private final LoadBalancerReconciler lbReconciler; private final LoadBalancerReconciler lbReconciler;
@ -179,7 +176,6 @@ public class Reconciler extends Component {
cmReconciler = new ConfigMapReconciler(fmConfig); cmReconciler = new ConfigMapReconciler(fmConfig);
dsReconciler = attach(new DisplaySecretReconciler(componentChannel)); dsReconciler = attach(new DisplaySecretReconciler(componentChannel));
stsReconciler = new StatefulSetReconciler(fmConfig);
pvcReconciler = new PvcReconciler(fmConfig); pvcReconciler = new PvcReconciler(fmConfig);
podReconciler = new PodReconciler(fmConfig); podReconciler = new PodReconciler(fmConfig);
lbReconciler = new LoadBalancerReconciler(fmConfig); lbReconciler = new LoadBalancerReconciler(fmConfig);
@ -208,64 +204,27 @@ public class Reconciler extends Component {
*/ */
@Handler @Handler
@SuppressWarnings("PMD.ConfusingTernary") @SuppressWarnings("PMD.ConfusingTernary")
public void onVmDefChanged(VmDefChanged event, VmChannel channel) public void onVmResourceChanged(VmResourceChanged event, VmChannel channel)
throws ApiException, TemplateException, IOException { throws ApiException, TemplateException, IOException {
// Ownership relationships takes care of deletions // Ownership relationships takes care of deletions
if (event.type() == K8sObserver.ResponseType.DELETED) { if (event.type() == K8sObserver.ResponseType.DELETED) {
return; return;
} }
// Reconcile
reconcile(event, channel);
}
private void reconcile(VmDefChanged event, VmChannel channel)
throws TemplateModelException, ApiException, IOException,
TemplateException {
// Create model for processing templates // Create model for processing templates
var vmDef = event.vmDefinition(); var vmDef = event.vmDefinition();
Map<String, Object> model = prepareModel(vmDef); Map<String, Object> model = prepareModel(vmDef);
cmReconciler.reconcile(model, channel, event.specChanged()); cmReconciler.reconcile(model, channel, event.specChanged());
// The remaining reconcilers depend only on changes of the spec part. // The remaining reconcilers depend only on changes of the spec part
if (!event.specChanged()) { // or the pod state.
if (!event.specChanged() && !event.podChanged()) {
return; return;
} }
dsReconciler.reconcile(vmDef, model, channel); dsReconciler.reconcile(vmDef, model, channel, event.specChanged());
// Manage (eventual) removal of stateful set. pvcReconciler.reconcile(vmDef, model, channel, event.specChanged());
stsReconciler.reconcile(vmDef, model, channel); podReconciler.reconcile(vmDef, model, channel, event.specChanged());
pvcReconciler.reconcile(vmDef, model, channel); lbReconciler.reconcile(vmDef, model, channel, event.specChanged());
podReconciler.reconcile(vmDef, model, channel);
lbReconciler.reconcile(vmDef, model, channel);
}
/**
* On pod changed.
*
* @param event the event
* @param channel the channel
* @throws ApiException the api exception
* @throws IOException Signals that an I/O exception has occurred.
* @throws TemplateException the template exception
*/
@Handler
public void onPodChanged(PodChanged event, VmChannel channel)
throws ApiException, IOException, TemplateException {
if (event.type() != ResponseType.DELETED) {
// Nothing to reconcile
return;
}
// If the pod was deleted, it may be necessary to recreate it
var vmDef = channel.vmDefinition();
Map<String, Object> model = prepareModel(vmDef);
// Call all steps because they may augment the model
cmReconciler.reconcile(model, channel, false);
dsReconciler.reconcile(vmDef, model, channel);
stsReconciler.reconcile(vmDef, model, channel);
pvcReconciler.reconcile(vmDef, model, channel);
podReconciler.reconcile(vmDef, model, channel);
} }
/** /**
@ -282,7 +241,8 @@ public class Reconciler extends Component {
public void onResetVm(ResetVm event, VmChannel channel) public void onResetVm(ResetVm event, VmChannel channel)
throws ApiException, IOException, TemplateException { throws ApiException, IOException, TemplateException {
var vmDef = channel.vmDefinition(); var vmDef = channel.vmDefinition();
vmDef.extra().ifPresent(e -> e.resetCount(e.resetCount() + 1)); var extra = vmDef.extra();
extra.resetCount(extra.resetCount() + 1);
Map<String, Object> model Map<String, Object> model
= prepareModel(channel.vmDefinition()); = prepareModel(channel.vmDefinition());
cmReconciler.reconcile(model, channel, true); cmReconciler.reconcile(model, channel, true);

View file

@ -1,107 +0,0 @@
/*
* 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 freemarker.template.Configuration;
import freemarker.template.TemplateException;
import io.kubernetes.client.custom.V1Patch;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.util.generic.options.PatchOptions;
import java.io.IOException;
import java.util.Map;
import java.util.logging.Logger;
import org.jdrupes.vmoperator.common.K8sV1StatefulSetStub;
import org.jdrupes.vmoperator.common.VmDefinition;
import org.jdrupes.vmoperator.common.VmDefinition.RequestedVmState;
import org.jdrupes.vmoperator.manager.events.VmChannel;
/**
* Before version 3.4, the pod running the VM was created by a stateful set.
* Starting with version 3.4, this reconciler simply deletes the stateful
* set, provided that the VM is not running.
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
/* default */ class StatefulSetReconciler {
protected final Logger logger = Logger.getLogger(getClass().getName());
/**
* Instantiates a new stateful set reconciler.
*
* @param fmConfig the fm config
*/
@SuppressWarnings("PMD.UnusedFormalParameter")
public StatefulSetReconciler(Configuration fmConfig) {
// Nothing to do
}
/**
* Reconcile stateful set.
*
* @param vmDef the VM definition
* @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
*/
@SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
public void reconcile(VmDefinition vmDef, Map<String, Object> model,
VmChannel channel)
throws IOException, TemplateException, ApiException {
model.put("usingSts", false);
// If exists, delete when not running or supposed to be not running.
var stsStub = K8sV1StatefulSetStub.get(channel.client(),
vmDef.namespace(), vmDef.name());
if (stsStub.model().isEmpty()) {
return;
}
// Stateful set still exists, check if replicas is 0 so we can
// delete it.
var stsModel = stsStub.model().get();
if (stsModel.getSpec().getReplicas() == 0) {
stsStub.delete();
return;
}
// Cannot yet delete the stateful set.
model.put("usingSts", true);
// Check if VM is supposed to be stopped. If so,
// set replicas to 0. This is the first step of the transition,
// the stateful set will be deleted when the VM is restarted.
if (vmDef.vmState() == RequestedVmState.RUNNING) {
return;
}
// Do apply changes (set replicas to 0)
PatchOptions opts = new PatchOptions();
opts.setForce(true);
opts.setFieldManager("kubernetes-java-kubectl-apply");
if (stsStub.patch(V1Patch.PATCH_FORMAT_JSON_PATCH,
new V1Patch("[{\"op\": \"replace\", \"path\": \"/spec/replicas"
+ "\", \"value\": 0}]"),
channel.client().defaultPatchOptions()).isEmpty()) {
logger.warning(
() -> "Could not patch stateful set for " + stsStub.name());
}
}
}

View file

@ -30,7 +30,6 @@ import java.net.HttpURLConnection;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@ -54,7 +53,7 @@ import org.jdrupes.vmoperator.manager.events.ModifyVm;
import org.jdrupes.vmoperator.manager.events.PodChanged; import org.jdrupes.vmoperator.manager.events.PodChanged;
import org.jdrupes.vmoperator.manager.events.UpdateAssignment; import org.jdrupes.vmoperator.manager.events.UpdateAssignment;
import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged; import org.jdrupes.vmoperator.manager.events.VmResourceChanged;
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.Event; import org.jgrapes.core.Event;
@ -157,11 +156,11 @@ public class VmMonitor extends
// Create and fire changed event. Remove channel from channel // Create and fire changed event. Remove channel from channel
// manager on completion. // manager on completion.
VmDefChanged chgEvt VmResourceChanged chgEvt
= new VmDefChanged(ResponseType.valueOf(response.type), = new VmResourceChanged(ResponseType.valueOf(response.type), vmDef,
channel.setGeneration(response.object.getMetadata() channel.setGeneration(response.object.getMetadata()
.getGeneration()), .getGeneration()),
vmDef); false);
if (ResponseType.valueOf(response.type) == ResponseType.DELETED) { if (ResponseType.valueOf(response.type) == ResponseType.DELETED) {
chgEvt = Event.onCompletion(chgEvt, chgEvt = Event.onCompletion(chgEvt,
e -> channelManager.remove(e.vmDefinition().name())); e -> channelManager.remove(e.vmDefinition().name()));
@ -181,8 +180,7 @@ public class VmMonitor extends
@SuppressWarnings("PMD.AvoidDuplicateLiterals") @SuppressWarnings("PMD.AvoidDuplicateLiterals")
private void addExtraData(VmDefinition vmDef, VmDefinition prevState) { private void addExtraData(VmDefinition vmDef, VmDefinition prevState) {
var extra = new VmExtraData(vmDef); var extra = new VmExtraData(vmDef);
var prevExtra var prevExtra = Optional.ofNullable(prevState).map(VmDefinition::extra);
= Optional.ofNullable(prevState).flatMap(VmDefinition::extra);
// Maintain (or initialize) the resetCount // Maintain (or initialize) the resetCount
extra.resetCount(prevExtra.map(VmExtraData::resetCount).orElse(0L)); extra.resetCount(prevExtra.map(VmExtraData::resetCount).orElse(0L));
@ -200,36 +198,36 @@ public class VmMonitor extends
*/ */
@Handler @Handler
public void onPodChanged(PodChanged event, VmChannel channel) { public void onPodChanged(PodChanged event, VmChannel channel) {
if (channel.vmDefinition().extra().isEmpty()) { var vmDef = channel.vmDefinition();
return; updateNodeInfo(event, vmDef);
channel
.fire(new VmResourceChanged(ResponseType.MODIFIED, vmDef, false, true));
} }
var extra = channel.vmDefinition().extra().get();
var pod = event.pod(); private void updateNodeInfo(PodChanged event, VmDefinition vmDef) {
var extra = vmDef.extra();
if (event.type() == ResponseType.DELETED) { if (event.type() == ResponseType.DELETED) {
// The status of a deleted pod is the status before deletion, // The status of a deleted pod is the status before deletion,
// i.e. the node info is still there. // i.e. the node info is still cached and must be removed.
extra.nodeInfo("", Collections.emptyList()); extra.nodeInfo("", Collections.emptyList());
} else { return;
}
// Get current node info from pod
var pod = event.pod();
var nodeName = Optional var nodeName = Optional
.ofNullable(pod.getSpec().getNodeName()).orElse(""); .ofNullable(pod.getSpec().getNodeName()).orElse("");
logger.finer(() -> "Adding node name " + nodeName logger.finer(() -> "Adding node name " + nodeName
+ " to VM info for " + channel.vmDefinition().name()); + " to VM info for " + vmDef.name());
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
var addrs = new ArrayList<String>(); var addrs = new ArrayList<String>();
Optional.ofNullable(pod.getStatus().getPodIPs()) Optional.ofNullable(pod.getStatus().getPodIPs())
.orElse(Collections.emptyList()).stream() .orElse(Collections.emptyList()).stream()
.map(ip -> ip.getIp()).forEach(addrs::add); .map(ip -> ip.getIp()).forEach(addrs::add);
logger.finer(() -> "Adding node addresses " + addrs logger.finer(() -> "Adding node addresses " + addrs
+ " to VM info for " + channel.vmDefinition().name()); + " to VM info for " + vmDef.name());
if (Objects.equals(nodeName, extra.nodeName())
&& Objects.equals(addrs, extra.nodeAddresses())) {
return;
}
extra.nodeInfo(nodeName, addrs); extra.nodeInfo(nodeName, addrs);
} }
channel.fire(new VmDefChanged(ResponseType.MODIFIED, false,
channel.vmDefinition()));
}
/** /**
* On modify vm. * On modify vm.

View file

@ -57,8 +57,8 @@ import org.jdrupes.vmoperator.manager.events.GetVms.VmData;
import org.jdrupes.vmoperator.manager.events.ModifyVm; import org.jdrupes.vmoperator.manager.events.ModifyVm;
import org.jdrupes.vmoperator.manager.events.ResetVm; import org.jdrupes.vmoperator.manager.events.ResetVm;
import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jdrupes.vmoperator.manager.events.VmPoolChanged; import org.jdrupes.vmoperator.manager.events.VmPoolChanged;
import org.jdrupes.vmoperator.manager.events.VmResourceChanged;
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;
@ -658,7 +658,7 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
@SuppressWarnings({ "PMD.ConfusingTernary", "PMD.CognitiveComplexity", @SuppressWarnings({ "PMD.ConfusingTernary", "PMD.CognitiveComplexity",
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.AvoidDuplicateLiterals", "PMD.AvoidInstantiatingObjectsInLoops", "PMD.AvoidDuplicateLiterals",
"PMD.ConfusingArgumentToVarargsMethod" }) "PMD.ConfusingArgumentToVarargsMethod" })
public void onVmDefChanged(VmDefChanged event, VmChannel channel) public void onVmResourceChanged(VmResourceChanged event, VmChannel channel)
throws IOException, InterruptedException { throws IOException, InterruptedException {
var vmDef = event.vmDefinition(); var vmDef = event.vmDefinition();
@ -847,8 +847,8 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
if (!event.secretAvailable()) { if (!event.secretAvailable()) {
return; return;
} }
vmDef.extra().map(xtra -> xtra.connectionFile(event.secret(), vmDef.extra().connectionFile(event.secret(),
preferredIpVersion, deleteConnectionFile)) preferredIpVersion, deleteConnectionFile)
.ifPresent(cf -> channel.respond(new NotifyConletView(type(), .ifPresent(cf -> channel.respond(new NotifyConletView(type(),
model.getConletId(), "openConsole", cf))); model.getConletId(), "openConsole", cf)));
} }

View file

@ -42,13 +42,12 @@ import org.jdrupes.vmoperator.common.Constants.Status;
import org.jdrupes.vmoperator.common.K8sObserver; import org.jdrupes.vmoperator.common.K8sObserver;
import org.jdrupes.vmoperator.common.VmDefinition; import org.jdrupes.vmoperator.common.VmDefinition;
import org.jdrupes.vmoperator.common.VmDefinition.Permission; import org.jdrupes.vmoperator.common.VmDefinition.Permission;
import org.jdrupes.vmoperator.common.VmExtraData;
import org.jdrupes.vmoperator.manager.events.ChannelTracker; import org.jdrupes.vmoperator.manager.events.ChannelTracker;
import org.jdrupes.vmoperator.manager.events.GetDisplaySecret; import org.jdrupes.vmoperator.manager.events.GetDisplaySecret;
import org.jdrupes.vmoperator.manager.events.ModifyVm; import org.jdrupes.vmoperator.manager.events.ModifyVm;
import org.jdrupes.vmoperator.manager.events.ResetVm; import org.jdrupes.vmoperator.manager.events.ResetVm;
import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged; import org.jdrupes.vmoperator.manager.events.VmResourceChanged;
import org.jdrupes.vmoperator.util.DataPath; import org.jdrupes.vmoperator.util.DataPath;
import org.jgrapes.core.Channel; import org.jgrapes.core.Channel;
import org.jgrapes.core.Event; import org.jgrapes.core.Event;
@ -258,7 +257,7 @@ public class VmMgmt extends FreeMarkerConlet<VmMgmt.VmsModel> {
"name", vmDef.name()), "name", vmDef.name()),
"spec", spec, "spec", spec,
"status", status, "status", status,
"nodeName", vmDef.extra().map(VmExtraData::nodeName).orElse(""), "nodeName", vmDef.extra().nodeName(),
"consoleAccessible", vmDef.consoleAccessible(user, perms), "consoleAccessible", vmDef.consoleAccessible(user, perms),
"permissions", perms); "permissions", perms);
} }
@ -274,7 +273,7 @@ public class VmMgmt extends FreeMarkerConlet<VmMgmt.VmsModel> {
@SuppressWarnings({ "PMD.ConfusingTernary", "PMD.CognitiveComplexity", @SuppressWarnings({ "PMD.ConfusingTernary", "PMD.CognitiveComplexity",
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.AvoidDuplicateLiterals", "PMD.AvoidInstantiatingObjectsInLoops", "PMD.AvoidDuplicateLiterals",
"PMD.ConfusingArgumentToVarargsMethod" }) "PMD.ConfusingArgumentToVarargsMethod" })
public void onVmDefChanged(VmDefChanged event, VmChannel channel) public void onVmResourceChanged(VmResourceChanged event, VmChannel channel)
throws IOException { throws IOException {
var vmName = event.vmDefinition().name(); var vmName = event.vmDefinition().name();
if (event.type() == K8sObserver.ResponseType.DELETED) { if (event.type() == K8sObserver.ResponseType.DELETED) {
@ -494,8 +493,8 @@ public class VmMgmt extends FreeMarkerConlet<VmMgmt.VmsModel> {
if (!event.secretAvailable()) { if (!event.secretAvailable()) {
return; return;
} }
vmDef.extra().map(xtra -> xtra.connectionFile(event.secret(), vmDef.extra().connectionFile(event.secret(),
preferredIpVersion, deleteConnectionFile)).ifPresent( preferredIpVersion, deleteConnectionFile).ifPresent(
cf -> channel.respond(new NotifyConletView(type(), cf -> channel.respond(new NotifyConletView(type(),
model.getConletId(), "openConsole", cf))); model.getConletId(), "openConsole", cf)));
} }

View file

@ -35,6 +35,10 @@ layout: vm-operator
have to add a virtual serial port (see the git history of the standard have to add a virtual serial port (see the git history of the standard
template for the required addition). template for the required addition).
* Stateful sets from pre 3.4.0 versions are no longer removed automatically
(see notes below). However, PVCs with the old naming scheme are still
reused.
## To version 3.4.0 ## To version 3.4.0
Starting with this version, the VM-Operator no longer uses a stateful set Starting with this version, the VM-Operator no longer uses a stateful set