From 77cfcff2ed003d1b886cfa5b2482adf8bf477f2e Mon Sep 17 00:00:00 2001 From: "Michael N. Lipp" Date: Fri, 29 Nov 2024 14:17:13 +0100 Subject: [PATCH 01/35] Fix toString. --- .../src/org/jdrupes/vmoperator/common/VmPool.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmPool.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmPool.java index 8da0a9f18..55746533c 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmPool.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmPool.java @@ -93,9 +93,8 @@ public String toString() { if (vms.size() <= 3) { builder.append(vms); } else { - builder.append('['); - vms.stream().limit(3).map(s -> s + ",").forEach(builder::append); - builder.append("...]"); + builder.append('[').append(vms.stream().limit(3).map(s -> s + ",") + .collect(Collectors.joining())).append("...]"); } builder.append(']'); return builder.toString(); From 367aebeee596c17695aa6ded9096037428d56ac2 Mon Sep 17 00:00:00 2001 From: "Michael N. Lipp" Date: Sun, 1 Dec 2024 13:41:18 +0100 Subject: [PATCH 02/35] Pool configurable in GUI. --- .../org/jdrupes/vmoperator/common/VmPool.java | 2 + .../vmaccess/VmAccess-edit.ftl.html | 38 +- .../vmoperator/vmaccess/l10n_de.properties | 2 +- .../jdrupes/vmoperator/vmaccess/VmAccess.java | 337 ++++++++++++------ .../vmaccess/browser/VmAccess-functions.ts | 44 ++- .../vmaccess/browser/VmAccess-style.scss | 5 + 6 files changed, 295 insertions(+), 133 deletions(-) diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmPool.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmPool.java index 55746533c..4b29adc9d 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmPool.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmPool.java @@ -60,6 +60,8 @@ public void setName(String name) { } /** + * All permissions. + * * @return the permissions */ public List permissions() { diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/VmAccess-edit.ftl.html b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/VmAccess-edit.ftl.html index ba61399d1..1a10a7dd8 100644 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/VmAccess-edit.ftl.html +++ b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/VmAccess-edit.ftl.html @@ -5,17 +5,33 @@ data-jgwc-on-unload="JGConsole.jgwc.unmountVueApps">
- {{ localize("Select VM") }} -

- -

+
+ {{ localize("Select VM or pool") }} +
    +
  • + +
  • +
  • + +
  • +
+
diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/l10n_de.properties b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/l10n_de.properties index bcdc33210..7d4ff231c 100644 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/l10n_de.properties +++ b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/l10n_de.properties @@ -1,7 +1,7 @@ conletName = VM-Zugriff okayLabel = Anwenden und Schließen -Select\ VM = VM auswählen +Select\ VM\ or\ pool = VM oder Pool auswählen Start\ VM = VM starten Stop\ VM = VM anhalten diff --git a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/VmAccess.java b/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/VmAccess.java index e1a41efb4..f671e93a7 100644 --- a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/VmAccess.java +++ b/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/VmAccess.java @@ -38,6 +38,7 @@ import java.util.Base64; import java.util.Collections; import java.util.EnumSet; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -49,13 +50,14 @@ import org.bouncycastle.util.Objects; import org.jdrupes.vmoperator.common.K8sObserver; import org.jdrupes.vmoperator.common.VmDefinition; -import org.jdrupes.vmoperator.common.VmDefinition.Permission; +import org.jdrupes.vmoperator.common.VmPool; import org.jdrupes.vmoperator.manager.events.ChannelTracker; import org.jdrupes.vmoperator.manager.events.GetDisplayPassword; import org.jdrupes.vmoperator.manager.events.ModifyVm; import org.jdrupes.vmoperator.manager.events.ResetVm; import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmDefChanged; +import org.jdrupes.vmoperator.manager.events.VmPoolChanged; import org.jgrapes.core.Channel; import org.jgrapes.core.Components; import org.jgrapes.core.Event; @@ -107,7 +109,7 @@ */ @SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports", "PMD.CouplingBetweenObjects", "PMD.GodClass", "PMD.TooManyMethods" }) -public class VmAccess extends FreeMarkerConlet { +public class VmAccess extends FreeMarkerConlet { private static final String VM_NAME_PROPERTY = "vmName"; private static final String RENDERED @@ -126,6 +128,8 @@ public class VmAccess extends FreeMarkerConlet { private Set syncUsers = Collections.emptySet(); private Set syncRoles = Collections.emptySet(); private boolean deleteConnectionFile = true; + @SuppressWarnings("PMD.UseConcurrentHashMap") + private final Map vmPools = new HashMap<>(); /** * The periodically generated update event. @@ -247,20 +251,28 @@ public void onConsoleReady(ConsoleReady event, ConsoleConnection channel) * @throws InterruptedException the interrupted exception */ @Handler - @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") public void onConsoleConfigured(ConsoleConfigured event, ConsoleConnection connection) throws InterruptedException, IOException { @SuppressWarnings("unchecked") - final var rendered = (Set) connection.session().get(RENDERED); + final var rendered + = (Set) connection.session().get(RENDERED); connection.session().remove(RENDERED); if (!syncPreviews(connection.session())) { return; } + addMissingVms(event, connection, rendered); + } + + @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") + private void addMissingVms(ConsoleConfigured event, + ConsoleConnection connection, final Set rendered) { boolean foundMissing = false; for (var vmName : accessibleVms(connection)) { - if (rendered.contains(vmName)) { + if (rendered.stream() + .anyMatch(r -> r.type() == ResourceModel.Type.VM + && r.name().equals(vmName))) { continue; } if (!foundMissing) { @@ -300,13 +312,12 @@ private String storagePath(Session session, String conletId) { } @Override - protected Optional createNewState(AddConletRequest event, + protected Optional createNewState(AddConletRequest event, ConsoleConnection connection, String conletId) throws Exception { - var model = new ViewerModel(conletId); - model.vmName = (String) event.properties().get(VM_NAME_PROPERTY); - if (model.vmName != null) { - model.setGenerated(true); - } + var model = new ResourceModel(conletId); + model.setType(ResourceModel.Type.VM); + model + .setName((String) event.properties().get(VM_NAME_PROPERTY)); String jsonState = objectMapper.writeValueAsString(model); connection.respond(new KeyValueStoreUpdate().update( storagePath(connection.session(), model.getConletId()), jsonState)); @@ -314,9 +325,9 @@ protected Optional createNewState(AddConletRequest event, } @Override - protected Optional createStateRepresentation(Event event, + protected Optional createStateRepresentation(Event event, ConsoleConnection connection, String conletId) throws Exception { - var model = new ViewerModel(conletId); + var model = new ResourceModel(conletId); String jsonState = objectMapper.writeValueAsString(model); connection.respond(new KeyValueStoreUpdate().update( storagePath(connection.session(), model.getConletId()), jsonState)); @@ -325,7 +336,7 @@ protected Optional createStateRepresentation(Event event, @Override @SuppressWarnings("PMD.EmptyCatchBlock") - protected Optional recreateState(Event event, + protected Optional recreateState(Event event, ConsoleConnection channel, String conletId) throws Exception { KeyValueStoreQuery query = new KeyValueStoreQuery( storagePath(channel.session(), conletId), channel); @@ -334,8 +345,8 @@ protected Optional recreateState(Event event, if (!query.results().isEmpty()) { var json = query.results().get(0).values().stream().findFirst() .get(); - ViewerModel model - = objectMapper.readValue(json, ViewerModel.class); + ResourceModel model + = objectMapper.readValue(json, ResourceModel.class); return Optional.of(model); } } catch (InterruptedException e) { @@ -347,58 +358,23 @@ protected Optional recreateState(Event event, } @Override - @SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops", "unchecked" }) + @SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops" }) protected Set doRenderConlet(RenderConletRequestBase event, - ConsoleConnection channel, String conletId, ViewerModel model) + ConsoleConnection channel, String conletId, ResourceModel model) throws Exception { - ResourceBundle resourceBundle = resourceBundle(channel.locale()); - Set renderedAs = EnumSet.noneOf(RenderMode.class); if (event.renderAs().contains(RenderMode.Preview)) { - channel.associated(PENDING, Event.class) - .ifPresent(e -> { - e.resumeHandling(); - channel.setAssociated(PENDING, null); - }); - - // Remove conlet if definition has been removed - if (model.vmName() != null - && !channelTracker.associated(model.vmName()).isPresent()) { - channel.respond( - new DeleteConlet(conletId, Collections.emptySet())); - return Collections.emptySet(); - } - - // Don't render if user has not at least one permission - if (model.vmName() != null - && channelTracker.associated(model.vmName()) - .map(d -> permissions(d, channel.session()).isEmpty()) - .orElse(true)) { - return Collections.emptySet(); - } - - // Render - Template tpl - = freemarkerConfig().getTemplate("VmAccess-preview.ftl.html"); - channel.respond(new RenderConlet(type(), conletId, - processTemplate(event, tpl, - fmModel(event, channel, conletId, model))) - .setRenderAs( - RenderMode.Preview.addModifiers(event.renderAs())) - .setSupportedModes(syncPreviews(channel.session()) - ? MODES_FOR_GENERATED - : MODES)); - renderedAs.add(RenderMode.Preview); - if (!Strings.isNullOrEmpty(model.vmName())) { - Optional.ofNullable(channel.session().get(RENDERED)) - .ifPresent(s -> ((Set) s).add(model.vmName())); - updateConfig(channel, model); - } + return renderPreview(event, channel, conletId, model); } + + // Render edit + ResourceBundle resourceBundle = resourceBundle(channel.locale()); + Set renderedAs = EnumSet.noneOf(RenderMode.class); if (event.renderAs().contains(RenderMode.Edit)) { Template tpl = freemarkerConfig() .getTemplate("VmAccess-edit.ftl.html"); var fmModel = fmModel(event, channel, conletId, model); fmModel.put("vmNames", accessibleVms(channel)); + fmModel.put("poolNames", accessiblePools(channel)); channel.respond(new OpenModalDialog(type(), conletId, processTemplate(event, tpl, fmModel)) .addOption("cancelable", true) @@ -408,13 +384,69 @@ protected Set doRenderConlet(RenderConletRequestBase event, return renderedAs; } + @SuppressWarnings("unchecked") + private Set renderPreview(RenderConletRequestBase event, + ConsoleConnection channel, String conletId, ResourceModel model) + throws TemplateNotFoundException, MalformedTemplateNameException, + ParseException, IOException { + channel.associated(PENDING, Event.class) + .ifPresent(e -> { + e.resumeHandling(); + channel.setAssociated(PENDING, null); + }); + + if (model.type() == ResourceModel.Type.VM && model.name() != null) { + // Remove conlet if VM definition has been removed + // or user has not at least one permission + Optional vmDef + = channelTracker.associated(model.name()); + if (vmDef.isEmpty() + || vmPermissions(vmDef.get(), channel.session()).isEmpty()) { + channel.respond( + new DeleteConlet(conletId, Collections.emptySet())); + return Collections.emptySet(); + } + } + + if (model.type() == ResourceModel.Type.POOL && model.name() != null) { + // Remove conlet if pool definition has been removed + // or user has not at least one permission + VmPool pool = vmPools.get(model.name()); + if (pool == null + || poolPermissions(pool, channel.session()).isEmpty()) { + channel.respond( + new DeleteConlet(conletId, Collections.emptySet())); + return Collections.emptySet(); + } + } + + // Render + Template tpl + = freemarkerConfig().getTemplate("VmAccess-preview.ftl.html"); + channel.respond(new RenderConlet(type(), conletId, + processTemplate(event, tpl, + fmModel(event, channel, conletId, model))) + .setRenderAs( + RenderMode.Preview.addModifiers(event.renderAs())) + .setSupportedModes(syncPreviews(channel.session()) + ? MODES_FOR_GENERATED + : MODES)); + if (!Strings.isNullOrEmpty(model.name())) { + Optional.ofNullable(channel.session().get(RENDERED)) + .ifPresent(s -> ((Set) s).add(model)); + updateConfig(channel, model); + } + return EnumSet.of(RenderMode.Preview); + } + private List accessibleVms(ConsoleConnection channel) { return channelTracker.associated().stream() - .filter(d -> !permissions(d, channel.session()).isEmpty()) + .filter(d -> !vmPermissions(d, channel.session()).isEmpty()) .map(d -> d.getMetadata().getName()).sorted().toList(); } - private Set permissions(VmDefinition vmDef, Session session) { + private Set vmPermissions(VmDefinition vmDef, + Session session) { var user = WebConsoleUtils.userFromSession(session) .map(ConsoleUser::getName).orElse(null); var roles = WebConsoleUtils.rolesFromSession(session) @@ -422,17 +454,32 @@ private Set permissions(VmDefinition vmDef, Session session) { return vmDef.permissionsFor(user, roles); } - private void updateConfig(ConsoleConnection channel, ViewerModel model) { + private List accessiblePools(ConsoleConnection channel) { + return vmPools.values().stream() + .filter(d -> !poolPermissions(d, channel.session()).isEmpty()) + .map(d -> d.name()).sorted().toList(); + } + + private Set poolPermissions(VmPool pool, + Session session) { + var user = WebConsoleUtils.userFromSession(session) + .map(ConsoleUser::getName).orElse(null); + var roles = WebConsoleUtils.rolesFromSession(session) + .stream().map(ConsoleRole::getName).toList(); + return pool.permissionsFor(user, roles); + } + + private void updateConfig(ConsoleConnection channel, ResourceModel model) { channel.respond(new NotifyConletView(type(), - model.getConletId(), "updateConfig", model.vmName())); + model.getConletId(), "updateConfig", model.type(), model.name())); updateVmDef(channel, model); } - private void updateVmDef(ConsoleConnection channel, ViewerModel model) { - if (Strings.isNullOrEmpty(model.vmName())) { + private void updateVmDef(ConsoleConnection channel, ResourceModel model) { + if (Strings.isNullOrEmpty(model.name())) { return; } - channelTracker.value(model.vmName()).ifPresent(item -> { + channelTracker.value(model.name()).ifPresent(item -> { try { var vmDef = item.associated(); var data = Map.of("metadata", @@ -441,8 +488,8 @@ private void updateVmDef(ConsoleConnection channel, ViewerModel model) { "spec", vmDef.spec(), "status", vmDef.getStatus(), "userPermissions", - permissions(vmDef, channel.session()).stream() - .map(Permission::toString).toList()); + vmPermissions(vmDef, channel.session()).stream() + .map(VmDefinition.Permission::toString).toList()); channel.respond(new NotifyConletView(type(), model.getConletId(), "updateVmDefinition", data)); } catch (JsonSyntaxException e) { @@ -454,7 +501,8 @@ private void updateVmDef(ConsoleConnection channel, ViewerModel model) { @Override protected void doConletDeleted(ConletDeleted event, - ConsoleConnection channel, String conletId, ViewerModel conletState) + ConsoleConnection channel, String conletId, + ResourceModel conletState) throws Exception { if (event.renderModes().isEmpty()) { channel.respond(new KeyValueStoreUpdate().delete( @@ -487,7 +535,7 @@ public void onVmDefChanged(VmDefChanged event, VmChannel channel) for (var conletId : entry.getValue()) { var model = stateFromSession(connection.session(), conletId); if (model.isEmpty() - || !Objects.areEqual(model.get().vmName(), vmName)) { + || !Objects.areEqual(model.get().name(), vmName)) { continue; } if (event.type() == K8sObserver.ResponseType.DELETED) { @@ -500,21 +548,36 @@ public void onVmDefChanged(VmDefChanged event, VmChannel channel) } } + /** + * On vm pool changed. + * + * @param event the event + * @param channel the channel + */ + @Handler(namedChannels = "manager") + public void onVmPoolChanged(VmPoolChanged event) { + if (event.deleted()) { + vmPools.remove(event.vmPool().name()); + return; + } + vmPools.put(event.vmPool().name(), event.vmPool()); + } + @Override @SuppressWarnings({ "PMD.AvoidDecimalLiteralsInBigDecimalConstructor", "PMD.ConfusingArgumentToVarargsMethod", "PMD.NcssCount", "PMD.AvoidLiteralsInIfCondition" }) protected void doUpdateConletState(NotifyConletModel event, - ConsoleConnection channel, ViewerModel model) + ConsoleConnection channel, ResourceModel model) throws Exception { event.stop(); - if ("selectedVm".equals(event.method())) { - selectVm(event, channel, model); + if ("selectedResource".equals(event.method())) { + selectResource(event, channel, model); return; } // Handle command for selected VM - var both = Optional.ofNullable(model.vmName()) + var both = Optional.ofNullable(model.name()) .flatMap(vm -> channelTracker.value(vm)); if (both.isEmpty()) { return; @@ -522,31 +585,31 @@ protected void doUpdateConletState(NotifyConletModel event, var vmChannel = both.get().channel(); var vmDef = both.get().associated(); var vmName = vmDef.metadata().getName(); - var perms = permissions(vmDef, channel.session()); + var perms = vmPermissions(vmDef, channel.session()); var resourceBundle = resourceBundle(channel.locale()); switch (event.method()) { case "start": - if (perms.contains(Permission.START)) { + if (perms.contains(VmDefinition.Permission.START)) { fire(new ModifyVm(vmName, "state", "Running", vmChannel)); } break; case "stop": - if (perms.contains(Permission.STOP)) { + if (perms.contains(VmDefinition.Permission.STOP)) { fire(new ModifyVm(vmName, "state", "Stopped", vmChannel)); } break; case "reset": - if (perms.contains(Permission.RESET)) { + if (perms.contains(VmDefinition.Permission.RESET)) { confirmReset(event, channel, model, resourceBundle); } break; case "resetConfirmed": - if (perms.contains(Permission.RESET)) { + if (perms.contains(VmDefinition.Permission.RESET)) { fire(new ResetVm(vmName), vmChannel); } break; case "openConsole": - if (perms.contains(Permission.ACCESS_CONSOLE)) { + if (perms.contains(VmDefinition.Permission.ACCESS_CONSOLE)) { var user = WebConsoleUtils.userFromSession(channel.session()) .map(ConsoleUser::getName).orElse(""); var pwQuery @@ -561,17 +624,26 @@ protected void doUpdateConletState(NotifyConletModel event, } } - private void selectVm(NotifyConletModel event, ConsoleConnection channel, - ViewerModel model) throws JsonProcessingException { - model.setVmName(event.param(0)); - String jsonState = objectMapper.writeValueAsString(model); - channel.respond(new KeyValueStoreUpdate().update(storagePath( - channel.session(), model.getConletId()), jsonState)); - updateConfig(channel, model); + @SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition", + "PMD.UseLocaleWithCaseConversions" }) + private void selectResource(NotifyConletModel event, + ConsoleConnection channel, ResourceModel model) + throws JsonProcessingException { + try { + model.setType(ResourceModel.Type + .valueOf(event. param(0).toUpperCase())); + model.setName(event.param(1)); + String jsonState = objectMapper.writeValueAsString(model); + channel.respond(new KeyValueStoreUpdate().update(storagePath( + channel.session(), model.getConletId()), jsonState)); + updateConfig(channel, model); + } catch (IllegalArgumentException e) { + logger.warning(() -> "Invalid resource type: " + e.getMessage()); + } } private void openConsole(String vmName, ConsoleConnection connection, - ViewerModel model, String password) { + ResourceModel model, String password) { var vmDef = channelTracker.associated(vmName).orElse(null); if (vmDef == null) { return; @@ -642,7 +714,7 @@ private Optional displayIp(VmDefinition vmDef) { } private void confirmReset(NotifyConletModel event, - ConsoleConnection channel, ViewerModel model, + ConsoleConnection channel, ResourceModel model, ResourceBundle resourceBundle) throws TemplateNotFoundException, MalformedTemplateNameException, ParseException, IOException { Template tpl = freemarkerConfig() @@ -662,58 +734,97 @@ protected boolean doSetLocale(SetLocale event, ConsoleConnection channel, } /** - * The Class VmsModel. + * The Class AccessModel. */ @SuppressWarnings("PMD.DataClass") - public static class ViewerModel extends ConletBaseModel { + public static class ResourceModel extends ConletBaseModel { - private String vmName; - private boolean generated; + /** + * The Enum ResourceType. + */ + @SuppressWarnings("PMD.ShortVariable") + public enum Type { + VM, POOL + } + + private Type type; + private String name; /** - * Instantiates a new vms model. + * Instantiates a new resource model. * * @param conletId the conlet id */ - public ViewerModel(@JsonProperty("conletId") String conletId) { + public ResourceModel(@JsonProperty("conletId") String conletId) { super(conletId); } /** - * Gets the vm name. + * Gets the resource name. * - * @return the vmName + * @return the string */ - @JsonGetter("vmName") - public String vmName() { - return vmName; + @JsonGetter("name") + public String name() { + return name; } /** - * Sets the vm name. + * Sets the name. * - * @param vmName the vmName to set + * @param name the resource name to set */ - public void setVmName(String vmName) { - this.vmName = vmName; + public void setName(String name) { + this.name = name; } /** - * Checks if is generated. - * - * @return the generated + * @return the resourceType */ - public boolean isGenerated() { - return generated; + @JsonGetter("type") + public Type type() { + return type; } /** - * Sets the generated. + * Sets the type. * - * @param generated the generated to set + * @param type the resource type to set */ - public void setGenerated(boolean generated) { - this.generated = generated; + public void setType(Type type) { + this.type = type; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + java.util.Objects.hash(name, type); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + ResourceModel other = (ResourceModel) obj; + return java.util.Objects.equals(name, other.name) + && type == other.type; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(50); + builder.append("AccessModel [resourceType=").append(type) + .append(", resourceName=").append(name).append(']'); + return builder.toString(); } } diff --git a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/browser/VmAccess-functions.ts b/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/browser/VmAccess-functions.ts index fb523537c..c5f3da023 100644 --- a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/browser/VmAccess-functions.ts +++ b/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/browser/VmAccess-functions.ts @@ -44,6 +44,7 @@ interface Api { /* eslint-disable @typescript-eslint/no-explicit-any */ vmName: string; vmDefinition: any; + poolName: string; } const localize = (key: string) => { @@ -62,7 +63,8 @@ window.orgJDrupesVmOperatorVmAccess.initPreview = (previewDom: HTMLElement, const previewApi: Api = reactive({ vmName: "", - vmDefinition: {} + vmDefinition: {}, + poolName: "" }); const configured = computed(() => previewApi.vmDefinition.spec); const startable = computed(() => previewApi.vmDefinition.spec && @@ -76,7 +78,8 @@ window.orgJDrupesVmOperatorVmAccess.initPreview = (previewDom: HTMLElement, const permissions = computed(() => previewApi.vmDefinition.spec ? previewApi.vmDefinition.userPermissions : []); - watch(() => previewApi.vmName, (name: string) => { + watch(previewApi, (api: Api) => { + const name = api.vmName || api.poolName; if (name !== "") { JGConsole.instance.updateConletTitle(conletId, name); } @@ -139,14 +142,21 @@ window.orgJDrupesVmOperatorVmAccess.initPreview = (previewDom: HTMLElement, }; JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmaccess.VmAccess", - "updateConfig", function(conletId: string, vmName: string) { + "updateConfig", + function(conletId: string, type: string, resource: string) { const conlet = JGConsole.findConletPreview(conletId); if (!conlet) { return; } const api = getApi(conlet.element().querySelector( ":scope .jdrupes-vmoperator-vmaccess-preview"))!; - api.vmName = vmName; + if (type === "VM") { + api.vmName = resource; + api.poolName = ""; + } else { + api.poolName = resource; + api.vmName = ""; + } }); JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmaccess.VmAccess", @@ -203,19 +213,36 @@ window.orgJDrupesVmOperatorVmAccess.initEdit = (dialogDom: HTMLElement, l10nBundles, JGWC.lang()!, key); }; + const resource = ref("vm"); const vmNameInput = ref(""); + const poolNameInput = ref(""); + + watch(resource, (resource: string) => { + if (resource === "vm") { + poolNameInput.value = ""; + } + if (resource === "pool") + vmNameInput.value = ""; + }); + const conletId = (dialogDom.closest( "[data-conlet-id]")!).dataset["conletId"]!; const conlet = JGConsole.findConletPreview(conletId); if (conlet) { const api = getApi(conlet.element().querySelector( ":scope .jdrupes-vmoperator-vmaccess-preview"))!; + if (api.poolName) { + resource.value = "pool"; + } vmNameInput.value = api.vmName; + poolNameInput.value = api.poolName; } - provideApi(dialogDom, vmNameInput); + provideApi(dialogDom, { resource: () => resource.value, + name: () => resource.value === "vm" + ? vmNameInput.value : poolNameInput.value }); - return { formId, localize, vmNameInput }; + return { formId, localize, resource, vmNameInput, poolNameInput }; } }); app.use(JgwcPlugin); @@ -229,8 +256,9 @@ window.orgJDrupesVmOperatorVmAccess.applyEdit = } const conletId = (dialogDom.closest("[data-conlet-id]")!) .dataset["conletId"]!; - const vmName = getApi>(dialogDom!)!.value; - JGConsole.notifyConletModel(conletId, "selectedVm", vmName); + const editApi = getApi>(dialogDom!)!; + JGConsole.notifyConletModel(conletId, "selectedResource", editApi.resource(), + editApi.name()); } window.orgJDrupesVmOperatorVmAccess.confirmReset = diff --git a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/browser/VmAccess-style.scss b/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/browser/VmAccess-style.scss index 547dc746c..cf0fb56a6 100644 --- a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/browser/VmAccess-style.scss +++ b/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/browser/VmAccess-style.scss @@ -77,6 +77,11 @@ } .jdrupes-vmoperator-vmaccess.jdrupes-vmoperator-vmaccess-edit { + + fieldset ul li { + margin-top: 0.5em; + } + select { width: 15em; } From 2dc93f1370debfa03f93db5be5be6e7201ac1280 Mon Sep 17 00:00:00 2001 From: "Michael N. Lipp" Date: Sun, 1 Dec 2024 14:21:25 +0100 Subject: [PATCH 03/35] Unify permission usage. --- .../vmoperator/common/VmDefinition.java | 23 +++++- .../org/jdrupes/vmoperator/common/VmPool.java | 71 +------------------ .../vmoperator/manager/PoolManager.java | 3 +- .../jdrupes/vmoperator/vmaccess/VmAccess.java | 44 +++++++++--- 4 files changed, 63 insertions(+), 78 deletions(-) diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinition.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinition.java index 71ea7f10a..bab0802b3 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinition.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinition.java @@ -94,6 +94,28 @@ public String toString() { } } + /** + * Permissions granted to a user or role. + * + * @param user the user + * @param role the role + * @param may the may + */ + public record Grant(String user, String role, Set may) { + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (user != null) { + builder.append("User ").append(user); + } else { + builder.append("Role ").append(role); + } + builder.append(" may=").append(may).append(']'); + return builder.toString(); + } + } + /** * Gets the kind. * @@ -292,7 +314,6 @@ public String namespace() { * @return the string */ public RequestedVmState vmState() { - // TODO return fromVm("state") .map(s -> "Running".equals(s) ? RequestedVmState.RUNNING : RequestedVmState.STOPPED) diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmPool.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmPool.java index 4b29adc9d..07e06161b 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmPool.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmPool.java @@ -20,14 +20,13 @@ import java.util.Collection; import java.util.Collections; -import java.util.EnumSet; -import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; +import org.jdrupes.vmoperator.common.VmDefinition.Grant; +import org.jdrupes.vmoperator.common.VmDefinition.Permission; import org.jdrupes.vmoperator.util.DataPath; /** @@ -60,7 +59,7 @@ public void setName(String name) { } /** - * All permissions. + * Permissions granted for a VM from the pool. * * @return the permissions */ @@ -120,68 +119,4 @@ public Set permissionsFor(String user, .flatMap(Function.identity()).collect(Collectors.toSet()); } - /** - * A permission grant to a user or role. - * - * @param user the user - * @param role the role - * @param may the may - */ - public record Grant(String user, String role, Set may) { - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - if (user != null) { - builder.append("User ").append(user); - } else { - builder.append("Role ").append(role); - } - builder.append(" may=").append(may).append(']'); - return builder.toString(); - } - } - - /** - * Permissions for accessing and manipulating the pool. - */ - public enum Permission { - START("start"), STOP("stop"), RESET("reset"), - ACCESS_CONSOLE("accessConsole"); - - @SuppressWarnings("PMD.UseConcurrentHashMap") - private static Map reprs = new HashMap<>(); - - static { - for (var value : EnumSet.allOf(Permission.class)) { - reprs.put(value.repr, value); - } - } - - private final String repr; - - Permission(String repr) { - this.repr = repr; - } - - /** - * Create permission from representation in CRD. - * - * @param value the value - * @return the permission - */ - @SuppressWarnings("PMD.AvoidLiteralsInIfCondition") - public static Set parse(String value) { - if ("*".equals(value)) { - return EnumSet.allOf(Permission.class); - } - return Set.of(reprs.get(value)); - } - - @Override - public String toString() { - return repr; - } - } - } diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PoolManager.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PoolManager.java index fb1de2738..7bc455d7e 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PoolManager.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PoolManager.java @@ -142,9 +142,10 @@ protected void handleChange(K8sClient client, V1ObjectMeta metadata = response.object.getMetadata(); vmPool.setName(metadata.getName()); - // If modified, merge changes + // If modified, merge changes and notify if (type == ResponseType.MODIFIED && pools.containsKey(poolName)) { pools.get(poolName).setPermissions(vmPool.permissions()); + poolPipeline.fire(new VmPoolChanged(vmPool)); return; } diff --git a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/VmAccess.java b/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/VmAccess.java index f671e93a7..4f4ef5312 100644 --- a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/VmAccess.java +++ b/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/VmAccess.java @@ -50,6 +50,7 @@ import org.bouncycastle.util.Objects; import org.jdrupes.vmoperator.common.K8sObserver; import org.jdrupes.vmoperator.common.VmDefinition; +import org.jdrupes.vmoperator.common.VmDefinition.Permission; import org.jdrupes.vmoperator.common.VmPool; import org.jdrupes.vmoperator.manager.events.ChannelTracker; import org.jdrupes.vmoperator.manager.events.GetDisplayPassword; @@ -108,7 +109,8 @@ * */ @SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports", - "PMD.CouplingBetweenObjects", "PMD.GodClass", "PMD.TooManyMethods" }) + "PMD.CouplingBetweenObjects", "PMD.GodClass", "PMD.TooManyMethods", + "PMD.CyclomaticComplexity" }) public class VmAccess extends FreeMarkerConlet { private static final String VM_NAME_PROPERTY = "vmName"; @@ -265,7 +267,8 @@ public void onConsoleConfigured(ConsoleConfigured event, addMissingVms(event, connection, rendered); } - @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") + @SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops", + "PMD.AvoidDuplicateLiterals" }) private void addMissingVms(ConsoleConfigured event, ConsoleConnection connection, final Set rendered) { boolean foundMissing = false; @@ -445,7 +448,7 @@ private List accessibleVms(ConsoleConnection channel) { .map(d -> d.getMetadata().getName()).sorted().toList(); } - private Set vmPermissions(VmDefinition vmDef, + private Set vmPermissions(VmDefinition vmDef, Session session) { var user = WebConsoleUtils.userFromSession(session) .map(ConsoleUser::getName).orElse(null); @@ -460,7 +463,7 @@ private List accessiblePools(ConsoleConnection channel) { .map(d -> d.name()).sorted().toList(); } - private Set poolPermissions(VmPool pool, + private Set poolPermissions(VmPool pool, Session session) { var user = WebConsoleUtils.userFromSession(session) .map(ConsoleUser::getName).orElse(null); @@ -530,15 +533,19 @@ public void onVmDefChanged(VmDefChanged event, VmChannel channel) } else { channelTracker.put(vmName, channel, vmDef); } + + // Update known conlets for (var entry : conletIdsByConsoleConnection().entrySet()) { var connection = entry.getKey(); for (var conletId : entry.getValue()) { var model = stateFromSession(connection.session(), conletId); if (model.isEmpty() + || model.get().type() != ResourceModel.Type.VM || !Objects.areEqual(model.get().name(), vmName)) { continue; } - if (event.type() == K8sObserver.ResponseType.DELETED) { + if (event.type() == K8sObserver.ResponseType.DELETED + || vmPermissions(vmDef, connection.session()).isEmpty()) { connection.respond( new DeleteConlet(conletId, Collections.emptySet())); } else { @@ -555,12 +562,33 @@ public void onVmDefChanged(VmDefChanged event, VmChannel channel) * @param channel the channel */ @Handler(namedChannels = "manager") + @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") public void onVmPoolChanged(VmPoolChanged event) { + var poolName = event.vmPool().name(); if (event.deleted()) { - vmPools.remove(event.vmPool().name()); - return; + vmPools.remove(poolName); + } else { + vmPools.put(poolName, event.vmPool()); + } + + // Update known conlets + for (var entry : conletIdsByConsoleConnection().entrySet()) { + var connection = entry.getKey(); + for (var conletId : entry.getValue()) { + var model = stateFromSession(connection.session(), conletId); + if (model.isEmpty() + || model.get().type() != ResourceModel.Type.POOL + || !Objects.areEqual(model.get().name(), poolName)) { + continue; + } + if (event.deleted() + || poolPermissions(event.vmPool(), connection.session()) + .isEmpty()) { + connection.respond( + new DeleteConlet(conletId, Collections.emptySet())); + } + } } - vmPools.put(event.vmPool().name(), event.vmPool()); } @Override From c3428ea4a5912e2b5bba46880f9435caf97a1cef Mon Sep 17 00:00:00 2001 From: "Michael N. Lipp" Date: Sun, 1 Dec 2024 14:22:18 +0100 Subject: [PATCH 04/35] Rename pool manager to monitor. --- .../src/org/jdrupes/vmoperator/manager/Controller.java | 2 +- .../vmoperator/manager/{PoolManager.java => PoolMonitor.java} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/{PoolManager.java => PoolMonitor.java} (98%) diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java index d847785a2..1ef17f7c4 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java @@ -106,7 +106,7 @@ public Controller(Channel componentChannel) { // to access the VM's console. Might change in the future. // attach(new ServiceMonitor(channel()).channelManager(chanMgr)); attach(new Reconciler(channel())); - attach(new PoolManager(channel())); + attach(new PoolMonitor(channel())); } /** diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PoolManager.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PoolMonitor.java similarity index 98% rename from org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PoolManager.java rename to org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PoolMonitor.java index 7bc455d7e..49c708dbc 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PoolManager.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PoolMonitor.java @@ -53,7 +53,7 @@ * avoid concurrent change informations. */ @SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports" }) -public class PoolManager extends +public class PoolMonitor extends AbstractMonitor { private final ReentrantLock pendingLock = new ReentrantLock(); @@ -67,7 +67,7 @@ public class PoolManager extends * @param componentChannel the component channel * @param channelManager the channel manager */ - public PoolManager(Channel componentChannel) { + public PoolMonitor(Channel componentChannel) { super(componentChannel, K8sDynamicModel.class, K8sDynamicModels.class); } From 84ac4bb28c4ab8d82f23042bd029c02afd369b5d Mon Sep 17 00:00:00 2001 From: "Michael N. Lipp" Date: Sun, 1 Dec 2024 16:43:35 +0100 Subject: [PATCH 05/35] Add hashCode and equals. --- .../vmoperator/common/VmDefinition.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinition.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinition.java index bab0802b3..754540566 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinition.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinition.java @@ -25,6 +25,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -350,4 +351,27 @@ public Optional displayPasswordSerial() { return this. fromStatus("displayPasswordSerial") .map(Number::longValue); } + + @Override + public int hashCode() { + return Objects.hash(metadata.getNamespace(), metadata.getName()); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + VmDefinition other = (VmDefinition) obj; + return Objects.equals(metadata.getNamespace(), + other.metadata.getNamespace()) + && Objects.equals(metadata.getName(), other.metadata.getName()); + } + } From db7fbe2b7c22d1c0ee285c1fbc30ea17a0ea828c Mon Sep 17 00:00:00 2001 From: "Michael N. Lipp" Date: Mon, 2 Dec 2024 12:18:41 +0100 Subject: [PATCH 06/35] Cleanup CR deletion. --- .../jdrupes/vmoperator/manager/VmMonitor.java | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmMonitor.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmMonitor.java index cc8ae7be0..315aee2f5 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmMonitor.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmMonitor.java @@ -119,11 +119,6 @@ protected void handleChange(K8sClient client, V1ObjectMeta metadata = response.object.getMetadata(); VmChannel channel = channelManager.channelGet(metadata.getName()); - // Remove from channel manager if deleted - if (ResponseType.valueOf(response.type) == ResponseType.DELETED) { - channelManager.remove(metadata.getName()); - } - // Get full definition and associate with channel as backup var vmModel = response.object; if (vmModel.data() == null) { @@ -151,17 +146,16 @@ protected void handleChange(K8sClient client, // Create and fire changed event. Remove channel from channel // manager on completion. - channel.pipeline() - .fire(Event.onCompletion( - new VmDefChanged(ResponseType.valueOf(response.type), - channel.setGeneration(response.object.getMetadata() - .getGeneration()), - vmDef), - e -> { - if (e.type() == ResponseType.DELETED) { - channelManager.remove(e.vmDefinition().name()); - } - }), channel); + VmDefChanged chgEvt + = new VmDefChanged(ResponseType.valueOf(response.type), + channel.setGeneration(response.object.getMetadata() + .getGeneration()), + vmDef); + if (ResponseType.valueOf(response.type) == ResponseType.DELETED) { + chgEvt = Event.onCompletion(chgEvt, + e -> channelManager.remove(e.vmDefinition().name())); + } + channel.pipeline().fire(chgEvt, channel); } private VmDefinitionModel getModel(K8sClient client, From 15ac0721a6449ab6ac052e67ce8859a782de6c25 Mon Sep 17 00:00:00 2001 From: "Michael N. Lipp" Date: Tue, 14 Jan 2025 10:22:56 +0100 Subject: [PATCH 07/35] Minor refactoring. --- .../jdrupes/vmoperator/common/VmDefinition.java | 15 +++++++++++++++ .../org/jdrupes/vmoperator/manager/VmMonitor.java | 15 +-------------- .../src/org/jdrupes/vmoperator/vmmgmt/VmMgmt.java | 13 +++---------- 3 files changed, 19 insertions(+), 24 deletions(-) diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinition.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinition.java index 754540566..b807e8cee 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinition.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinition.java @@ -268,6 +268,21 @@ public void setStatus(Map status) { this.status = status; } + /** + * Return a condition's status. + * + * @param name the condition's name + * @return the status, if the condition is defined + */ + public Optional conditionStatus(String name) { + return this.>> fromStatus("conditions") + .orElse(Collections.emptyList()).stream() + .filter(cond -> DataPath.get(cond, "type") + .map(name::equals).orElse(false)) + .findFirst().map(cond -> DataPath.get(cond, "status") + .map("True"::equals).orElse(false)); + } + /** * Set extra data (locally used, unknown to kubernetes). * diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmMonitor.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmMonitor.java index 315aee2f5..6c769d270 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmMonitor.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmMonitor.java @@ -24,9 +24,6 @@ import io.kubernetes.client.util.generic.options.ListOptions; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.logging.Level; @@ -49,7 +46,6 @@ import org.jdrupes.vmoperator.manager.events.ChannelManager; import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmDefChanged; -import org.jdrupes.vmoperator.util.DataPath; import org.jgrapes.core.Channel; import org.jgrapes.core.Event; @@ -184,16 +180,7 @@ private void addDynamicData(K8sClient client, VmDefinition vmDef, // VM definition status changes before the pod terminates. // This results in pod information being shown for a stopped // VM which is irritating. So check condition first. - @SuppressWarnings("PMD.LambdaCanBeMethodReference") - var isRunning - = vmDef.>> fromStatus("conditions") - .orElse(Collections.emptyList()).stream() - .filter(cond -> DataPath.get(cond, "type") - .map(t -> "Running".equals(t)).orElse(false)) - .findFirst().map(cond -> DataPath.get(cond, "status") - .map(s -> "True".equals(s)).orElse(false)) - .orElse(false); - if (!isRunning) { + if (!vmDef.conditionStatus("Running").orElse(false)) { return; } var podSearch = new ListOptions(); diff --git a/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/VmMgmt.java b/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/VmMgmt.java index 8395b4c54..dbe17ca48 100644 --- a/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/VmMgmt.java +++ b/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/VmMgmt.java @@ -29,9 +29,7 @@ import java.math.BigInteger; import java.time.Duration; import java.time.Instant; -import java.util.Collections; import java.util.EnumSet; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -328,14 +326,9 @@ private Summary evaluateSummary(boolean force) { .add(vmDef. fromStatus("ram") .map(r -> Quantity.fromString(r).getNumber().toBigInteger()) .orElse(BigInteger.ZERO)); - summary.runningVms - += vmDef.>> fromStatus("conditions") - .orElse(Collections.emptyList()).stream() - .filter(cond -> DataPath.get(cond, "type") - .map(t -> "Running".equals(t)).orElse(false) - && DataPath.get(cond, "status") - .map(s -> "True".equals(s)).orElse(false)) - .count(); + if (vmDef.conditionStatus("Running").orElse(false)) { + summary.runningVms += 1; + } } cachedSummary = summary; return summary; From 4943baf3e38bf9353ba75ef7e6cde6d878ad312c Mon Sep 17 00:00:00 2001 From: "Michael N. Lipp" Date: Tue, 14 Jan 2025 10:23:32 +0100 Subject: [PATCH 08/35] Save assignment information. --- deploy/crds/vms-crd.yaml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/deploy/crds/vms-crd.yaml b/deploy/crds/vms-crd.yaml index 5f67b4c05..1e79e27f5 100644 --- a/deploy/crds/vms-crd.yaml +++ b/deploy/crds/vms-crd.yaml @@ -1486,6 +1486,27 @@ spec: by the runner if password protection is not enabled. type: integer default: 0 + assignment: + description: >- + The assignment of this VM to a a particular user. + type: object + properties: + pool: + description: >- + The pool this VM is taken from. + type: string + default: "" + user: + description: >- + The user this VM is assigned to. + type: string + default: "" + lastUsed: + description: >- + The last time this VM was used by the user. + type: string + default: "1970-01-01T00:00:00Z" + default: {} conditions: description: >- List of component conditions observed From bd5227fda3e3981f81bb3bddad3a942544888497 Mon Sep 17 00:00:00 2001 From: "Michael N. Lipp" Date: Wed, 15 Jan 2025 21:58:08 +0100 Subject: [PATCH 09/35] Simplify pool management. --- .../org/jdrupes/vmoperator/common/VmPool.java | 28 ++++++ .../vmoperator/manager/PoolMonitor.java | 96 +++++-------------- 2 files changed, 53 insertions(+), 71 deletions(-) diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmPool.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmPool.java index 07e06161b..426a69cd8 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmPool.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmPool.java @@ -36,10 +36,20 @@ public class VmPool { private String name; + private boolean defined; private List permissions = Collections.emptyList(); private final Set vms = Collections.synchronizedSet(new HashSet<>()); + /** + * Instantiates a new vm pool. + * + * @param name the name + */ + public VmPool(String name) { + this.name = name; + } + /** * Returns the name. * @@ -58,6 +68,24 @@ public void setName(String name) { this.name = name; } + /** + * Checks if is defined. + * + * @return the result + */ + public boolean isDefined() { + return defined; + } + + /** + * Sets if is. + * + * @param defined the defined to set + */ + public void setDefined(boolean defined) { + this.defined = defined; + } + /** * Permissions granted for a VM from the pool. * diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PoolMonitor.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PoolMonitor.java index 49c708dbc..27dfb7d12 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PoolMonitor.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PoolMonitor.java @@ -19,17 +19,13 @@ package org.jdrupes.vmoperator.manager; import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.openapi.models.V1ObjectMeta; import io.kubernetes.client.util.Watch; import java.io.IOException; import java.util.Collections; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.locks.ReentrantLock; import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP; import org.jdrupes.vmoperator.common.K8s; import org.jdrupes.vmoperator.common.K8sClient; @@ -56,8 +52,6 @@ public class PoolMonitor extends AbstractMonitor { - private final ReentrantLock pendingLock = new ReentrantLock(); - private final Map> pending = new ConcurrentHashMap<>(); private final Map pools = new ConcurrentHashMap<>(); private EventPipeline poolPipeline; @@ -107,18 +101,13 @@ protected void handleChange(K8sClient client, // When pool is deleted, save VMs in pending if (type == ResponseType.DELETED) { - try { - pendingLock.lock(); - Optional.ofNullable(pools.get(poolName)).ifPresent( - p -> { - pending.computeIfAbsent(poolName, k -> Collections - .synchronizedSet(new HashSet<>())).addAll(p.vms()); - pools.remove(poolName); - poolPipeline.fire(new VmPoolChanged(p, true)); - }); - } finally { - pendingLock.unlock(); - } + Optional.ofNullable(pools.get(poolName)).ifPresent(pool -> { + pool.setDefined(false); + if (pool.vms().isEmpty()) { + pools.remove(poolName); + } + poolPipeline.fire(new VmPoolChanged(pool, true)); + }); return; } @@ -135,32 +124,13 @@ protected void handleChange(K8sClient client, } } - // Convert to VM pool - var vmPool = client().getJSON().getGson().fromJson( - GsonPtr.to(poolModel.data()).to("spec").get(), - VmPool.class); - V1ObjectMeta metadata = response.object.getMetadata(); - vmPool.setName(metadata.getName()); - - // If modified, merge changes and notify - if (type == ResponseType.MODIFIED && pools.containsKey(poolName)) { - pools.get(poolName).setPermissions(vmPool.permissions()); - poolPipeline.fire(new VmPoolChanged(vmPool)); - return; - } - - // Add new pool - try { - pendingLock.lock(); - Optional.ofNullable(pending.get(poolName)).ifPresent(s -> { - vmPool.vms().addAll(s); - }); - pending.remove(poolName); - pools.put(poolName, vmPool); - poolPipeline.fire(new VmPoolChanged(vmPool)); - } finally { - pendingLock.unlock(); - } + // Get pool and merge changes + var vmPool = pools.computeIfAbsent(poolName, k -> new VmPool(poolName)); + var newData = client().getJSON().getGson().fromJson( + GsonPtr.to(poolModel.data()).to("spec").get(), VmPool.class); + vmPool.setPermissions(newData.permissions()); + vmPool.setDefined(true); + poolPipeline.fire(new VmPoolChanged(vmPool)); } /** @@ -173,35 +143,19 @@ public void onVmDefChanged(VmDefChanged event) { String vmName = event.vmDefinition().name(); switch (event.type()) { case ADDED: - try { - pendingLock.lock(); - event.vmDefinition().> fromSpec("pools") - .orElse(Collections.emptyList()).stream().forEach(p -> { - if (pools.containsKey(p)) { - pools.get(p).vms().add(vmName); - } else { - pending.computeIfAbsent(p, k -> Collections - .synchronizedSet(new HashSet<>())).add(vmName); - } - poolPipeline.fire(new VmPoolChanged(pools.get(p))); - }); - } finally { - pendingLock.unlock(); - } + event.vmDefinition().> fromSpec("pools") + .orElse(Collections.emptyList()).stream().forEach(p -> { + pools.computeIfAbsent(p, k -> new VmPool(p)) + .vms().add(vmName); + poolPipeline.fire(new VmPoolChanged(pools.get(p))); + }); break; case DELETED: - try { - pendingLock.lock(); - pools.values().stream().forEach(p -> { - if (p.vms().remove(vmName)) { - poolPipeline.fire(new VmPoolChanged(p)); - } - }); - // Should not be necessary, but just in case - pending.values().stream().forEach(s -> s.remove(vmName)); - } finally { - pendingLock.unlock(); - } + pools.values().stream().forEach(p -> { + if (p.vms().remove(vmName)) { + poolPipeline.fire(new VmPoolChanged(p)); + } + }); break; default: break; From 5bd6700541c1a0722f313ff1a6320c61fc922636 Mon Sep 17 00:00:00 2001 From: "Michael N. Lipp" Date: Wed, 15 Jan 2025 22:06:00 +0100 Subject: [PATCH 10/35] Name not needed. --- .../org/jdrupes/vmoperator/vmaccess/VmAccess-edit.ftl.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/VmAccess-edit.ftl.html b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/VmAccess-edit.ftl.html index 1a10a7dd8..afbff3a99 100644 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/VmAccess-edit.ftl.html +++ b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/VmAccess-edit.ftl.html @@ -10,7 +10,7 @@