diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetPools.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetPools.java new file mode 100644 index 000000000..12575995e --- /dev/null +++ b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetPools.java @@ -0,0 +1,67 @@ +/* + * VM-Operator + * Copyright (C) 2024 Michael N. Lipp + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jdrupes.vmoperator.manager.events; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import org.jdrupes.vmoperator.common.VmPool; +import org.jgrapes.core.Event; + +/** + * Gets the known pools' definitions. + */ +@SuppressWarnings("PMD.DataClass") +public class GetPools extends Event> { + + private String user; + private List roles = Collections.emptyList(); + + /** + * Return only {@link VmPool}s that are accessible by + * the given user or roles. + * + * @param user the user + * @param roles the roles + * @return the event + */ + public GetPools accessibleFor(String user, List roles) { + this.user = user; + this.roles = roles; + return this; + } + + /** + * Returns the user filter criterion, if set. + * + * @return the optional + */ + public Optional forUser() { + return Optional.ofNullable(user); + } + + /** + * Returns the roles criterion. + * + * @return the list + */ + public List forRoles() { + return roles; + } +} diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetVms.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetVms.java new file mode 100644 index 000000000..ebb19cca7 --- /dev/null +++ b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetVms.java @@ -0,0 +1,97 @@ +/* + * VM-Operator + * Copyright (C) 2024 Michael N. Lipp + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jdrupes.vmoperator.manager.events; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import org.jdrupes.vmoperator.common.VmDefinition; +import org.jgrapes.core.Event; + +/** + * Gets the known VMs' definitions and channels. + */ +@SuppressWarnings("PMD.DataClass") +public class GetVms extends Event> { + + private String name; + private String user; + private List roles = Collections.emptyList(); + + /** + * Return only the VMs with the given name. + * + * @param name the name + * @return the returns the vms + */ + public GetVms withName(String name) { + this.name = name; + return this; + } + + /** + * Return only {@link VmDefinition}s that are accessible by + * the given user or roles. + * + * @param user the user + * @param roles the roles + * @return the event + */ + public GetVms accessibleFor(String user, List roles) { + this.user = user; + this.roles = roles; + return this; + } + + /** + * Returns the name filter criterion, if set. + * + * @return the optional + */ + public Optional name() { + return Optional.ofNullable(name); + } + + /** + * Returns the user filter criterion, if set. + * + * @return the optional + */ + public Optional user() { + return Optional.ofNullable(user); + } + + /** + * Returns the roles criterion. + * + * @return the list + */ + public List roles() { + return roles; + } + + /** + * Return tuple. + * + * @param definition the definition + * @param channel the channel + */ + public record VmData(VmDefinition definition, VmChannel channel) { + } +} 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 27dfb7d12..1864ea7df 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 @@ -35,6 +35,7 @@ import org.jdrupes.vmoperator.common.K8sObserver.ResponseType; import org.jdrupes.vmoperator.common.VmPool; import static org.jdrupes.vmoperator.manager.Constants.VM_OP_KIND_VM_POOL; +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.util.GsonPtr; @@ -161,4 +162,18 @@ public void onVmDefChanged(VmDefChanged event) { break; } } + + /** + * Return the requested pools. + * + * @param event the event + */ + @Handler + public void onGetPools(GetPools event) { + event.setResult(pools.values().stream() + .filter(p -> event.forUser().isEmpty() && event.forRoles().isEmpty() + || !p.permissionsFor(event.forUser().orElse(null), + event.forRoles()).isEmpty()) + .toList()); + } } 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 6c769d270..b5268a2c5 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 @@ -44,10 +44,13 @@ import static org.jdrupes.vmoperator.manager.Constants.VM_OP_KIND_VM; import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME; import org.jdrupes.vmoperator.manager.events.ChannelManager; +import org.jdrupes.vmoperator.manager.events.GetVms; +import org.jdrupes.vmoperator.manager.events.GetVms.VmData; import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmDefChanged; import org.jgrapes.core.Channel; import org.jgrapes.core.Event; +import org.jgrapes.core.annotation.Handler; /** * Watches for changes of VM definitions. @@ -208,4 +211,21 @@ private void addDynamicData(K8sClient client, VmDefinition vmDef, () -> "Cannot access node information: " + e.getMessage()); } } + + /** + * Returns the VM data. + * + * @param event the event + */ + @Handler + public void onGetVms(GetVms event) { + event.setResult(channelManager.channels().stream() + .filter(c -> event.name().isEmpty() + || c.vmDefinition().name().equals(event.name().get())) + .filter(c -> event.user().isEmpty() && event.roles().isEmpty() + || !c.vmDefinition().permissionsFor(event.user().orElse(null), + event.roles()).isEmpty()) + .map(c -> new VmData(c.vmDefinition(), c)) + .toList()); + } } 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 4f4ef5312..8d1a71adb 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 @@ -52,8 +52,10 @@ 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.GetPools; +import org.jdrupes.vmoperator.manager.events.GetVms; +import org.jdrupes.vmoperator.manager.events.GetVms.VmData; import org.jdrupes.vmoperator.manager.events.ModifyVm; import org.jdrupes.vmoperator.manager.events.ResetVm; import org.jdrupes.vmoperator.manager.events.VmChannel; @@ -62,8 +64,10 @@ import org.jgrapes.core.Channel; import org.jgrapes.core.Components; import org.jgrapes.core.Event; +import org.jgrapes.core.EventPipeline; import org.jgrapes.core.Manager; import org.jgrapes.core.annotation.Handler; +import org.jgrapes.core.events.Start; import org.jgrapes.http.Session; import org.jgrapes.util.events.ConfigurationUpdate; import org.jgrapes.util.events.KeyValueStoreQuery; @@ -122,8 +126,7 @@ public class VmAccess extends FreeMarkerConlet { RenderMode.Preview, RenderMode.Edit); private static final Set MODES_FOR_GENERATED = RenderMode.asSet( RenderMode.Preview, RenderMode.StickyPreview); - private final ChannelTracker channelTracker = new ChannelTracker<>(); + private EventPipeline appPipeline; private static ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule()); private Class preferredIpVersion = Inet4Address.class; @@ -150,6 +153,16 @@ public VmAccess(Channel componentChannel) { super(componentChannel); } + /** + * On start. + * + * @param event the event + */ + @Handler + public void onStart(Start event) { + appPipeline = event.processedBy().get(); + } + /** * Configure the component. * @@ -263,18 +276,24 @@ public void onConsoleConfigured(ConsoleConfigured event, if (!syncPreviews(connection.session())) { return; } - - addMissingVms(event, connection, rendered); + addMissingConlets(event, connection, rendered); } @SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops", "PMD.AvoidDuplicateLiterals" }) - private void addMissingVms(ConsoleConfigured event, - ConsoleConnection connection, final Set rendered) { + private void addMissingConlets(ConsoleConfigured event, + ConsoleConnection connection, final Set rendered) + throws InterruptedException { boolean foundMissing = false; - for (var vmName : accessibleVms(connection)) { + var session = connection.session(); + for (var vmName : appPipeline.fire(new GetVms().accessibleFor( + WebConsoleUtils.userFromSession(session) + .map(ConsoleUser::getName).orElse(null), + WebConsoleUtils.rolesFromSession(session).stream() + .map(ConsoleRole::getName).toList())) + .get().stream().map(d -> d.definition().name()).toList()) { if (rendered.stream() - .anyMatch(r -> r.type() == ResourceModel.Type.VM + .anyMatch(r -> r.mode() == ResourceModel.Mode.VM && r.name().equals(vmName))) { continue; } @@ -318,7 +337,7 @@ private String storagePath(Session session, String conletId) { protected Optional createNewState(AddConletRequest event, ConsoleConnection connection, String conletId) throws Exception { var model = new ResourceModel(conletId); - model.setType(ResourceModel.Type.VM); + model.setMode(ResourceModel.Mode.VM); model .setName((String) event.properties().get(VM_NAME_PROPERTY)); String jsonState = objectMapper.writeValueAsString(model); @@ -373,11 +392,25 @@ protected Set doRenderConlet(RenderConletRequestBase event, 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 session = channel.session(); + var vmNames = appPipeline.fire(new GetVms().accessibleFor( + WebConsoleUtils.userFromSession(session) + .map(ConsoleUser::getName).orElse(null), + WebConsoleUtils.rolesFromSession(session).stream() + .map(ConsoleRole::getName).toList())) + .get().stream().map(d -> d.definition().name()).sorted() + .toList(); + var poolNames = appPipeline.fire(new GetPools().accessibleFor( + WebConsoleUtils.userFromSession(session) + .map(ConsoleUser::getName).orElse(null), + WebConsoleUtils.rolesFromSession(session).stream() + .map(ConsoleRole::getName).toList())) + .get().stream().map(VmPool::name).sorted().toList(); + 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)); + fmModel.put("vmNames", vmNames); + fmModel.put("poolNames", poolNames); channel.respond(new OpenModalDialog(type(), conletId, processTemplate(event, tpl, fmModel)) .addOption("cancelable", true) @@ -391,27 +424,32 @@ protected Set doRenderConlet(RenderConletRequestBase event, private Set renderPreview(RenderConletRequestBase event, ConsoleConnection channel, String conletId, ResourceModel model) throws TemplateNotFoundException, MalformedTemplateNameException, - ParseException, IOException { + ParseException, IOException, InterruptedException { channel.associated(PENDING, Event.class) .ifPresent(e -> { e.resumeHandling(); channel.setAssociated(PENDING, null); }); - if (model.type() == ResourceModel.Type.VM && model.name() != null) { + var session = channel.session(); + if (model.mode() == ResourceModel.Mode.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()) { + Optional vmData = appPipeline.fire(new GetVms() + .withName(model.name()).accessibleFor( + WebConsoleUtils.userFromSession(session) + .map(ConsoleUser::getName).orElse(null), + WebConsoleUtils.rolesFromSession(session).stream() + .map(ConsoleRole::getName).toList())) + .get().stream().findFirst(); + if (vmData.isEmpty()) { channel.respond( new DeleteConlet(conletId, Collections.emptySet())); return Collections.emptySet(); } } - if (model.type() == ResourceModel.Type.POOL && model.name() != null) { + if (model.mode() == ResourceModel.Mode.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()); @@ -442,12 +480,6 @@ private Set renderPreview(RenderConletRequestBase event, return EnumSet.of(RenderMode.Preview); } - private List accessibleVms(ConsoleConnection channel) { - return channelTracker.associated().stream() - .filter(d -> !vmPermissions(d, channel.session()).isEmpty()) - .map(d -> d.getMetadata().getName()).sorted().toList(); - } - private Set vmPermissions(VmDefinition vmDef, Session session) { var user = WebConsoleUtils.userFromSession(session) @@ -457,12 +489,6 @@ private Set vmPermissions(VmDefinition vmDef, return vmDef.permissionsFor(user, roles); } - 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) @@ -472,34 +498,36 @@ private Set poolPermissions(VmPool pool, return pool.permissionsFor(user, roles); } - private void updateConfig(ConsoleConnection channel, ResourceModel model) { + private void updateConfig(ConsoleConnection channel, ResourceModel model) + throws InterruptedException { channel.respond(new NotifyConletView(type(), - model.getConletId(), "updateConfig", model.type(), model.name())); + model.getConletId(), "updateConfig", model.mode(), model.name())); updateVmDef(channel, model); } - private void updateVmDef(ConsoleConnection channel, ResourceModel model) { + private void updateVmDef(ConsoleConnection channel, ResourceModel model) + throws InterruptedException { if (Strings.isNullOrEmpty(model.name())) { return; } - channelTracker.value(model.name()).ifPresent(item -> { - try { - var vmDef = item.associated(); - var data = Map.of("metadata", - Map.of("namespace", vmDef.namespace(), - "name", vmDef.name()), - "spec", vmDef.spec(), - "status", vmDef.getStatus(), - "userPermissions", - vmPermissions(vmDef, channel.session()).stream() - .map(VmDefinition.Permission::toString).toList()); - channel.respond(new NotifyConletView(type(), - model.getConletId(), "updateVmDefinition", data)); - } catch (JsonSyntaxException e) { - logger.log(Level.SEVERE, e, - () -> "Failed to serialize VM definition"); - } - }); + appPipeline.fire(new GetVms().withName(model.name())).get().stream() + .findFirst().map(d -> d.definition()).ifPresent(vmDef -> { + try { + var data = Map.of("metadata", + Map.of("namespace", vmDef.namespace(), + "name", vmDef.name()), + "spec", vmDef.spec(), + "status", vmDef.getStatus(), + "userPermissions", + vmPermissions(vmDef, channel.session()).stream() + .map(VmDefinition.Permission::toString).toList()); + channel.respond(new NotifyConletView(type(), + model.getConletId(), "updateVmDefinition", data)); + } catch (JsonSyntaxException e) { + logger.log(Level.SEVERE, e, + () -> "Failed to serialize VM definition"); + } + }); } @Override @@ -519,20 +547,15 @@ protected void doConletDeleted(ConletDeleted event, * @param event the event * @param channel the channel * @throws IOException + * @throws InterruptedException */ @Handler(namedChannels = "manager") @SuppressWarnings({ "PMD.ConfusingTernary", "PMD.CognitiveComplexity", "PMD.AvoidInstantiatingObjectsInLoops", "PMD.AvoidDuplicateLiterals", "PMD.ConfusingArgumentToVarargsMethod" }) public void onVmDefChanged(VmDefChanged event, VmChannel channel) - throws IOException { + throws IOException, InterruptedException { var vmDef = event.vmDefinition(); - var vmName = vmDef.name(); - if (event.type() == K8sObserver.ResponseType.DELETED) { - channelTracker.remove(vmName); - } else { - channelTracker.put(vmName, channel, vmDef); - } // Update known conlets for (var entry : conletIdsByConsoleConnection().entrySet()) { @@ -540,8 +563,8 @@ public void onVmDefChanged(VmDefChanged event, VmChannel channel) 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)) { + || model.get().mode() != ResourceModel.Mode.VM + || !Objects.areEqual(model.get().name(), vmDef.name())) { continue; } if (event.type() == K8sObserver.ResponseType.DELETED @@ -577,7 +600,7 @@ public void onVmPoolChanged(VmPoolChanged event) { for (var conletId : entry.getValue()) { var model = stateFromSession(connection.session(), conletId); if (model.isEmpty() - || model.get().type() != ResourceModel.Type.POOL + || model.get().mode() != ResourceModel.Mode.POOL || !Objects.areEqual(model.get().name(), poolName)) { continue; } @@ -605,13 +628,13 @@ protected void doUpdateConletState(NotifyConletModel event, } // Handle command for selected VM - var both = Optional.ofNullable(model.name()) - .flatMap(vm -> channelTracker.value(vm)); - if (both.isEmpty()) { + var vmData = appPipeline.fire(new GetVms().withName(model.name())).get() + .stream().findFirst(); + if (vmData.isEmpty()) { return; } - var vmChannel = both.get().channel(); - var vmDef = both.get().associated(); + var vmChannel = vmData.get().channel(); + var vmDef = vmData.get().definition(); var vmName = vmDef.metadata().getName(); var perms = vmPermissions(vmDef, channel.session()); var resourceBundle = resourceBundle(channel.locale()); @@ -656,9 +679,9 @@ protected void doUpdateConletState(NotifyConletModel event, "PMD.UseLocaleWithCaseConversions" }) private void selectResource(NotifyConletModel event, ConsoleConnection channel, ResourceModel model) - throws JsonProcessingException { + throws JsonProcessingException, InterruptedException { try { - model.setType(ResourceModel.Type + model.setMode(ResourceModel.Mode .valueOf(event. param(0).toUpperCase())); model.setName(event.param(1)); String jsonState = objectMapper.writeValueAsString(model); @@ -672,7 +695,13 @@ private void selectResource(NotifyConletModel event, private void openConsole(String vmName, ConsoleConnection connection, ResourceModel model, String password) { - var vmDef = channelTracker.associated(vmName).orElse(null); + VmDefinition vmDef; + try { + vmDef = appPipeline.fire(new GetVms().withName(model.name())).get() + .stream().findFirst().map(VmData::definition).orElse(null); + } catch (InterruptedException e) { + return; + } if (vmDef == null) { return; } @@ -771,11 +800,11 @@ public static class ResourceModel extends ConletBaseModel { * The Enum ResourceType. */ @SuppressWarnings("PMD.ShortVariable") - public enum Type { + public enum Mode { VM, POOL } - private Type type; + private Mode mode; private String name; /** @@ -807,27 +836,29 @@ public void setName(String name) { } /** + * Returns the mode. + * * @return the resourceType */ - @JsonGetter("type") - public Type type() { - return type; + @JsonGetter("mode") + public Mode mode() { + return mode; } /** - * Sets the type. + * Sets the mode. * - * @param type the resource type to set + * @param mode the resource mode to set */ - public void setType(Type type) { - this.type = type; + public void setMode(Mode mode) { + this.mode = mode; } @Override public int hashCode() { final int prime = 31; int result = super.hashCode(); - result = prime * result + java.util.Objects.hash(name, type); + result = prime * result + java.util.Objects.hash(name, mode); return result; } @@ -844,14 +875,14 @@ public boolean equals(Object obj) { } ResourceModel other = (ResourceModel) obj; return java.util.Objects.equals(name, other.name) - && type == other.type; + && mode == other.mode; } @Override public String toString() { StringBuilder builder = new StringBuilder(50); - builder.append("AccessModel [resourceType=").append(type) - .append(", resourceName=").append(name).append(']'); + builder.append("AccessModel [mode=").append(mode) + .append(", name=").append(name).append(']'); return builder.toString(); }