From 00adeba62521d263add55311885b5e6c70422e15 Mon Sep 17 00:00:00 2001 From: "Michael N. Lipp" Date: Tue, 19 Nov 2024 17:10:55 +0100 Subject: [PATCH] Add pool manager. --- .../jdrupes/vmoperator/common/Constants.java | 3 + .../org/jdrupes/vmoperator/common/VmPool.java | 164 +++++++++++++++ .../manager/events/VmPoolChanged.java | 88 ++++++++ org.jdrupes.vmoperator.manager/build.gradle | 1 + .../vmoperator/manager/Controller.java | 1 + .../vmoperator/manager/PoolManager.java | 191 ++++++++++++++++++ 6 files changed, 448 insertions(+) create mode 100644 org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmPool.java create mode 100644 org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmPoolChanged.java create mode 100644 org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PoolManager.java diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Constants.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Constants.java index 150bb3b..5837264 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Constants.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Constants.java @@ -38,4 +38,7 @@ public class Constants { /** The Constant VM_OP_KIND_VM. */ public static final String VM_OP_KIND_VM = "VirtualMachine"; + + /** The Constant VM_OP_KIND_VM_POOL. */ + public static final String VM_OP_KIND_VM_POOL = "VmPool"; } 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 new file mode 100644 index 0000000..aaa2746 --- /dev/null +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmPool.java @@ -0,0 +1,164 @@ +/* + * 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.common; + +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; + +/** + * Represents a VM pool. + */ +@SuppressWarnings({ "PMD.DataClass" }) +public class VmPool { + + private String name; + private List permissions = Collections.emptyList(); + private final Set vms + = Collections.synchronizedSet(new HashSet<>()); + + /** + * Returns the name. + * + * @return the name + */ + public String name() { + return name; + } + + /** + * Sets the name. + * + * @param name the name to set + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return the permissions + */ + public List permissions() { + return permissions; + } + + /** + * Sets the permissions. + * + * @param permissions the permissions to set + */ + public void setPermissions(List permissions) { + this.permissions = permissions; + } + + /** + * Returns the VM names. + * + * @return the vms + */ + public Set vms() { + return vms; + } + + @Override + @SuppressWarnings("PMD.AvoidLiteralsInIfCondition") + public String toString() { + StringBuilder builder = new StringBuilder(50); + builder.append("VmPool [name=").append(name).append(", permissions=") + .append(permissions).append(", vms="); + if (vms.size() <= 3) { + builder.append(vms); + } else { + builder.append('['); + vms.stream().limit(3).map(s -> s + ",").forEach(builder::append); + builder.append("...]"); + } + builder.append(']'); + return builder.toString(); + } + + /** + * 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.events/src/org/jdrupes/vmoperator/manager/events/VmPoolChanged.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmPoolChanged.java new file mode 100644 index 0000000..0c506a1 --- /dev/null +++ b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmPoolChanged.java @@ -0,0 +1,88 @@ +/* + * VM-Operator + * Copyright (C) 2023 Michael N. Lipp + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jdrupes.vmoperator.manager.events; + +import org.jdrupes.vmoperator.common.VmPool; +import org.jgrapes.core.Channel; +import org.jgrapes.core.Components; +import org.jgrapes.core.Event; + +/** + * Indicates a change in a pool configuration. + */ +@SuppressWarnings("PMD.DataClass") +public class VmPoolChanged extends Event { + + private final VmPool vmPool; + private final boolean deleted; + + /** + * Instantiates a new VM changed event. + * + * @param pool the pool + * @param deleted true, if the pool was deleted + */ + public VmPoolChanged(VmPool pool, boolean deleted) { + vmPool = pool; + this.deleted = deleted; + } + + /** + * Instantiates a new VM changed event for an existing pool. + * + * @param pool the pool + */ + public VmPoolChanged(VmPool pool) { + this(pool, false); + } + + /** + * Returns the VM pool. + * + * @return the vm pool + */ + public VmPool vmPool() { + return vmPool; + } + + /** + * Pool has been deleted. + * + * @return true, if successful + */ + public boolean deleted() { + return deleted; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(30); + builder.append(Components.objectName(this)) + .append(" ["); + if (deleted) { + builder.append("Deleted: "); + } + builder.append(vmPool); + if (channels() != null) { + builder.append(", channels=").append(Channel.toString(channels())); + } + builder.append(']'); + return builder.toString(); + } +} diff --git a/org.jdrupes.vmoperator.manager/build.gradle b/org.jdrupes.vmoperator.manager/build.gradle index e286f7b..d3d80cb 100644 --- a/org.jdrupes.vmoperator.manager/build.gradle +++ b/org.jdrupes.vmoperator.manager/build.gradle @@ -33,6 +33,7 @@ dependencies { runtimeOnly project(':org.jdrupes.vmoperator.vmconlet') runtimeOnly project(':org.jdrupes.vmoperator.vmviewer') + runtimeOnly project(':org.jdrupes.vmoperator.poolaccess') } application { 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 effc938..d847785 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,6 +106,7 @@ public class Controller extends Component { // 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())); } /** 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 new file mode 100644 index 0000000..f47c569 --- /dev/null +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PoolManager.java @@ -0,0 +1,191 @@ +/* + * VM-Operator + * Copyright (C) 2023,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; + +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; +import org.jdrupes.vmoperator.common.K8sDynamicModel; +import org.jdrupes.vmoperator.common.K8sDynamicModels; +import org.jdrupes.vmoperator.common.K8sDynamicStub; +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.VmDefChanged; +import org.jdrupes.vmoperator.manager.events.VmPoolChanged; +import org.jdrupes.vmoperator.util.GsonPtr; +import org.jgrapes.core.Channel; +import org.jgrapes.core.annotation.Handler; + +/** + * Watches for changes of VM pools. + */ +@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports" }) +public class PoolManager extends + AbstractMonitor { + + private final ReentrantLock pendingLock = new ReentrantLock(); + private final Map> pending = new ConcurrentHashMap<>(); + private final Map pools = new ConcurrentHashMap<>(); + + /** + * Instantiates a new VM pool manager. + * + * @param componentChannel the component channel + * @param channelManager the channel manager + */ + public PoolManager(Channel componentChannel) { + super(componentChannel, K8sDynamicModel.class, + K8sDynamicModels.class); + } + + @Override + protected void prepareMonitoring() throws IOException, ApiException { + client(new K8sClient()); + + // Get all our API versions + var ctx = K8s.context(client(), VM_OP_GROUP, "", VM_OP_KIND_VM_POOL); + if (ctx.isEmpty()) { + logger.severe(() -> "Cannot get CRD context."); + return; + } + context(ctx.get()); + } + + @Override + protected void handleChange(K8sClient client, + Watch.Response response) { + + var type = ResponseType.valueOf(response.type); + var poolName = response.object.metadata().getName(); + + // 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); + fire(new VmPoolChanged(p, true)); + }); + } finally { + pendingLock.unlock(); + } + return; + } + + // Get full definition + var poolModel = response.object; + if (poolModel.data() == null) { + // ADDED event does not provide data, see + // https://github.com/kubernetes-client/java/issues/3215 + try { + poolModel = K8sDynamicStub.get(client(), context(), namespace(), + poolModel.metadata().getName()).model().orElse(null); + } catch (ApiException e) { + return; + } + } + + // 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 + if (type == ResponseType.MODIFIED && pools.containsKey(poolName)) { + pools.get(poolName).setPermissions(vmPool.permissions()); + 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); + fire(new VmPoolChanged(vmPool)); + } finally { + pendingLock.unlock(); + } + } + + /** + * Track VM definition changes. + * + * @param event the event + */ + @Handler + 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); + } + fire(new VmPoolChanged(pools.get(p))); + }); + } finally { + pendingLock.unlock(); + } + break; + case DELETED: + try { + pendingLock.lock(); + pools.values().stream().forEach(p -> { + if (p.vms().remove(vmName)) { + fire(new VmPoolChanged(p)); + } + }); + // Should not be necessary, but just in case + pending.values().stream().forEach(s -> s.remove(vmName)); + } finally { + pendingLock.unlock(); + } + break; + default: + break; + } + } +}