Set loggedInUser via Reconciler.

This commit is contained in:
Michael Lipp 2025-03-04 13:33:54 +01:00
parent 28b1903acc
commit bfe4c2bb32
10 changed files with 111 additions and 74 deletions

View file

@ -141,6 +141,16 @@ public class VmDefinition extends K8sDynamicModel {
} }
} }
/**
* The assignment information.
*
* @param pool the pool
* @param user the user
* @param lastUsed the last used
*/
public record Assignment(String pool, String user, Instant lastUsed) {
}
/** /**
* Instantiates a new vm definition. * Instantiates a new vm definition.
* *
@ -215,31 +225,15 @@ public class VmDefinition extends K8sDynamicModel {
} }
/** /**
* The pool that the VM was taken from. * The assignment information.
* *
* @return the optional * @return the optional
*/ */
public Optional<String> assignedFrom() { public Optional<Assignment> assignment() {
return fromStatus(Status.ASSIGNMENT, "pool"); return this.<Map<String, Object>> fromStatus(Status.ASSIGNMENT)
} .filter(m -> !m.isEmpty()).map(a -> new Assignment(
a.get("pool").toString(), a.get("user").toString(),
/** Instant.parse(a.get("lastUsed").toString())));
* The user that the VM was assigned to.
*
* @return the optional
*/
public Optional<String> assignedTo() {
return fromStatus(Status.ASSIGNMENT, "user");
}
/**
* Last usage of assigned VM.
*
* @return the optional
*/
public Optional<Instant> assignmentLastUsed() {
return this.<String> fromStatus(Status.ASSIGNMENT, "lastUsed")
.map(Instant::parse);
} }
/** /**

View file

@ -27,6 +27,7 @@ import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.jdrupes.vmoperator.common.VmDefinition.Assignment;
import org.jdrupes.vmoperator.common.VmDefinition.Grant; import org.jdrupes.vmoperator.common.VmDefinition.Grant;
import org.jdrupes.vmoperator.common.VmDefinition.Permission; import org.jdrupes.vmoperator.common.VmDefinition.Permission;
import org.jdrupes.vmoperator.util.DataPath; import org.jdrupes.vmoperator.util.DataPath;
@ -165,13 +166,12 @@ public class VmPool {
} }
// If not assigned, it's usable // If not assigned, it's usable
if (vmDef.assignedTo().isEmpty()) { if (vmDef.assignment().isEmpty()) {
return true; return true;
} }
// Check if it is to be retained // Check if it is to be retained
if (vmDef.assignmentLastUsed() if (vmDef.assignment().map(Assignment::lastUsed).map(this::retainUntil)
.map(this::retainUntil)
.map(ru -> Instant.now().isBefore(ru)).orElse(false)) { .map(ru -> Instant.now().isBefore(ru)).orElse(false)) {
return false; return false;
} }

View file

@ -18,6 +18,7 @@
package org.jdrupes.vmoperator.manager.events; package org.jdrupes.vmoperator.manager.events;
import org.jdrupes.vmoperator.common.VmPool;
import org.jgrapes.core.Event; import org.jgrapes.core.Event;
/** /**
@ -26,31 +27,31 @@ import org.jgrapes.core.Event;
@SuppressWarnings("PMD.DataClass") @SuppressWarnings("PMD.DataClass")
public class UpdateAssignment extends Event<Boolean> { public class UpdateAssignment extends Event<Boolean> {
private final String usedPool; private final VmPool fromPool;
private final String toUser; private final String toUser;
/** /**
* Instantiates a new event. * Instantiates a new event.
* *
* @param usedPool the used pool * @param fromPool the pool from which the VM was assigned
* @param toUser the to user * @param toUser the to user
*/ */
public UpdateAssignment(String usedPool, String toUser) { public UpdateAssignment(VmPool fromPool, String toUser) {
this.usedPool = usedPool; this.fromPool = fromPool;
this.toUser = toUser; this.toUser = toUser;
} }
/** /**
* Gets the pool to assign from. * Gets the pool from which the VM was assigned.
* *
* @return the pool * @return the pool
*/ */
public String usedPool() { public VmPool fromPool() {
return usedPool; return fromPool;
} }
/** /**
* Gets the user to assign to. * Gets the user to whom the VM was assigned.
* *
* @return the to user * @return the to user
*/ */

View file

@ -201,8 +201,8 @@ data:
<#if spec.vm.display.outputs?? > <#if spec.vm.display.outputs?? >
outputs: ${ spec.vm.display.outputs?c } outputs: ${ spec.vm.display.outputs?c }
</#if> </#if>
<#if spec.vm.display.loggedInUser?? > <#if loginRequestedFor?? >
loggedInUser: "${ spec.vm.display.loggedInUser }" loggedInUser: "${ loginRequestedFor }"
</#if> </#if>
<#if spec.vm.display.spice??> <#if spec.vm.display.spice??>
spice: spice:

View file

@ -232,7 +232,7 @@ public class Controller extends Component {
if (vmStub.updateStatus(vmDef, from -> { if (vmStub.updateStatus(vmDef, from -> {
JsonObject status = from.statusJson(); JsonObject status = from.statusJson();
var assignment = GsonPtr.to(status).to(Status.ASSIGNMENT); var assignment = GsonPtr.to(status).to(Status.ASSIGNMENT);
assignment.set("pool", event.usedPool()); assignment.set("pool", event.fromPool().name());
assignment.set("user", event.toUser()); assignment.set("user", event.toUser());
assignment.set("lastUsed", Instant.now().toString()); assignment.set("lastUsed", Instant.now().toString());
return status; return status;

View file

@ -36,6 +36,7 @@ import org.jdrupes.vmoperator.common.K8sDynamicModel;
import org.jdrupes.vmoperator.common.K8sDynamicModels; 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.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;
@ -165,7 +166,7 @@ public class PoolMonitor extends
} }
// Sync last usage to console state change if user matches // Sync last usage to console state change if user matches
if (vmDef.assignedTo() if (vmDef.assignment().map(Assignment::user)
.map(at -> at.equals(vmDef.consoleUser().orElse(null))) .map(at -> at.equals(vmDef.consoleUser().orElse(null)))
.orElse(true)) { .orElse(true)) {
return; return;
@ -174,8 +175,8 @@ public class PoolMonitor extends
var ccChange = vmDef.condition("ConsoleConnected") var ccChange = vmDef.condition("ConsoleConnected")
.map(cc -> cc.getLastTransitionTime().toInstant()); .map(cc -> cc.getLastTransitionTime().toInstant());
if (ccChange if (ccChange
.map(tt -> vmDef.assignmentLastUsed().map(alu -> alu.isAfter(tt)) .map(tt -> vmDef.assignment().map(Assignment::lastUsed)
.orElse(true)) .map(alu -> alu.isAfter(tt)).orElse(true))
.orElse(true)) { .orElse(true)) {
return; return;
} }

View file

@ -45,6 +45,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.logging.Level;
import static org.jdrupes.vmoperator.common.Constants.APP_NAME; import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
import org.jdrupes.vmoperator.common.Constants.DisplaySecret; import org.jdrupes.vmoperator.common.Constants.DisplaySecret;
import org.jdrupes.vmoperator.common.Convertions; import org.jdrupes.vmoperator.common.Convertions;
@ -52,6 +53,9 @@ import org.jdrupes.vmoperator.common.K8sClient;
import org.jdrupes.vmoperator.common.K8sObserver; import org.jdrupes.vmoperator.common.K8sObserver;
import org.jdrupes.vmoperator.common.K8sV1SecretStub; import org.jdrupes.vmoperator.common.K8sV1SecretStub;
import org.jdrupes.vmoperator.common.VmDefinition; import org.jdrupes.vmoperator.common.VmDefinition;
import org.jdrupes.vmoperator.common.VmDefinition.Assignment;
import org.jdrupes.vmoperator.common.VmPool;
import org.jdrupes.vmoperator.manager.events.GetPools;
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.VmDefChanged;
@ -212,11 +216,6 @@ public class Reconciler extends Component {
@SuppressWarnings("PMD.ConfusingTernary") @SuppressWarnings("PMD.ConfusingTernary")
public void onVmDefChanged(VmDefChanged event, VmChannel channel) public void onVmDefChanged(VmDefChanged event, VmChannel channel)
throws ApiException, TemplateException, IOException { throws ApiException, TemplateException, IOException {
// We're only interested in "spec" changes.
if (!event.specChanged()) {
return;
}
// Ownership relationships takes care of deletions // Ownership relationships takes care of deletions
if (event.type() == K8sObserver.ResponseType.DELETED) { if (event.type() == K8sObserver.ResponseType.DELETED) {
logger.fine( logger.fine(
@ -228,6 +227,11 @@ public class Reconciler extends Component {
Map<String, Object> model Map<String, Object> model
= prepareModel(channel.client(), event.vmDefinition()); = prepareModel(channel.client(), event.vmDefinition());
var configMap = cmReconciler.reconcile(model, channel); var configMap = cmReconciler.reconcile(model, channel);
// The remaining reconcilers depend only on changes of the spec part.
if (!event.specChanged()) {
return;
}
model.put("cm", configMap); model.put("cm", configMap);
dsReconciler.reconcile(event, model, channel); dsReconciler.reconcile(event, model, channel);
// Manage (eventual) removal of stateful set. // Manage (eventual) removal of stateful set.
@ -266,24 +270,10 @@ public class Reconciler extends Component {
Optional.ofNullable(Reconciler.class.getPackage() Optional.ofNullable(Reconciler.class.getPackage()
.getImplementationVersion()).orElse("(Unknown)")); .getImplementationVersion()).orElse("(Unknown)"));
model.put("cr", vmDef); model.put("cr", vmDef);
// Freemarker's static models don't handle nested classes.
model.put("constants", constantsMap(Constants.class));
model.put("reconciler", config); model.put("reconciler", config);
model.put("constants", constantsMap(Constants.class));
// Check if we have a display secret addLoginRequestedFor(model, vmDef);
ListOptions options = new ListOptions(); addDisplaySecret(client, model, vmDef);
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
+ "app.kubernetes.io/component=" + DisplaySecret.NAME + ","
+ "app.kubernetes.io/instance=" + vmDef.name());
var dsStub = K8sV1SecretStub
.list(client, vmDef.namespace(), options)
.stream()
.findFirst();
if (dsStub.isPresent()) {
dsStub.get().model().ifPresent(m -> {
model.put("displaySecret", m.getMetadata().getName());
});
}
// Methods // Methods
model.put("parseQuantity", parseQuantityModel); model.put("parseQuantity", parseQuantityModel);
@ -294,6 +284,13 @@ public class Reconciler extends Component {
return model; return model;
} }
/**
* Creates a map with constants. Needed because freemarker doesn't support
* nested classes with its static models.
*
* @param clazz the clazz
* @return the map
*/
@SuppressWarnings("PMD.EmptyCatchBlock") @SuppressWarnings("PMD.EmptyCatchBlock")
private Map<String, Object> constantsMap(Class<?> clazz) { private Map<String, Object> constantsMap(Class<?> clazz) {
@SuppressWarnings("PMD.UseConcurrentHashMap") @SuppressWarnings("PMD.UseConcurrentHashMap")
@ -318,6 +315,38 @@ public class Reconciler extends Component {
return result; return result;
} }
private void addLoginRequestedFor(Map<String, Object> model,
VmDefinition vmDef) {
vmDef.assignment().filter(a -> {
try {
return newEventPipeline()
.fire(new GetPools().withName(a.pool())).get()
.stream().findFirst().map(VmPool::loginOnAssignment)
.orElse(false);
} catch (InterruptedException e) {
logger.log(Level.WARNING, e, e::getMessage);
}
return false;
}).map(Assignment::user)
.or(() -> vmDef.fromSpec("vm", "display", "loggedInUser"))
.ifPresent(u -> model.put("loginRequestedFor", u));
}
private void addDisplaySecret(K8sClient client, Map<String, Object> model,
VmDefinition vmDef) throws ApiException {
ListOptions options = new ListOptions();
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
+ "app.kubernetes.io/component=" + DisplaySecret.NAME + ","
+ "app.kubernetes.io/instance=" + vmDef.name());
var dsStub = K8sV1SecretStub
.list(client, vmDef.namespace(), options).stream().findFirst();
if (dsStub.isPresent()) {
dsStub.get().model().ifPresent(m -> {
model.put("displaySecret", m.getMetadata().getName());
});
}
}
private final TemplateMethodModelEx parseQuantityModel private final TemplateMethodModelEx parseQuantityModel
= new TemplateMethodModelEx() { = new TemplateMethodModelEx() {
@Override @Override

View file

@ -40,6 +40,7 @@ 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.VmDefinition; import org.jdrupes.vmoperator.common.VmDefinition;
import org.jdrupes.vmoperator.common.VmDefinition.Assignment;
import org.jdrupes.vmoperator.common.VmDefinitionStub; import org.jdrupes.vmoperator.common.VmDefinitionStub;
import org.jdrupes.vmoperator.common.VmDefinitions; import org.jdrupes.vmoperator.common.VmDefinitions;
import org.jdrupes.vmoperator.common.VmExtraData; import org.jdrupes.vmoperator.common.VmExtraData;
@ -234,10 +235,10 @@ public class VmMonitor extends
|| !c.vmDefinition().permissionsFor(event.user().orElse(null), || !c.vmDefinition().permissionsFor(event.user().orElse(null),
event.roles()).isEmpty()) event.roles()).isEmpty())
.filter(c -> event.fromPool().isEmpty() .filter(c -> event.fromPool().isEmpty()
|| c.vmDefinition().assignedFrom() || c.vmDefinition().assignment().map(Assignment::pool)
.map(p -> p.equals(event.fromPool().get())).orElse(false)) .map(p -> p.equals(event.fromPool().get())).orElse(false))
.filter(c -> event.toUser().isEmpty() .filter(c -> event.toUser().isEmpty()
|| c.vmDefinition().assignedTo() || c.vmDefinition().assignment().map(Assignment::user)
.map(u -> u.equals(event.toUser().get())).orElse(false)) .map(u -> u.equals(event.toUser().get())).orElse(false))
.map(c -> new VmData(c.vmDefinition(), c)) .map(c -> new VmData(c.vmDefinition(), c))
.toList()); .toList());
@ -257,9 +258,9 @@ public class VmMonitor extends
while (true) { while (true) {
// Search for existing assignment. // Search for existing assignment.
var vmQuery = channelManager.channels().stream() var vmQuery = channelManager.channels().stream()
.filter(c -> c.vmDefinition().assignedFrom() .filter(c -> c.vmDefinition().assignment().map(Assignment::pool)
.map(p -> p.equals(event.fromPool())).orElse(false)) .map(p -> p.equals(event.fromPool())).orElse(false))
.filter(c -> c.vmDefinition().assignedTo() .filter(c -> c.vmDefinition().assignment().map(Assignment::user)
.map(u -> u.equals(event.toUser())).orElse(false)) .map(u -> u.equals(event.toUser())).orElse(false))
.findFirst(); .findFirst();
if (vmQuery.isPresent()) { if (vmQuery.isPresent()) {
@ -280,7 +281,8 @@ public class VmMonitor extends
vmQuery = channelManager.channels().stream() vmQuery = channelManager.channels().stream()
.filter(c -> vmPool.isAssignable(c.vmDefinition())) .filter(c -> vmPool.isAssignable(c.vmDefinition()))
.sorted(Comparator.comparing((VmChannel c) -> c.vmDefinition() .sorted(Comparator.comparing((VmChannel c) -> c.vmDefinition()
.assignmentLastUsed().orElse(Instant.ofEpochSecond(0))) .assignment().map(Assignment::lastUsed)
.orElse(Instant.ofEpochSecond(0)))
.thenComparing(preferRunning)) .thenComparing(preferRunning))
.findFirst(); .findFirst();
@ -293,7 +295,7 @@ public class VmMonitor extends
var chosenVm = vmQuery.get(); var chosenVm = vmQuery.get();
var vmPipeline = chosenVm.pipeline(); var vmPipeline = chosenVm.pipeline();
if (Optional.ofNullable(vmPipeline.fire(new UpdateAssignment( if (Optional.ofNullable(vmPipeline.fire(new UpdateAssignment(
vmPool.name(), event.toUser()), chosenVm).get()) vmPool, event.toUser()), chosenVm).get())
.orElse(false)) { .orElse(false)) {
var vmDef = chosenVm.vmDefinition(); var vmDef = chosenVm.vmDefinition();
event.setResult(new VmData(vmDef, chosenVm)); event.setResult(new VmData(vmDef, chosenVm));
@ -301,10 +303,6 @@ public class VmMonitor extends
// Make sure that a newly assigned VM is running. // Make sure that a newly assigned VM is running.
chosenVm.pipeline().fire(new ModifyVm(vmDef.name(), chosenVm.pipeline().fire(new ModifyVm(vmDef.name(),
"state", "Running", chosenVm)); "state", "Running", chosenVm));
if (vmPool.loginOnAssignment()) {
chosenVm.pipeline().fire(new ModifyVm(vmDef.name(),
"display/loggedInUser", event.toUser(), chosenVm));
}
return; return;
} }
} }

View file

@ -327,6 +327,18 @@ public class GsonPtr {
return set(selector, new JsonPrimitive(value)); return set(selector, new JsonPrimitive(value));
} }
/**
* Short for `set(selector, new JsonPrimitive(value))`.
*
* @param selector the selector
* @param value the value
* @return the gson ptr
* @see #set(Object, JsonElement)
*/
public GsonPtr set(Object selector, Boolean value) {
return set(selector, new JsonPrimitive(value));
}
/** /**
* Same as {@link #set(Object, JsonElement)}, but sets the value * Same as {@link #set(Object, JsonElement)}, but sets the value
* only if it doesn't exist yet, else returns the existing value. * only if it doesn't exist yet, else returns the existing value.

View file

@ -46,6 +46,7 @@ import java.util.stream.Collectors;
import org.bouncycastle.util.Objects; import org.bouncycastle.util.Objects;
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.Assignment;
import org.jdrupes.vmoperator.common.VmDefinition.Permission; import org.jdrupes.vmoperator.common.VmDefinition.Permission;
import org.jdrupes.vmoperator.common.VmPool; import org.jdrupes.vmoperator.common.VmPool;
import org.jdrupes.vmoperator.manager.events.AssignVm; import org.jdrupes.vmoperator.manager.events.AssignVm;
@ -657,10 +658,11 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
var user var user
= WebConsoleUtils.userFromSession(connection.session()) = WebConsoleUtils.userFromSession(connection.session())
.map(ConsoleUser::getName).orElse(null); .map(ConsoleUser::getName).orElse(null);
var toBeUsedByConlet = vmDef.assignedFrom() var toBeUsedByConlet = vmDef.assignment()
.map(Assignment::pool)
.map(p -> p.equals(model.get().name())).orElse(false) .map(p -> p.equals(model.get().name())).orElse(false)
&& vmDef.assignedTo().map(u -> u.equals(user)) && vmDef.assignment().map(Assignment::user)
.orElse(false); .map(u -> u.equals(user)).orElse(false);
if (!Objects.areEqual(model.get().assignedVm(), if (!Objects.areEqual(model.get().assignedVm(),
vmDef.name()) && !toBeUsedByConlet) { vmDef.name()) && !toBeUsedByConlet) {
continue; continue;