Merge branch 'release/v3.4' into release/v3.x
This commit is contained in:
commit
7c2a214261
35 changed files with 1036 additions and 659 deletions
|
|
@ -1,9 +1,7 @@
|
||||||
|
#
|
||||||
|
#Wed Oct 02 14:48:43 CEST 2024
|
||||||
eclipse.preferences.version=1
|
eclipse.preferences.version=1
|
||||||
org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull
|
|
||||||
org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable
|
|
||||||
org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
|
|
||||||
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
|
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
|
||||||
org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
|
|
||||||
org.eclipse.jdt.core.compiler.codegen.targetPlatform=21
|
org.eclipse.jdt.core.compiler.codegen.targetPlatform=21
|
||||||
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
|
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
|
||||||
org.eclipse.jdt.core.compiler.compliance=21
|
org.eclipse.jdt.core.compiler.compliance=21
|
||||||
|
|
@ -11,12 +9,5 @@ org.eclipse.jdt.core.compiler.debug.lineNumber=generate
|
||||||
org.eclipse.jdt.core.compiler.debug.localVariable=generate
|
org.eclipse.jdt.core.compiler.debug.localVariable=generate
|
||||||
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
|
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
|
||||||
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
|
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
|
||||||
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
|
|
||||||
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
|
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
|
||||||
org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
|
|
||||||
org.eclipse.jdt.core.compiler.problem.nullReference=warning
|
|
||||||
org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
|
|
||||||
org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore
|
|
||||||
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
|
|
||||||
org.eclipse.jdt.core.compiler.release=disabled
|
|
||||||
org.eclipse.jdt.core.compiler.source=21
|
org.eclipse.jdt.core.compiler.source=21
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
eclipse.preferences.version=1
|
eclipse.preferences.version=1
|
||||||
groovy.compiler.level=40
|
groovy.compiler.level=-1
|
||||||
groovy.script.filters=**/*.dsld,y,**/*.gradle,n
|
groovy.script.filters=**/*.dsld,y,**/*.gradle,n
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,3 @@
|
||||||
/*
|
|
||||||
* This file was generated by the Gradle 'init' task.
|
|
||||||
*
|
|
||||||
* This project uses @Incubating APIs which are subject to change.
|
|
||||||
*/
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
// Support convention plugins written in Groovy. Convention plugins
|
// Support convention plugins written in Groovy. Convention plugins
|
||||||
// are build scripts in 'src/main' that automatically become available
|
// are build scripts in 'src/main' that automatically become available
|
||||||
|
|
@ -14,52 +8,24 @@ plugins {
|
||||||
id 'eclipse'
|
id 'eclipse'
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
|
||||||
// Use the plugin portal to apply community plugins in convention plugins.
|
|
||||||
gradlePluginPortal()
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
main {
|
main {
|
||||||
groovy {
|
groovy {
|
||||||
srcDirs = ['src']
|
srcDirs = ['src']
|
||||||
}
|
}
|
||||||
}
|
resources {
|
||||||
|
srcDirs = ['resources']
|
||||||
test {
|
}
|
||||||
groovy {
|
}
|
||||||
srcDirs = ['test']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
eclipse {
|
eclipse {
|
||||||
|
|
||||||
project {
|
|
||||||
file {
|
|
||||||
// closure executed after .project content is loaded from existing file
|
|
||||||
// and before gradle build information is merged
|
|
||||||
beforeMerged { project ->
|
|
||||||
project.natures.clear()
|
|
||||||
project.buildCommands.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
project.natures += 'org.eclipse.buildship.core.gradleprojectnature'
|
|
||||||
// Don't build, result not used by Eclipse anyway
|
|
||||||
// project.buildCommand 'org.eclipse.buildship.core.gradleprojectbuilder'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
classpath {
|
|
||||||
downloadJavadoc = true
|
|
||||||
downloadSources = true
|
|
||||||
}
|
|
||||||
|
|
||||||
jdt {
|
jdt {
|
||||||
file {
|
file {
|
||||||
withProperties { properties ->
|
withProperties { properties ->
|
||||||
def formatterPrefs = new Properties()
|
def formatterPrefs = new Properties()
|
||||||
rootProject.file("gradle/org.eclipse.jdt.core.formatter.prefs")
|
rootProject.file("../gradle/org.eclipse.jdt.core.formatter.prefs")
|
||||||
.withInputStream { formatterPrefs.load(it) }
|
.withInputStream { formatterPrefs.load(it) }
|
||||||
properties.putAll(formatterPrefs)
|
properties.putAll(formatterPrefs)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
/*
|
|
||||||
* This file was generated by the Gradle 'init' task.
|
|
||||||
*
|
|
||||||
* This settings file is used to specify which projects to include in your build-logic build.
|
|
||||||
*/
|
|
||||||
|
|
||||||
rootProject.name = 'buildSrc'
|
|
||||||
|
|
@ -20,7 +20,7 @@ spec:
|
||||||
containers:
|
containers:
|
||||||
- name: vm-operator
|
- name: vm-operator
|
||||||
image: >-
|
image: >-
|
||||||
ghcr.io/mnlipp/org.jdrupes.vmoperator.manager:3.3.0
|
ghcr.io/mnlipp/org.jdrupes.vmoperator.manager:3.4.0
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: config
|
- name: config
|
||||||
mountPath: /etc/opt/vmoperator
|
mountPath: /etc/opt/vmoperator
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,11 @@ rules:
|
||||||
- apiGroups:
|
- apiGroups:
|
||||||
- ""
|
- ""
|
||||||
resources:
|
resources:
|
||||||
|
- persistentvolumeclaims
|
||||||
- pods
|
- pods
|
||||||
verbs:
|
verbs:
|
||||||
- list
|
- list
|
||||||
- get
|
- get
|
||||||
|
- create
|
||||||
- delete
|
- delete
|
||||||
- patch
|
- patch
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ plugins {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api project(':org.jdrupes.vmoperator.util')
|
api project(':org.jdrupes.vmoperator.util')
|
||||||
|
api 'org.jgrapes:org.jgrapes.core:[1.22.1,2)'
|
||||||
api 'io.kubernetes:client-java:[19.0.0,20.0.0)'
|
api 'io.kubernetes:client-java:[19.0.0,20.0.0)'
|
||||||
api 'org.yaml:snakeyaml'
|
api 'org.yaml:snakeyaml'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import java.time.Instant;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
import org.jgrapes.core.Components;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An observer that watches namespaced resources in a given context and
|
* An observer that watches namespaced resources in a given context and
|
||||||
|
|
@ -73,7 +74,7 @@ public class K8sObserver<O extends KubernetesObject,
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
|
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
|
||||||
"PMD.UseObjectForClearerAPI", "PMD.AvoidCatchingThrowable",
|
"PMD.UseObjectForClearerAPI", "PMD.AvoidCatchingThrowable",
|
||||||
"PMD.CognitiveComplexity" })
|
"PMD.CognitiveComplexity", "PMD.AvoidCatchingGenericException" })
|
||||||
public K8sObserver(Class<O> objectClass, Class<L> objectListClass,
|
public K8sObserver(Class<O> objectClass, Class<L> objectListClass,
|
||||||
K8sClient client, APIResource context, String namespace,
|
K8sClient client, APIResource context, String namespace,
|
||||||
ListOptions options) {
|
ListOptions options) {
|
||||||
|
|
@ -85,38 +86,41 @@ public class K8sObserver<O extends KubernetesObject,
|
||||||
api = new GenericKubernetesApi<>(objectClass, objectListClass,
|
api = new GenericKubernetesApi<>(objectClass, objectListClass,
|
||||||
context.getGroup(), context.getPreferredVersion(),
|
context.getGroup(), context.getPreferredVersion(),
|
||||||
context.getResourcePlural(), client);
|
context.getResourcePlural(), client);
|
||||||
thread = Thread.ofVirtual().unstarted(() -> {
|
thread = (Components.useVirtualThreads() ? Thread.ofVirtual()
|
||||||
try {
|
: Thread.ofPlatform()).unstarted(() -> {
|
||||||
logger.config(() -> "Watching " + context.getResourcePlural()
|
try {
|
||||||
+ " (" + context.getPreferredVersion() + ")"
|
logger
|
||||||
+ " in " + namespace);
|
.config(() -> "Watching " + context.getResourcePlural()
|
||||||
|
+ " (" + context.getPreferredVersion() + ")"
|
||||||
|
+ " in " + namespace);
|
||||||
|
|
||||||
// Watch sometimes terminates without apparent reason.
|
// Watch sometimes terminates without apparent reason.
|
||||||
while (!Thread.currentThread().isInterrupted()) {
|
while (!Thread.currentThread().isInterrupted()) {
|
||||||
Instant startedAt = Instant.now();
|
Instant startedAt = Instant.now();
|
||||||
try {
|
try {
|
||||||
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
|
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
|
||||||
var changed = api.watch(namespace, options).iterator();
|
var changed
|
||||||
while (changed.hasNext()) {
|
= api.watch(namespace, options).iterator();
|
||||||
handler.accept(client, changed.next());
|
while (changed.hasNext()) {
|
||||||
|
handler.accept(client, changed.next());
|
||||||
|
}
|
||||||
|
} catch (ApiException | RuntimeException e) {
|
||||||
|
logger.log(Level.FINE, e, () -> "Problem watching"
|
||||||
|
+ " (will retry): " + e.getMessage());
|
||||||
|
delayRestart(startedAt);
|
||||||
}
|
}
|
||||||
} catch (ApiException | RuntimeException e) {
|
}
|
||||||
logger.log(Level.FINE, e, () -> "Problem watching"
|
if (onTerminated != null) {
|
||||||
+ " (will retry): " + e.getMessage());
|
onTerminated.accept(this, null);
|
||||||
delayRestart(startedAt);
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
logger.log(Level.SEVERE, e, () -> "Probem watching: "
|
||||||
|
+ e.getMessage());
|
||||||
|
if (onTerminated != null) {
|
||||||
|
onTerminated.accept(this, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (onTerminated != null) {
|
});
|
||||||
onTerminated.accept(this, null);
|
|
||||||
}
|
|
||||||
} catch (Throwable e) {
|
|
||||||
logger.log(Level.SEVERE, e, () -> "Probem watching: "
|
|
||||||
+ e.getMessage());
|
|
||||||
if (onTerminated != null) {
|
|
||||||
onTerminated.accept(this, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
|
@SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
* 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.jdrupes.vmoperator.common;
|
||||||
|
|
||||||
|
import io.kubernetes.client.Discovery.APIResource;
|
||||||
|
import io.kubernetes.client.openapi.ApiException;
|
||||||
|
import io.kubernetes.client.openapi.models.V1PersistentVolumeClaim;
|
||||||
|
import io.kubernetes.client.openapi.models.V1PersistentVolumeClaimList;
|
||||||
|
import io.kubernetes.client.util.generic.options.ListOptions;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A stub for pods (v1).
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
|
||||||
|
public class K8sV1PvcStub extends
|
||||||
|
K8sGenericStub<V1PersistentVolumeClaim, V1PersistentVolumeClaimList> {
|
||||||
|
|
||||||
|
/** The pods' context. */
|
||||||
|
public static final APIResource CONTEXT
|
||||||
|
= new APIResource("", List.of("v1"), "v1", "PersistentVolumeClaim",
|
||||||
|
true, "persistentvolumeclaims", "persistentvolumeclaim");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new stub.
|
||||||
|
*
|
||||||
|
* @param client the client
|
||||||
|
* @param namespace the namespace
|
||||||
|
* @param name the name
|
||||||
|
*/
|
||||||
|
protected K8sV1PvcStub(K8sClient client, String namespace, String name) {
|
||||||
|
super(V1PersistentVolumeClaim.class, V1PersistentVolumeClaimList.class,
|
||||||
|
client, CONTEXT, namespace, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the stub for the given namespace and name.
|
||||||
|
*
|
||||||
|
* @param client the client
|
||||||
|
* @param namespace the namespace
|
||||||
|
* @param name the name
|
||||||
|
* @return the kpod stub
|
||||||
|
*/
|
||||||
|
public static K8sV1PvcStub get(K8sClient client, String namespace,
|
||||||
|
String name) {
|
||||||
|
return new K8sV1PvcStub(client, namespace, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the stubs for the objects in the given namespace that match
|
||||||
|
* the criteria from the given options.
|
||||||
|
*
|
||||||
|
* @param client the client
|
||||||
|
* @param namespace the namespace
|
||||||
|
* @param options the options
|
||||||
|
* @return the collection
|
||||||
|
* @throws ApiException the api exception
|
||||||
|
*/
|
||||||
|
public static Collection<K8sV1PvcStub> list(K8sClient client,
|
||||||
|
String namespace, ListOptions options) throws ApiException {
|
||||||
|
return K8sGenericStub.list(V1PersistentVolumeClaim.class,
|
||||||
|
V1PersistentVolumeClaimList.class, client, CONTEXT, namespace,
|
||||||
|
options, (clnt, nscp, name) -> new K8sV1PvcStub(clnt, nscp, name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -37,6 +37,13 @@ import org.jdrupes.vmoperator.util.GsonPtr;
|
||||||
@SuppressWarnings("PMD.DataClass")
|
@SuppressWarnings("PMD.DataClass")
|
||||||
public class VmDefinitionModel extends K8sDynamicModel {
|
public class VmDefinitionModel extends K8sDynamicModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The VM state from the VM definition.
|
||||||
|
*/
|
||||||
|
public enum RequestedVmState {
|
||||||
|
STOPPED, RUNNING
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Permissions for accessing and manipulating the VM.
|
* Permissions for accessing and manipulating the VM.
|
||||||
*/
|
*/
|
||||||
|
|
@ -111,6 +118,18 @@ public class VmDefinitionModel extends K8sDynamicModel {
|
||||||
.flatMap(Function.identity()).collect(Collectors.toSet());
|
.flatMap(Function.identity()).collect(Collectors.toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the requested VM state
|
||||||
|
*
|
||||||
|
* @return the string
|
||||||
|
*/
|
||||||
|
public RequestedVmState vmState() {
|
||||||
|
return GsonPtr.to(data()).getAsString("spec", "vm", "state")
|
||||||
|
.map(s -> "Running".equals(s) ? RequestedVmState.RUNNING
|
||||||
|
: RequestedVmState.STOPPED)
|
||||||
|
.orElse(RequestedVmState.STOPPED);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the display password serial.
|
* Get the display password serial.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api 'org.jgrapes:org.jgrapes.core:[1.21.0,2)'
|
|
||||||
api project(':org.jdrupes.vmoperator.common')
|
api project(':org.jdrupes.vmoperator.common')
|
||||||
api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:[2.16.1,3]'
|
api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:[2.16.1,3]'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
/*
|
||||||
|
* 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.jdrupes.vmoperator.manager.events;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.jgrapes.core.Channel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supports the lookup of a channel by a name (an id). As a convenience,
|
||||||
|
* it is possible to additionally associate arbitrary data with the entry
|
||||||
|
* (and thus with the channel). Note that this interface defines a
|
||||||
|
* read-only view of the dictionary.
|
||||||
|
*
|
||||||
|
* @param <K> the key type
|
||||||
|
* @param <C> the channel type
|
||||||
|
* @param <A> the type of the associated data
|
||||||
|
*/
|
||||||
|
public interface ChannelDictionary<K, C extends Channel, A> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combines the channel and the associated data.
|
||||||
|
*
|
||||||
|
* @param <C> the channel type
|
||||||
|
* @param <A> the type of the associated data
|
||||||
|
* @param channel the channel
|
||||||
|
* @param associated the associated
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("PMD.ShortClassName")
|
||||||
|
public record Value<C extends Channel, A>(C channel, A associated) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all known keys.
|
||||||
|
*
|
||||||
|
* @return the keys
|
||||||
|
*/
|
||||||
|
Set<K> keys();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all known values.
|
||||||
|
*
|
||||||
|
* @return the collection
|
||||||
|
*/
|
||||||
|
Collection<Value<C, A>> values();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the channel and associates data registered for the key
|
||||||
|
* or an empty optional if no entry exists.
|
||||||
|
*
|
||||||
|
* @param key the key
|
||||||
|
* @return the result
|
||||||
|
*/
|
||||||
|
Optional<Value<C, A>> value(K key);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all known channels.
|
||||||
|
*
|
||||||
|
* @return the collection
|
||||||
|
*/
|
||||||
|
default Collection<C> channels() {
|
||||||
|
return values().stream().map(v -> v.channel).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the channel registered for the key or an empty optional
|
||||||
|
* if no mapping exists.
|
||||||
|
*
|
||||||
|
* @param key the key
|
||||||
|
* @return the optional
|
||||||
|
*/
|
||||||
|
default Optional<C> channel(K key) {
|
||||||
|
return value(key).map(b -> b.channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all known associated data.
|
||||||
|
*
|
||||||
|
* @return the collection
|
||||||
|
*/
|
||||||
|
default Collection<A> associated() {
|
||||||
|
return values().stream()
|
||||||
|
.filter(v -> v.associated() != null)
|
||||||
|
.map(v -> v.associated).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the data associated with the entry for the channel.
|
||||||
|
*
|
||||||
|
* @param key the key
|
||||||
|
* @return the data
|
||||||
|
*/
|
||||||
|
default Optional<A> associated(K key) {
|
||||||
|
return value(key).map(b -> b.associated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,53 +27,24 @@ import java.util.function.Function;
|
||||||
import org.jgrapes.core.Channel;
|
import org.jgrapes.core.Channel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A channel manager that maintains mappings from a key to a channel.
|
* Provides an actively managed implementation of the {@link ChannelDictionary}.
|
||||||
* As a convenience, it is possible to additionally associate arbitrary
|
|
||||||
* data with the entry (and thus with the channel).
|
|
||||||
*
|
*
|
||||||
* The manager should be used by a component that defines channels for
|
* The {@link ChannelManager} can be used for housekeeping by any component
|
||||||
* housekeeping. It can be shared between this component and another
|
* that creates channels. It can be shared between this component and
|
||||||
* component, preferably using the {@link #fixed()} view for the
|
* some other component, preferably passing it as {@link ChannelDictionary}
|
||||||
* second component. Alternatively, the second component can use a
|
* (the read-only view) to the second component. Alternatively, the other
|
||||||
* {@link ChannelCache} to track the mappings using events.
|
* component can use a {@link ChannelTracker} to track the mappings using
|
||||||
|
* events.
|
||||||
*
|
*
|
||||||
* @param <K> the key type
|
* @param <K> the key type
|
||||||
* @param <C> the channel type
|
* @param <C> the channel type
|
||||||
* @param <A> the type of the associated data
|
* @param <A> the type of the associated data
|
||||||
*/
|
*/
|
||||||
public class ChannelManager<K, C extends Channel, A> {
|
public class ChannelManager<K, C extends Channel, A>
|
||||||
|
implements ChannelDictionary<K, C, A> {
|
||||||
|
|
||||||
private final Map<K, Both<C, A>> channels = new ConcurrentHashMap<>();
|
private final Map<K, Value<C, A>> entries = new ConcurrentHashMap<>();
|
||||||
private final Function<K, C> supplier;
|
private final Function<K, C> supplier;
|
||||||
private ChannelManager<K, C, A> readOnly;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Combines the channel and the associated data.
|
|
||||||
*
|
|
||||||
* @param <C> the generic type
|
|
||||||
* @param <A> the generic type
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("PMD.ShortClassName")
|
|
||||||
public static class Both<C extends Channel, A> {
|
|
||||||
|
|
||||||
/** The channel. */
|
|
||||||
public C channel;
|
|
||||||
|
|
||||||
/** The associated. */
|
|
||||||
public A associated;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instantiates a new both.
|
|
||||||
*
|
|
||||||
* @param channel the channel
|
|
||||||
* @param associated the associated
|
|
||||||
*/
|
|
||||||
public Both(C channel, A associated) {
|
|
||||||
super();
|
|
||||||
this.channel = channel;
|
|
||||||
this.associated = associated;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiates a new channel manager.
|
* Instantiates a new channel manager.
|
||||||
|
|
@ -91,6 +62,21 @@ public class ChannelManager<K, C extends Channel, A> {
|
||||||
this(k -> null);
|
this(k -> null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<K> keys() {
|
||||||
|
return entries.keySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all known values.
|
||||||
|
*
|
||||||
|
* @return the collection
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Collection<Value<C, A>> values() {
|
||||||
|
return entries.values();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the channel and associates data registered for the key
|
* Returns the channel and associates data registered for the key
|
||||||
* or an empty optional if no mapping exists.
|
* or an empty optional if no mapping exists.
|
||||||
|
|
@ -98,10 +84,8 @@ public class ChannelManager<K, C extends Channel, A> {
|
||||||
* @param key the key
|
* @param key the key
|
||||||
* @return the result
|
* @return the result
|
||||||
*/
|
*/
|
||||||
public Optional<Both<C, A>> both(K key) {
|
public Optional<Value<C, A>> value(K key) {
|
||||||
synchronized (channels) {
|
return Optional.ofNullable(entries.get(key));
|
||||||
return Optional.ofNullable(channels.get(key));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -113,7 +97,7 @@ public class ChannelManager<K, C extends Channel, A> {
|
||||||
* @return the channel manager
|
* @return the channel manager
|
||||||
*/
|
*/
|
||||||
public ChannelManager<K, C, A> put(K key, C channel, A associated) {
|
public ChannelManager<K, C, A> put(K key, C channel, A associated) {
|
||||||
channels.put(key, new Both<>(channel, associated));
|
entries.put(key, new Value<>(channel, associated));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -129,17 +113,6 @@ public class ChannelManager<K, C extends Channel, A> {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the channel registered for the key or an empty optional
|
|
||||||
* if no mapping exists.
|
|
||||||
*
|
|
||||||
* @param key the key
|
|
||||||
* @return the optional
|
|
||||||
*/
|
|
||||||
public Optional<C> channel(K key) {
|
|
||||||
return both(key).map(b -> b.channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the {@link Channel} for the given name, creating it using
|
* Returns the {@link Channel} for the given name, creating it using
|
||||||
* the supplier passed to the constructor if it doesn't exist yet.
|
* the supplier passed to the constructor if it doesn't exist yet.
|
||||||
|
|
@ -147,8 +120,8 @@ public class ChannelManager<K, C extends Channel, A> {
|
||||||
* @param key the key
|
* @param key the key
|
||||||
* @return the channel
|
* @return the channel
|
||||||
*/
|
*/
|
||||||
public Optional<C> getChannel(K key) {
|
public C channelGet(K key) {
|
||||||
return getChannel(key, supplier);
|
return computeIfAbsent(key, supplier);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -161,17 +134,9 @@ public class ChannelManager<K, C extends Channel, A> {
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings({ "PMD.AssignmentInOperand",
|
@SuppressWarnings({ "PMD.AssignmentInOperand",
|
||||||
"PMD.DataflowAnomalyAnalysis" })
|
"PMD.DataflowAnomalyAnalysis" })
|
||||||
public Optional<C> getChannel(K key, Function<K, C> supplier) {
|
public C computeIfAbsent(K key, Function<K, C> supplier) {
|
||||||
synchronized (channels) {
|
return entries.computeIfAbsent(key,
|
||||||
return Optional
|
k -> new Value<>(supplier.apply(k), null)).channel();
|
||||||
.of(Optional.ofNullable(channels.get(key))
|
|
||||||
.map(v -> v.channel)
|
|
||||||
.orElseGet(() -> {
|
|
||||||
var channel = supplier.apply(key);
|
|
||||||
channels.put(key, new Both<>(channel, null));
|
|
||||||
return channel;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -183,121 +148,17 @@ public class ChannelManager<K, C extends Channel, A> {
|
||||||
* @return the channel manager
|
* @return the channel manager
|
||||||
*/
|
*/
|
||||||
public ChannelManager<K, C, A> associate(K key, A data) {
|
public ChannelManager<K, C, A> associate(K key, A data) {
|
||||||
synchronized (channels) {
|
Optional.ofNullable(entries.computeIfPresent(key,
|
||||||
Optional.ofNullable(channels.get(key))
|
(k, existing) -> new Value<>(existing.channel(), data)));
|
||||||
.ifPresent(v -> v.associated = data);
|
|
||||||
}
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the data associated with the entry for the channel.
|
|
||||||
*
|
|
||||||
* @param key the key
|
|
||||||
* @return the data
|
|
||||||
*/
|
|
||||||
public Optional<A> associated(K key) {
|
|
||||||
return both(key).map(b -> b.associated);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns all associated data.
|
|
||||||
*
|
|
||||||
* @return the collection
|
|
||||||
*/
|
|
||||||
public Collection<A> associated() {
|
|
||||||
synchronized (channels) {
|
|
||||||
return channels.values().stream()
|
|
||||||
.filter(v -> v.associated != null)
|
|
||||||
.map(v -> v.associated).toList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the channel with the given name.
|
* Removes the channel with the given name.
|
||||||
*
|
*
|
||||||
* @param name the name
|
* @param name the name
|
||||||
*/
|
*/
|
||||||
public void remove(String name) {
|
public void remove(String name) {
|
||||||
synchronized (channels) {
|
entries.remove(name);
|
||||||
channels.remove(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns all known keys.
|
|
||||||
*
|
|
||||||
* @return the sets the
|
|
||||||
*/
|
|
||||||
public Set<K> keys() {
|
|
||||||
return channels.keySet();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a read only view of this channel manager. The methods
|
|
||||||
* that usually create a new entry refrain from doing so. The
|
|
||||||
* methods that change the value of channel and {@link #remove(String)}
|
|
||||||
* do nothing. The associated data, however, can still be changed.
|
|
||||||
*
|
|
||||||
* @return the channel manager
|
|
||||||
*/
|
|
||||||
public ChannelManager<K, C, A> fixed() {
|
|
||||||
if (readOnly == null) {
|
|
||||||
readOnly = new ChannelManager<>(supplier) {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<Both<C, A>> both(K key) {
|
|
||||||
return ChannelManager.this.both(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ChannelManager<K, C, A> put(K key, C channel,
|
|
||||||
A associated) {
|
|
||||||
return associate(key, associated);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<C> getChannel(K key) {
|
|
||||||
return ChannelManager.this.channel(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<C> getChannel(K key, Function<K, C> supplier) {
|
|
||||||
return ChannelManager.this.channel(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ChannelManager<K, C, A> associate(K key, A data) {
|
|
||||||
return ChannelManager.this.associate(key, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<A> associated(K key) {
|
|
||||||
return ChannelManager.this.associated(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Collection<A> associated() {
|
|
||||||
return ChannelManager.this.associated();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void remove(String name) {
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Set<K> keys() {
|
|
||||||
return ChannelManager.this.keys();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ChannelManager<K, C, A> fixed() {
|
|
||||||
return ChannelManager.this.fixed();
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return readOnly;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
package org.jdrupes.vmoperator.manager.events;
|
package org.jdrupes.vmoperator.manager.events;
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
@ -27,20 +28,30 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||||
import org.jgrapes.core.Channel;
|
import org.jgrapes.core.Channel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A channel manager that tracks mappings from a key to a channel using
|
* Used to track mapping from a key to a channel. Entries must
|
||||||
* "add/remove" (or "open/close") events and the channels on which they
|
* be maintained by handlers for "add/remove" (or "open/close")
|
||||||
* are delivered.
|
* events delivered on the channels that are to be
|
||||||
|
* made available by the tracker.
|
||||||
|
*
|
||||||
|
* The channels are stored in the dictionary using {@link WeakReference}s.
|
||||||
|
* Removing entries is therefore best practice but not an absolute necessity
|
||||||
|
* as entries for cleared references are removed when one of the methods
|
||||||
|
* {@link #values()}, {@link #channels()} or {@link #associated()} is called.
|
||||||
*
|
*
|
||||||
* @param <K> the key type
|
* @param <K> the key type
|
||||||
* @param <C> the channel type
|
* @param <C> the channel type
|
||||||
* @param <A> the type of the associated data
|
* @param <A> the type of the associated data
|
||||||
*/
|
*/
|
||||||
public class ChannelCache<K, C extends Channel, A> {
|
public class ChannelTracker<K, C extends Channel, A>
|
||||||
|
implements ChannelDictionary<K, C, A> {
|
||||||
|
|
||||||
private final Map<K, Data<C, A>> channels = new ConcurrentHashMap<>();
|
private final Map<K, Data<C, A>> entries = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper
|
* Combines the channel and associated data.
|
||||||
|
*
|
||||||
|
* @param <C> the generic type
|
||||||
|
* @param <A> the generic type
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("PMD.ShortClassName")
|
@SuppressWarnings("PMD.ShortClassName")
|
||||||
private static class Data<C extends Channel, A> {
|
private static class Data<C extends Channel, A> {
|
||||||
|
|
@ -57,32 +68,24 @@ public class ChannelCache<K, C extends Channel, A> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* Combines the channel and the associated data.
|
public Set<K> keys() {
|
||||||
*
|
return entries.keySet();
|
||||||
* @param <C> the generic type
|
}
|
||||||
* @param <A> the generic type
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("PMD.ShortClassName")
|
|
||||||
public static class Both<C extends Channel, A> {
|
|
||||||
|
|
||||||
/** The channel. */
|
@Override
|
||||||
public C channel;
|
public Collection<Value<C, A>> values() {
|
||||||
|
var result = new ArrayList<Value<C, A>>();
|
||||||
/** The associated. */
|
for (var itr = entries.entrySet().iterator(); itr.hasNext();) {
|
||||||
public A associated;
|
var value = itr.next().getValue();
|
||||||
|
var channel = value.channel.get();
|
||||||
/**
|
if (channel == null) {
|
||||||
* Instantiates a new both.
|
itr.remove();
|
||||||
*
|
continue;
|
||||||
* @param channel the channel
|
}
|
||||||
* @param associated the associated
|
result.add(new Value<>(channel, value.associated));
|
||||||
*/
|
|
||||||
public Both(C channel, A associated) {
|
|
||||||
super();
|
|
||||||
this.channel = channel;
|
|
||||||
this.associated = associated;
|
|
||||||
}
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -92,20 +95,18 @@ public class ChannelCache<K, C extends Channel, A> {
|
||||||
* @param key the key
|
* @param key the key
|
||||||
* @return the result
|
* @return the result
|
||||||
*/
|
*/
|
||||||
public Optional<Both<C, A>> both(K key) {
|
public Optional<Value<C, A>> value(K key) {
|
||||||
synchronized (channels) {
|
var value = entries.get(key);
|
||||||
var value = channels.get(key);
|
if (value == null) {
|
||||||
if (value == null) {
|
return Optional.empty();
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
var channel = value.channel.get();
|
|
||||||
if (channel == null) {
|
|
||||||
// Cleanup old reference
|
|
||||||
channels.remove(key);
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
return Optional.of(new Both<>(channel, value.associated));
|
|
||||||
}
|
}
|
||||||
|
var channel = value.channel.get();
|
||||||
|
if (channel == null) {
|
||||||
|
// Cleanup old reference
|
||||||
|
entries.remove(key);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return Optional.of(new Value<>(channel, value.associated));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -116,10 +117,10 @@ public class ChannelCache<K, C extends Channel, A> {
|
||||||
* @param associated the associated
|
* @param associated the associated
|
||||||
* @return the channel manager
|
* @return the channel manager
|
||||||
*/
|
*/
|
||||||
public ChannelCache<K, C, A> put(K key, C channel, A associated) {
|
public ChannelTracker<K, C, A> put(K key, C channel, A associated) {
|
||||||
Data<C, A> data = new Data<>(channel);
|
Data<C, A> data = new Data<>(channel);
|
||||||
data.associated = associated;
|
data.associated = associated;
|
||||||
channels.put(key, data);
|
entries.put(key, data);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,22 +131,11 @@ public class ChannelCache<K, C extends Channel, A> {
|
||||||
* @param channel the channel
|
* @param channel the channel
|
||||||
* @return the channel manager
|
* @return the channel manager
|
||||||
*/
|
*/
|
||||||
public ChannelCache<K, C, A> put(K key, C channel) {
|
public ChannelTracker<K, C, A> put(K key, C channel) {
|
||||||
put(key, channel, null);
|
put(key, channel, null);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the channel registered for the key or an empty optional
|
|
||||||
* if no mapping exists.
|
|
||||||
*
|
|
||||||
* @param key the key
|
|
||||||
* @return the optional
|
|
||||||
*/
|
|
||||||
public Optional<C> channel(K key) {
|
|
||||||
return both(key).map(b -> b.channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Associate the entry for the channel with the given data. The entry
|
* Associate the entry for the channel with the given data. The entry
|
||||||
* for the channel must already exist.
|
* for the channel must already exist.
|
||||||
|
|
@ -154,54 +144,18 @@ public class ChannelCache<K, C extends Channel, A> {
|
||||||
* @param data the data
|
* @param data the data
|
||||||
* @return the channel manager
|
* @return the channel manager
|
||||||
*/
|
*/
|
||||||
public ChannelCache<K, C, A> associate(K key, A data) {
|
public ChannelTracker<K, C, A> associate(K key, A data) {
|
||||||
synchronized (channels) {
|
Optional.ofNullable(entries.get(key))
|
||||||
Optional.ofNullable(channels.get(key))
|
.ifPresent(v -> v.associated = data);
|
||||||
.ifPresent(v -> v.associated = data);
|
|
||||||
}
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the data associated with the entry for the channel.
|
|
||||||
*
|
|
||||||
* @param key the key
|
|
||||||
* @return the data
|
|
||||||
*/
|
|
||||||
public Optional<A> associated(K key) {
|
|
||||||
return both(key).map(b -> b.associated);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns all associated data.
|
|
||||||
*
|
|
||||||
* @return the collection
|
|
||||||
*/
|
|
||||||
public Collection<A> associated() {
|
|
||||||
synchronized (channels) {
|
|
||||||
return channels.values().stream()
|
|
||||||
.filter(v -> v.channel.get() != null && v.associated != null)
|
|
||||||
.map(v -> v.associated).toList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the channel with the given name.
|
* Removes the channel with the given name.
|
||||||
*
|
*
|
||||||
* @param name the name
|
* @param name the name
|
||||||
*/
|
*/
|
||||||
public void remove(String name) {
|
public void remove(String name) {
|
||||||
synchronized (channels) {
|
entries.remove(name);
|
||||||
channels.remove(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns all known keys.
|
|
||||||
*
|
|
||||||
* @return the sets the
|
|
||||||
*/
|
|
||||||
public Set<K> keys() {
|
|
||||||
return channels.keySet();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.jdrupes.vmoperator.manager.events;
|
|
||||||
|
|
||||||
import io.kubernetes.client.openapi.models.V1Service;
|
|
||||||
import org.jdrupes.vmoperator.common.K8sObserver.ResponseType;
|
|
||||||
import org.jgrapes.core.Channel;
|
|
||||||
import org.jgrapes.core.Components;
|
|
||||||
import org.jgrapes.core.Event;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates that a service has changed.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("PMD.DataClass")
|
|
||||||
public class ServiceChanged extends Event<Void> {
|
|
||||||
|
|
||||||
private final ResponseType type;
|
|
||||||
private final V1Service service;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes a new service changed event.
|
|
||||||
*
|
|
||||||
* @param type the type
|
|
||||||
* @param service the service
|
|
||||||
*/
|
|
||||||
public ServiceChanged(ResponseType type, V1Service service) {
|
|
||||||
this.type = type;
|
|
||||||
this.service = service;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the type.
|
|
||||||
*
|
|
||||||
* @return the type
|
|
||||||
*/
|
|
||||||
public ResponseType type() {
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the service.
|
|
||||||
*
|
|
||||||
* @return the service
|
|
||||||
*/
|
|
||||||
public V1Service service() {
|
|
||||||
return service;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
StringBuilder builder = new StringBuilder();
|
|
||||||
builder.append(Components.objectName(this)).append(" [")
|
|
||||||
.append(service.getMetadata().getName()).append(' ').append(type);
|
|
||||||
if (channels() != null) {
|
|
||||||
builder.append(", channels=").append(Channel.toString(channels()));
|
|
||||||
}
|
|
||||||
builder.append(']');
|
|
||||||
return builder.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -13,8 +13,8 @@ dependencies {
|
||||||
|
|
||||||
implementation 'commons-cli:commons-cli:1.5.0'
|
implementation 'commons-cli:commons-cli:1.5.0'
|
||||||
|
|
||||||
implementation 'org.jgrapes:org.jgrapes.util:[1.36.0,2)'
|
implementation 'org.jgrapes:org.jgrapes.util:[1.38.1,2)'
|
||||||
implementation 'org.jgrapes:org.jgrapes.io:[2.11.0,3)'
|
implementation 'org.jgrapes:org.jgrapes.io:[2.12.1,3)'
|
||||||
implementation 'org.jgrapes:org.jgrapes.http:[3.5.0,4)'
|
implementation 'org.jgrapes:org.jgrapes.http:[3.5.0,4)'
|
||||||
|
|
||||||
implementation 'org.jgrapes:org.jgrapes.webconsole.base:[1.8.0,2)'
|
implementation 'org.jgrapes:org.jgrapes.webconsole.base:[1.8.0,2)'
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
namespace: ${ cr.metadata.namespace.asString }
|
||||||
|
name: ${ runnerDataPvcName }
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: ${ constants.APP_NAME }
|
||||||
|
app.kubernetes.io/instance: ${ cr.metadata.name.asString }
|
||||||
|
app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME }
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
<#if reconciler.runnerDataPvc?? && reconciler.runnerDataPvc.storageClassName??>
|
||||||
|
storageClassName: ${ reconciler.runnerDataPvc.storageClassName }
|
||||||
|
</#if>
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 1Mi
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
namespace: ${ cr.metadata.namespace.asString }
|
||||||
|
name: ${ disk.generatedPvcName }
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: ${ constants.APP_NAME }
|
||||||
|
app.kubernetes.io/instance: ${ cr.metadata.name.asString }
|
||||||
|
app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME }
|
||||||
|
<#if disk.volumeClaimTemplate.metadata??
|
||||||
|
&& disk.volumeClaimTemplate.metadata.annotations??>
|
||||||
|
annotations:
|
||||||
|
${ disk.volumeClaimTemplate.metadata.annotations.toString() }
|
||||||
|
</#if>
|
||||||
|
spec:
|
||||||
|
${ disk.volumeClaimTemplate.spec.toString() }
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
kind: Pod
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
namespace: ${ cr.metadata.namespace.asString }
|
||||||
|
name: ${ cr.metadata.name.asString }
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: ${ constants.APP_NAME }
|
||||||
|
app.kubernetes.io/instance: ${ cr.metadata.name.asString }
|
||||||
|
app.kubernetes.io/component: ${ constants.APP_NAME }
|
||||||
|
app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME }
|
||||||
|
annotations:
|
||||||
|
# Triggers update of config map mounted in pod
|
||||||
|
# See https://ahmet.im/blog/kubernetes-secret-volumes-delay/
|
||||||
|
vmrunner.jdrupes.org/cmVersion: "${ cm.metadata.resourceVersion.asString }"
|
||||||
|
vmoperator.jdrupes.org/version: ${ managerVersion }
|
||||||
|
ownerReferences:
|
||||||
|
- apiVersion: ${ cr.apiVersion.asString }
|
||||||
|
kind: ${ constants.VM_OP_KIND_VM }
|
||||||
|
name: ${ cr.metadata.name.asString }
|
||||||
|
uid: ${ cr.metadata.uid.asString }
|
||||||
|
blockOwnerDeletion: true
|
||||||
|
controller: false
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: ${ cr.metadata.name.asString }
|
||||||
|
<#assign image = cr.spec.image>
|
||||||
|
<#if image.source??>
|
||||||
|
image: ${ image.source.asString }
|
||||||
|
<#else>
|
||||||
|
image: ${ image.repository.asString }/${ image.path.asString }<#if image.version??>:${ image.version.asString }</#if>
|
||||||
|
</#if>
|
||||||
|
<#if image.pullPolicy??>
|
||||||
|
imagePullPolicy: ${ image.pullPolicy.asString }
|
||||||
|
</#if>
|
||||||
|
<#if cr.spec.vm.display.spice??>
|
||||||
|
ports:
|
||||||
|
<#if cr.spec.vm.display.spice??>
|
||||||
|
- name: spice
|
||||||
|
containerPort: ${ cr.spec.vm.display.spice.port.asInt?c }
|
||||||
|
protocol: TCP
|
||||||
|
</#if>
|
||||||
|
</#if>
|
||||||
|
volumeMounts:
|
||||||
|
# Not needed because pod is priviledged:
|
||||||
|
# - mountPath: /dev/kvm
|
||||||
|
# name: dev-kvm
|
||||||
|
# - mountPath: /dev/net/tun
|
||||||
|
# name: dev-tun
|
||||||
|
# - mountPath: /sys/fs/cgroup
|
||||||
|
# name: cgroup
|
||||||
|
- name: config
|
||||||
|
mountPath: /etc/opt/vmrunner
|
||||||
|
- name: runner-data
|
||||||
|
mountPath: /var/local/vm-data
|
||||||
|
- name: vmop-image-repository
|
||||||
|
mountPath: ${ constants.IMAGE_REPO_PATH }
|
||||||
|
volumeDevices:
|
||||||
|
<#list cr.spec.vm.disks.asList() as disk>
|
||||||
|
<#if disk.volumeClaimTemplate??>
|
||||||
|
- name: ${ disk.generatedDiskName.asString }
|
||||||
|
devicePath: /dev/${ disk.generatedDiskName.asString }
|
||||||
|
</#if>
|
||||||
|
</#list>
|
||||||
|
securityContext:
|
||||||
|
privileged: true
|
||||||
|
<#if cr.spec.resources??>
|
||||||
|
resources: ${ cr.spec.resources.toString() }
|
||||||
|
<#else>
|
||||||
|
<#if cr.spec.vm.currentCpus?? || cr.spec.vm.currentRam?? >
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
<#if cr.spec.vm.currentCpus?? >
|
||||||
|
<#assign factor = 2.0 />
|
||||||
|
<#if reconciler.cpuOvercommit??>
|
||||||
|
<#assign factor = reconciler.cpuOvercommit * 1.0 />
|
||||||
|
</#if>
|
||||||
|
cpu: ${ (parseQuantity(cr.spec.vm.currentCpus.asString) / factor)?c }
|
||||||
|
</#if>
|
||||||
|
<#if cr.spec.vm.currentRam?? >
|
||||||
|
<#assign factor = 1.25 />
|
||||||
|
<#if reconciler.ramOvercommit??>
|
||||||
|
<#assign factor = reconciler.ramOvercommit * 1.0 />
|
||||||
|
</#if>
|
||||||
|
memory: ${ (parseQuantity(cr.spec.vm.currentRam.asString) / factor)?floor?c }
|
||||||
|
</#if>
|
||||||
|
</#if>
|
||||||
|
</#if>
|
||||||
|
volumes:
|
||||||
|
# Not needed because pod is priviledged:
|
||||||
|
# - name: dev-kvm
|
||||||
|
# hostPath:
|
||||||
|
# path: /dev/kvm
|
||||||
|
# type: CharDevice
|
||||||
|
# - hostPath:
|
||||||
|
# path: /dev/net/tun
|
||||||
|
# type: CharDevice
|
||||||
|
# name: dev-tun
|
||||||
|
# - name: cgroup
|
||||||
|
# hostPath:
|
||||||
|
# path: /sys/fs/cgroup
|
||||||
|
- name: config
|
||||||
|
projected:
|
||||||
|
sources:
|
||||||
|
- configMap:
|
||||||
|
name: ${ cr.metadata.name.asString }
|
||||||
|
<#if displaySecret??>
|
||||||
|
- secret:
|
||||||
|
name: ${ displaySecret }
|
||||||
|
</#if>
|
||||||
|
- name: vmop-image-repository
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: vmop-image-repository
|
||||||
|
- name: runner-data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: ${ runnerDataPvcName }
|
||||||
|
<#list cr.spec.vm.disks.asList() as disk>
|
||||||
|
<#if disk.volumeClaimTemplate??>
|
||||||
|
- name: ${ disk.generatedDiskName.asString }
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: ${ disk.generatedPvcName.asString }
|
||||||
|
</#if>
|
||||||
|
</#list>
|
||||||
|
hostNetwork: true
|
||||||
|
terminationGracePeriodSeconds: ${ (cr.spec.vm.powerdownTimeout.asInt + 5)?c }
|
||||||
|
<#if cr.spec.nodeName??>
|
||||||
|
nodeName: ${ cr.spec.nodeName.asString }
|
||||||
|
</#if>
|
||||||
|
<#if cr.spec.nodeSelector??>
|
||||||
|
nodeSelector: ${ cr.spec.nodeSelector.toString() }
|
||||||
|
</#if>
|
||||||
|
<#if cr.spec.affinity??>
|
||||||
|
affinity: ${ cr.spec.affinity.toString() }
|
||||||
|
</#if>
|
||||||
|
serviceAccountName: vm-runner
|
||||||
|
|
@ -27,14 +27,11 @@ import io.kubernetes.client.util.generic.options.ListOptions;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import org.jdrupes.vmoperator.common.K8s;
|
import org.jdrupes.vmoperator.common.K8s;
|
||||||
import org.jdrupes.vmoperator.common.K8sClient;
|
import org.jdrupes.vmoperator.common.K8sClient;
|
||||||
import org.jdrupes.vmoperator.common.K8sObserver;
|
import org.jdrupes.vmoperator.common.K8sObserver;
|
||||||
import org.jdrupes.vmoperator.common.K8sObserver.ResponseType;
|
|
||||||
import org.jdrupes.vmoperator.manager.events.ChannelManager;
|
|
||||||
import org.jdrupes.vmoperator.manager.events.Exit;
|
import org.jdrupes.vmoperator.manager.events.Exit;
|
||||||
import org.jgrapes.core.Channel;
|
import org.jgrapes.core.Channel;
|
||||||
import org.jgrapes.core.Component;
|
import org.jgrapes.core.Component;
|
||||||
|
|
@ -45,7 +42,11 @@ import org.jgrapes.core.events.Stop;
|
||||||
import org.jgrapes.util.events.ConfigurationUpdate;
|
import org.jgrapes.util.events.ConfigurationUpdate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A base class for monitoring VM related resources.
|
* A base class for monitoring VM related resources. When started,
|
||||||
|
* it creates observers for all versions of the the {@link APIResource}
|
||||||
|
* configured by {@link #context(APIResource)}. The APIResource is not
|
||||||
|
* passed to the constructor because in some cases it has to be
|
||||||
|
* evaluated lazily.
|
||||||
*
|
*
|
||||||
* @param <O> the object type for the context
|
* @param <O> the object type for the context
|
||||||
* @param <L> the object list type for the context
|
* @param <L> the object list type for the context
|
||||||
|
|
@ -61,16 +62,17 @@ public abstract class AbstractMonitor<O extends KubernetesObject,
|
||||||
private String namespace;
|
private String namespace;
|
||||||
private ListOptions options = new ListOptions();
|
private ListOptions options = new ListOptions();
|
||||||
private final AtomicInteger observerCounter = new AtomicInteger(0);
|
private final AtomicInteger observerCounter = new AtomicInteger(0);
|
||||||
private ChannelManager<String, C, ?> channelManager;
|
|
||||||
private boolean channelManagerMaster;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the instance.
|
* Initializes the instance.
|
||||||
*
|
*
|
||||||
* @param componentChannel the component channel
|
* @param componentChannel the component channel
|
||||||
|
* @param objectClass the class of the Kubernetes object to watch
|
||||||
|
* @param objectListClass the class of the list of Kubernetes objects
|
||||||
|
* to watch
|
||||||
*/
|
*/
|
||||||
protected AbstractMonitor(Channel componentChannel, Class<O> objectClass,
|
protected AbstractMonitor(Channel componentChannel,
|
||||||
Class<L> objectListClass) {
|
Class<O> objectClass, Class<L> objectListClass) {
|
||||||
super(componentChannel);
|
super(componentChannel);
|
||||||
this.objectClass = objectClass;
|
this.objectClass = objectClass;
|
||||||
this.objectListClass = objectListClass;
|
this.objectListClass = objectListClass;
|
||||||
|
|
@ -156,27 +158,6 @@ public abstract class AbstractMonitor<O extends KubernetesObject,
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the channel manager.
|
|
||||||
*
|
|
||||||
* @return the context
|
|
||||||
*/
|
|
||||||
public ChannelManager<String, C, ?> channelManager() {
|
|
||||||
return channelManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the channel manager.
|
|
||||||
*
|
|
||||||
* @param channelManager the channel manager
|
|
||||||
* @return the abstract monitor
|
|
||||||
*/
|
|
||||||
public AbstractMonitor<O, L, C>
|
|
||||||
channelManager(ChannelManager<String, C, ?> channelManager) {
|
|
||||||
this.channelManager = channelManager;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Looks for a key "namespace" in the configuration and, if found,
|
* Looks for a key "namespace" in the configuration and, if found,
|
||||||
* sets the namespace to its value.
|
* sets the namespace to its value.
|
||||||
|
|
@ -194,7 +175,7 @@ public abstract class AbstractMonitor<O extends KubernetesObject,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle the start event. Configures the namespace invokes
|
* Handle the start event. Configures the namespace, invokes
|
||||||
* {@link #prepareMonitoring()} and starts the observers.
|
* {@link #prepareMonitoring()} and starts the observers.
|
||||||
*
|
*
|
||||||
* @param event the event
|
* @param event the event
|
||||||
|
|
@ -240,10 +221,6 @@ public abstract class AbstractMonitor<O extends KubernetesObject,
|
||||||
K8s.preferred(context, version), namespace, options)
|
K8s.preferred(context, version), namespace, options)
|
||||||
.handler((c, r) -> {
|
.handler((c, r) -> {
|
||||||
handleChange(c, r);
|
handleChange(c, r);
|
||||||
if (ResponseType.valueOf(r.type) == ResponseType.DELETED
|
|
||||||
&& channelManagerMaster) {
|
|
||||||
channelManager.remove(r.object.getMetadata().getName());
|
|
||||||
}
|
|
||||||
}).onTerminated((o, t) -> {
|
}).onTerminated((o, t) -> {
|
||||||
if (observerCounter.decrementAndGet() == 0) {
|
if (observerCounter.decrementAndGet() == 0) {
|
||||||
unregisterAsGenerator();
|
unregisterAsGenerator();
|
||||||
|
|
@ -257,7 +234,8 @@ public abstract class AbstractMonitor<O extends KubernetesObject,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoked by {@link #onStart(Start)} after the namespace has
|
* Invoked by {@link #onStart(Start)} after the namespace has
|
||||||
* been configured and before starting the observer.
|
* been configured and before starting the observer. This is
|
||||||
|
* the last opportunity to invoke {@link #context(APIResource)}.
|
||||||
*
|
*
|
||||||
* @throws IOException Signals that an I/O exception has occurred.
|
* @throws IOException Signals that an I/O exception has occurred.
|
||||||
* @throws ApiException the api exception
|
* @throws ApiException the api exception
|
||||||
|
|
@ -274,14 +252,4 @@ public abstract class AbstractMonitor<O extends KubernetesObject,
|
||||||
* @param change the change
|
* @param change the change
|
||||||
*/
|
*/
|
||||||
protected abstract void handleChange(K8sClient client, Response<O> change);
|
protected abstract void handleChange(K8sClient client, Response<O> change);
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the {@link Channel} for the given name.
|
|
||||||
*
|
|
||||||
* @param name the name
|
|
||||||
* @return the channel used for events related to the specified object
|
|
||||||
*/
|
|
||||||
protected Optional<C> channel(String name) {
|
|
||||||
return channelManager.getChannel(name);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -100,9 +100,8 @@ public class Controller extends Component {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
attach(new VmMonitor(channel()).channelManager(chanMgr));
|
attach(new VmMonitor(channel(), chanMgr));
|
||||||
attach(new DisplaySecretMonitor(channel())
|
attach(new DisplaySecretMonitor(channel(), chanMgr));
|
||||||
.channelManager(chanMgr.fixed()));
|
|
||||||
// Currently, we don't use the IP assigned by the load balancer
|
// Currently, we don't use the IP assigned by the load balancer
|
||||||
// to access the VM's console. Might change in the future.
|
// to access the VM's console. Might change in the future.
|
||||||
// attach(new ServiceMonitor(channel()).channelManager(chanMgr));
|
// attach(new ServiceMonitor(channel()).channelManager(chanMgr));
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ import org.jdrupes.vmoperator.common.K8sV1SecretStub;
|
||||||
import static org.jdrupes.vmoperator.manager.Constants.COMP_DISPLAY_SECRET;
|
import static org.jdrupes.vmoperator.manager.Constants.COMP_DISPLAY_SECRET;
|
||||||
import static org.jdrupes.vmoperator.manager.Constants.DATA_DISPLAY_PASSWORD;
|
import static org.jdrupes.vmoperator.manager.Constants.DATA_DISPLAY_PASSWORD;
|
||||||
import static org.jdrupes.vmoperator.manager.Constants.DATA_PASSWORD_EXPIRY;
|
import static org.jdrupes.vmoperator.manager.Constants.DATA_PASSWORD_EXPIRY;
|
||||||
|
import org.jdrupes.vmoperator.manager.events.ChannelDictionary;
|
||||||
import org.jdrupes.vmoperator.manager.events.GetDisplayPassword;
|
import org.jdrupes.vmoperator.manager.events.GetDisplayPassword;
|
||||||
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;
|
||||||
|
|
@ -68,14 +69,18 @@ public class DisplaySecretMonitor
|
||||||
private int passwordValidity = 10;
|
private int passwordValidity = 10;
|
||||||
private final List<PendingGet> pendingGets
|
private final List<PendingGet> pendingGets
|
||||||
= Collections.synchronizedList(new LinkedList<>());
|
= Collections.synchronizedList(new LinkedList<>());
|
||||||
|
private final ChannelDictionary<String, VmChannel, ?> channelDictionary;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiates a new display secrets monitor.
|
* Instantiates a new display secrets monitor.
|
||||||
*
|
*
|
||||||
* @param componentChannel the component channel
|
* @param componentChannel the component channel
|
||||||
|
* @param channelDictionary the channel dictionary
|
||||||
*/
|
*/
|
||||||
public DisplaySecretMonitor(Channel componentChannel) {
|
public DisplaySecretMonitor(Channel componentChannel,
|
||||||
|
ChannelDictionary<String, VmChannel, ?> channelDictionary) {
|
||||||
super(componentChannel, V1Secret.class, V1SecretList.class);
|
super(componentChannel, V1Secret.class, V1SecretList.class);
|
||||||
|
this.channelDictionary = channelDictionary;
|
||||||
context(K8sV1SecretStub.CONTEXT);
|
context(K8sV1SecretStub.CONTEXT);
|
||||||
ListOptions options = new ListOptions();
|
ListOptions options = new ListOptions();
|
||||||
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
|
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
|
||||||
|
|
@ -116,7 +121,7 @@ public class DisplaySecretMonitor
|
||||||
if (vmName == null) {
|
if (vmName == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var channel = channel(vmName).orElse(null);
|
var channel = channelDictionary.channel(vmName).orElse(null);
|
||||||
if (channel == null || channel.vmDefinition() == null) {
|
if (channel == null || channel.vmDefinition() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -248,6 +253,7 @@ public class DisplaySecretMonitor
|
||||||
* @param channel the channel
|
* @param channel the channel
|
||||||
*/
|
*/
|
||||||
@Handler
|
@Handler
|
||||||
|
@SuppressWarnings("PMD.AvoidSynchronizedStatement")
|
||||||
public void onVmDefChanged(VmDefChanged event, Channel channel) {
|
public void onVmDefChanged(VmDefChanged event, Channel channel) {
|
||||||
synchronized (pendingGets) {
|
synchronized (pendingGets) {
|
||||||
String vmName = event.vmDefinition().metadata().getName();
|
String vmName = event.vmDefinition().metadata().getName();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
/*
|
||||||
|
* VM-Operator
|
||||||
|
* Copyright (C) 2023 Michael N. Lipp
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.jdrupes.vmoperator.manager;
|
||||||
|
|
||||||
|
import freemarker.template.Configuration;
|
||||||
|
import freemarker.template.TemplateException;
|
||||||
|
import io.kubernetes.client.custom.V1Patch;
|
||||||
|
import io.kubernetes.client.openapi.ApiException;
|
||||||
|
import io.kubernetes.client.util.generic.dynamic.Dynamics;
|
||||||
|
import io.kubernetes.client.util.generic.options.PatchOptions;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import org.jdrupes.vmoperator.common.K8sV1PodStub;
|
||||||
|
import org.jdrupes.vmoperator.common.VmDefinitionModel.RequestedVmState;
|
||||||
|
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
||||||
|
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
||||||
|
import org.yaml.snakeyaml.LoaderOptions;
|
||||||
|
import org.yaml.snakeyaml.Yaml;
|
||||||
|
import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delegee for reconciling the pod.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
|
||||||
|
/* default */ class PodReconciler {
|
||||||
|
|
||||||
|
protected final Logger logger = Logger.getLogger(getClass().getName());
|
||||||
|
private final Configuration fmConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new pod reconciler.
|
||||||
|
*
|
||||||
|
* @param fmConfig the fm config
|
||||||
|
*/
|
||||||
|
public PodReconciler(Configuration fmConfig) {
|
||||||
|
this.fmConfig = fmConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reconcile the pod.
|
||||||
|
*
|
||||||
|
* @param event the event
|
||||||
|
* @param model the model
|
||||||
|
* @param channel the channel
|
||||||
|
* @throws IOException Signals that an I/O exception has occurred.
|
||||||
|
* @throws TemplateException the template exception
|
||||||
|
* @throws ApiException the api exception
|
||||||
|
*/
|
||||||
|
public void reconcile(VmDefChanged event, Map<String, Object> model,
|
||||||
|
VmChannel channel)
|
||||||
|
throws IOException, TemplateException, ApiException {
|
||||||
|
// Don't do anything if stateful set is still in use (pre v3.4)
|
||||||
|
if ((Boolean) model.get("usingSts")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get pod stub.
|
||||||
|
var metadata = event.vmDefinition().getMetadata();
|
||||||
|
var podStub = K8sV1PodStub.get(channel.client(),
|
||||||
|
metadata.getNamespace(), metadata.getName());
|
||||||
|
|
||||||
|
// Nothing to do if exists and should be running
|
||||||
|
if (event.vmDefinition().vmState() == RequestedVmState.RUNNING
|
||||||
|
&& podStub.model().isPresent()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete if running but should be stopped
|
||||||
|
if (event.vmDefinition().vmState() == RequestedVmState.STOPPED) {
|
||||||
|
if (podStub.model().isPresent()) {
|
||||||
|
podStub.delete();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create pod. First combine template and data and parse result
|
||||||
|
var fmTemplate = fmConfig.getTemplate("runnerPod.ftl.yaml");
|
||||||
|
StringWriter out = new StringWriter();
|
||||||
|
fmTemplate.process(model, out);
|
||||||
|
// Avoid Yaml.load due to
|
||||||
|
// https://github.com/kubernetes-client/java/issues/2741
|
||||||
|
var podDef = Dynamics.newFromYaml(
|
||||||
|
new Yaml(new SafeConstructor(new LoaderOptions())), out.toString());
|
||||||
|
|
||||||
|
// Do apply changes
|
||||||
|
PatchOptions opts = new PatchOptions();
|
||||||
|
opts.setForce(true);
|
||||||
|
opts.setFieldManager("kubernetes-java-kubectl-apply");
|
||||||
|
if (podStub.patch(V1Patch.PATCH_FORMAT_APPLY_YAML,
|
||||||
|
new V1Patch(channel.client().getJSON().serialize(podDef)), opts)
|
||||||
|
.isEmpty()) {
|
||||||
|
logger.warning(
|
||||||
|
() -> "Could not patch pod for " + podStub.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,197 @@
|
||||||
|
/*
|
||||||
|
* VM-Operator
|
||||||
|
* Copyright (C) 2023 Michael N. Lipp
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.jdrupes.vmoperator.manager;
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import freemarker.core.ParseException;
|
||||||
|
import freemarker.template.Configuration;
|
||||||
|
import freemarker.template.MalformedTemplateNameException;
|
||||||
|
import freemarker.template.TemplateException;
|
||||||
|
import freemarker.template.TemplateNotFoundException;
|
||||||
|
import io.kubernetes.client.custom.V1Patch;
|
||||||
|
import io.kubernetes.client.openapi.ApiException;
|
||||||
|
import io.kubernetes.client.util.generic.dynamic.Dynamics;
|
||||||
|
import io.kubernetes.client.util.generic.options.ListOptions;
|
||||||
|
import io.kubernetes.client.util.generic.options.PatchOptions;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
|
||||||
|
import static org.jdrupes.vmoperator.common.Constants.VM_OP_NAME;
|
||||||
|
import org.jdrupes.vmoperator.common.K8sV1PvcStub;
|
||||||
|
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
||||||
|
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
||||||
|
import org.jdrupes.vmoperator.util.GsonPtr;
|
||||||
|
import org.yaml.snakeyaml.LoaderOptions;
|
||||||
|
import org.yaml.snakeyaml.Yaml;
|
||||||
|
import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delegee for reconciling the stateful set (effectively the pod).
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
|
||||||
|
/* default */ class PvcReconciler {
|
||||||
|
|
||||||
|
protected final Logger logger = Logger.getLogger(getClass().getName());
|
||||||
|
private final Configuration fmConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new pvc reconciler.
|
||||||
|
*
|
||||||
|
* @param fmConfig the fm config
|
||||||
|
*/
|
||||||
|
public PvcReconciler(Configuration fmConfig) {
|
||||||
|
this.fmConfig = fmConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reconcile the PVCs.
|
||||||
|
*
|
||||||
|
* @param event the event
|
||||||
|
* @param model the model
|
||||||
|
* @param channel the channel
|
||||||
|
* @throws IOException Signals that an I/O exception has occurred.
|
||||||
|
* @throws TemplateException the template exception
|
||||||
|
* @throws ApiException the api exception
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
|
||||||
|
public void reconcile(VmDefChanged event, Map<String, Object> model,
|
||||||
|
VmChannel channel)
|
||||||
|
throws IOException, TemplateException, ApiException {
|
||||||
|
var metadata = event.vmDefinition().getMetadata();
|
||||||
|
|
||||||
|
// Existing disks
|
||||||
|
ListOptions listOpts = new ListOptions();
|
||||||
|
listOpts.setLabelSelector(
|
||||||
|
"app.kubernetes.io/managed-by=" + VM_OP_NAME + ","
|
||||||
|
+ "app.kubernetes.io/name=" + APP_NAME + ","
|
||||||
|
+ "app.kubernetes.io/instance=" + metadata.getName());
|
||||||
|
var knownDisks = K8sV1PvcStub.list(channel.client(),
|
||||||
|
metadata.getNamespace(), listOpts);
|
||||||
|
var knownPvcs = knownDisks.stream().map(K8sV1PvcStub::name)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
// Reconcile runner data pvc
|
||||||
|
reconcileRunnerDataPvc(event, model, channel, knownPvcs);
|
||||||
|
|
||||||
|
// Reconcile pvcs for defined disks
|
||||||
|
var diskDefs = GsonPtr.to((JsonObject) model.get("cr"))
|
||||||
|
.getAsListOf(JsonObject.class, "spec", "vm", "disks");
|
||||||
|
var diskCounter = 0;
|
||||||
|
for (var diskDef : diskDefs) {
|
||||||
|
if (!diskDef.has("volumeClaimTemplate")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var diskName = GsonPtr.to(diskDef)
|
||||||
|
.getAsString("volumeClaimTemplate", "metadata", "name")
|
||||||
|
.map(name -> name + "-disk").orElse("disk-" + diskCounter);
|
||||||
|
diskCounter += 1;
|
||||||
|
diskDef.addProperty("generatedDiskName", diskName);
|
||||||
|
|
||||||
|
// Don't do anything if pvc with old (sts generated) name exists.
|
||||||
|
var stsDiskPvcName = diskName + "-" + metadata.getName() + "-0";
|
||||||
|
if (knownPvcs.contains(stsDiskPvcName)) {
|
||||||
|
diskDef.addProperty("generatedPvcName", stsDiskPvcName);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update PVC
|
||||||
|
model.put("disk", diskDef);
|
||||||
|
reconcileRunnerDiskPvc(event, model, channel);
|
||||||
|
}
|
||||||
|
model.remove("disk");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reconcileRunnerDataPvc(VmDefChanged event,
|
||||||
|
Map<String, Object> model, VmChannel channel,
|
||||||
|
Set<String> knownPvcs)
|
||||||
|
throws TemplateNotFoundException, MalformedTemplateNameException,
|
||||||
|
ParseException, IOException, TemplateException, ApiException {
|
||||||
|
var metadata = event.vmDefinition().getMetadata();
|
||||||
|
|
||||||
|
// Look for old (sts generated) name.
|
||||||
|
var stsRunnerDataPvcName
|
||||||
|
= "runner-data" + "-" + metadata.getName() + "-0";
|
||||||
|
if (knownPvcs.contains(stsRunnerDataPvcName)) {
|
||||||
|
model.put("runnerDataPvcName", stsRunnerDataPvcName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate PVC
|
||||||
|
model.put("runnerDataPvcName", metadata.getName() + "-runner-data");
|
||||||
|
var fmTemplate = fmConfig.getTemplate("runnerDataPvc.ftl.yaml");
|
||||||
|
StringWriter out = new StringWriter();
|
||||||
|
fmTemplate.process(model, out);
|
||||||
|
// Avoid Yaml.load due to
|
||||||
|
// https://github.com/kubernetes-client/java/issues/2741
|
||||||
|
var pvcDef = Dynamics.newFromYaml(
|
||||||
|
new Yaml(new SafeConstructor(new LoaderOptions())), out.toString());
|
||||||
|
|
||||||
|
// Do apply changes
|
||||||
|
var pvcStub = K8sV1PvcStub.get(channel.client(),
|
||||||
|
metadata.getNamespace(), (String) model.get("runnerDataPvcName"));
|
||||||
|
PatchOptions opts = new PatchOptions();
|
||||||
|
opts.setForce(true);
|
||||||
|
opts.setFieldManager("kubernetes-java-kubectl-apply");
|
||||||
|
if (pvcStub.patch(V1Patch.PATCH_FORMAT_APPLY_YAML,
|
||||||
|
new V1Patch(channel.client().getJSON().serialize(pvcDef)), opts)
|
||||||
|
.isEmpty()) {
|
||||||
|
logger.warning(
|
||||||
|
() -> "Could not patch pvc for " + pvcStub.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reconcileRunnerDiskPvc(VmDefChanged event,
|
||||||
|
Map<String, Object> model, VmChannel channel)
|
||||||
|
throws TemplateNotFoundException, MalformedTemplateNameException,
|
||||||
|
ParseException, IOException, TemplateException, ApiException {
|
||||||
|
var metadata = event.vmDefinition().getMetadata();
|
||||||
|
|
||||||
|
// Generate PVC
|
||||||
|
var diskDef = GsonPtr.to((JsonElement) model.get("disk"));
|
||||||
|
var pvcName = metadata.getName() + "-"
|
||||||
|
+ diskDef.getAsString("generatedDiskName").get();
|
||||||
|
diskDef.set("generatedPvcName", pvcName);
|
||||||
|
var fmTemplate = fmConfig.getTemplate("runnerDiskPvc.ftl.yaml");
|
||||||
|
StringWriter out = new StringWriter();
|
||||||
|
fmTemplate.process(model, out);
|
||||||
|
// Avoid Yaml.load due to
|
||||||
|
// https://github.com/kubernetes-client/java/issues/2741
|
||||||
|
var pvcDef = Dynamics.newFromYaml(
|
||||||
|
new Yaml(new SafeConstructor(new LoaderOptions())), out.toString());
|
||||||
|
|
||||||
|
// Do apply changes
|
||||||
|
var pvcStub = K8sV1PvcStub.get(channel.client(),
|
||||||
|
metadata.getNamespace(), GsonPtr.to((JsonElement) model.get("disk"))
|
||||||
|
.getAsString("generatedPvcName").get());
|
||||||
|
PatchOptions opts = new PatchOptions();
|
||||||
|
opts.setForce(true);
|
||||||
|
opts.setFieldManager("kubernetes-java-kubectl-apply");
|
||||||
|
if (pvcStub.patch(V1Patch.PATCH_FORMAT_APPLY_YAML,
|
||||||
|
new V1Patch(channel.client().getJSON().serialize(pvcDef)), opts)
|
||||||
|
.isEmpty()) {
|
||||||
|
logger.warning(
|
||||||
|
() -> "Could not patch pvc for " + pvcStub.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -69,20 +69,25 @@ import org.jgrapes.util.events.ConfigurationUpdate;
|
||||||
*
|
*
|
||||||
* * A [`ConfigMap`](https://kubernetes.io/docs/concepts/configuration/configmap/)
|
* * A [`ConfigMap`](https://kubernetes.io/docs/concepts/configuration/configmap/)
|
||||||
* that defines the configuration file for the runner.
|
* that defines the configuration file for the runner.
|
||||||
*
|
*
|
||||||
* * A [`StatefulSet`](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/)
|
* * A [`PVC`](https://kubernetes.io/docs/concepts/storage/persistent-volumes/)
|
||||||
* that creates
|
* for 1 MiB of persistent storage used by the Runner (referred to as the
|
||||||
* * the [`Pod`](https://kubernetes.io/docs/concepts/workloads/pods/)
|
* "runnerDataPvc")
|
||||||
* with the Runner instance,
|
*
|
||||||
* * a PVC for 1 MiB of persistent storage used by the Runner
|
* * The PVCs for the VM's disks.
|
||||||
* (referred to as the "runnerDataPvc") and
|
*
|
||||||
* * the PVCs for the VM's disks.
|
* * A [`Pod`](https://kubernetes.io/docs/concepts/workloads/pods/) with the
|
||||||
*
|
* runner instance[^oldSts].
|
||||||
|
*
|
||||||
* * (Optional) A load balancer
|
* * (Optional) A load balancer
|
||||||
* [`Service`](https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/)
|
* [`Service`](https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/)
|
||||||
* that allows the user to access a VM's console without knowing which
|
* that allows the user to access a VM's console without knowing which
|
||||||
* node it runs on.
|
* node it runs on.
|
||||||
*
|
*
|
||||||
|
* [^oldSts]: Before version 3.4, the operator created a
|
||||||
|
* [`StatefulSet`](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/)
|
||||||
|
* that created the pod.
|
||||||
|
*
|
||||||
* The reconciler is part of the {@link Controller} component. It's
|
* The reconciler is part of the {@link Controller} component. It's
|
||||||
* configuration properties are therefore defined in
|
* configuration properties are therefore defined in
|
||||||
* ```yaml
|
* ```yaml
|
||||||
|
|
@ -135,6 +140,8 @@ public class Reconciler extends Component {
|
||||||
private final ConfigMapReconciler cmReconciler;
|
private final ConfigMapReconciler cmReconciler;
|
||||||
private final DisplaySecretReconciler dsReconciler;
|
private final DisplaySecretReconciler dsReconciler;
|
||||||
private final StatefulSetReconciler stsReconciler;
|
private final StatefulSetReconciler stsReconciler;
|
||||||
|
private final PvcReconciler pvcReconciler;
|
||||||
|
private final PodReconciler podReconciler;
|
||||||
private final LoadBalancerReconciler lbReconciler;
|
private final LoadBalancerReconciler lbReconciler;
|
||||||
@SuppressWarnings("PMD.UseConcurrentHashMap")
|
@SuppressWarnings("PMD.UseConcurrentHashMap")
|
||||||
private final Map<String, Object> config = new HashMap<>();
|
private final Map<String, Object> config = new HashMap<>();
|
||||||
|
|
@ -160,6 +167,8 @@ public class Reconciler extends Component {
|
||||||
cmReconciler = new ConfigMapReconciler(fmConfig);
|
cmReconciler = new ConfigMapReconciler(fmConfig);
|
||||||
dsReconciler = new DisplaySecretReconciler();
|
dsReconciler = new DisplaySecretReconciler();
|
||||||
stsReconciler = new StatefulSetReconciler(fmConfig);
|
stsReconciler = new StatefulSetReconciler(fmConfig);
|
||||||
|
pvcReconciler = new PvcReconciler(fmConfig);
|
||||||
|
podReconciler = new PodReconciler(fmConfig);
|
||||||
lbReconciler = new LoadBalancerReconciler(fmConfig);
|
lbReconciler = new LoadBalancerReconciler(fmConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -206,7 +215,10 @@ public class Reconciler extends Component {
|
||||||
var configMap = cmReconciler.reconcile(model, channel);
|
var configMap = cmReconciler.reconcile(model, channel);
|
||||||
model.put("cm", configMap.getRaw());
|
model.put("cm", configMap.getRaw());
|
||||||
dsReconciler.reconcile(event, model, channel);
|
dsReconciler.reconcile(event, model, channel);
|
||||||
|
// Manage (eventual) removal of stateful set.
|
||||||
stsReconciler.reconcile(event, model, channel);
|
stsReconciler.reconcile(event, model, channel);
|
||||||
|
pvcReconciler.reconcile(event, model, channel);
|
||||||
|
podReconciler.reconcile(event, model, channel);
|
||||||
lbReconciler.reconcile(event, model, channel);
|
lbReconciler.reconcile(event, model, channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.jdrupes.vmoperator.manager;
|
|
||||||
|
|
||||||
import io.kubernetes.client.openapi.ApiException;
|
|
||||||
import io.kubernetes.client.openapi.models.V1Service;
|
|
||||||
import io.kubernetes.client.openapi.models.V1ServiceList;
|
|
||||||
import io.kubernetes.client.util.Watch.Response;
|
|
||||||
import io.kubernetes.client.util.generic.options.ListOptions;
|
|
||||||
import java.io.IOException;
|
|
||||||
import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
|
|
||||||
import org.jdrupes.vmoperator.common.K8sClient;
|
|
||||||
import org.jdrupes.vmoperator.common.K8sObserver.ResponseType;
|
|
||||||
import org.jdrupes.vmoperator.common.K8sV1ServiceStub;
|
|
||||||
import org.jdrupes.vmoperator.manager.events.ServiceChanged;
|
|
||||||
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
|
||||||
import org.jgrapes.core.Channel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Watches for changes of services.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
|
|
||||||
public class ServiceMonitor
|
|
||||||
extends AbstractMonitor<V1Service, V1ServiceList, VmChannel> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instantiates a new display secrets monitor.
|
|
||||||
*
|
|
||||||
* @param componentChannel the component channel
|
|
||||||
*/
|
|
||||||
public ServiceMonitor(Channel componentChannel) {
|
|
||||||
super(componentChannel, V1Service.class, V1ServiceList.class);
|
|
||||||
context(K8sV1ServiceStub.CONTEXT);
|
|
||||||
ListOptions options = new ListOptions();
|
|
||||||
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME);
|
|
||||||
options(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void prepareMonitoring() throws IOException, ApiException {
|
|
||||||
client(new K8sClient());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void handleChange(K8sClient client, Response<V1Service> change) {
|
|
||||||
String vmName = change.object.getMetadata().getLabels()
|
|
||||||
.get("app.kubernetes.io/instance");
|
|
||||||
if (vmName == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var channel = channel(vmName).orElse(null);
|
|
||||||
if (channel == null || channel.vmDefinition() == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
channel.pipeline().fire(new ServiceChanged(
|
|
||||||
ResponseType.valueOf(change.type), change.object), channel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -37,7 +37,9 @@ import org.yaml.snakeyaml.Yaml;
|
||||||
import org.yaml.snakeyaml.constructor.SafeConstructor;
|
import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delegee for reconciling the stateful set (effectively the pod).
|
* Before version 3.4, the pod running the VM was created by a stateful set.
|
||||||
|
* Starting with version 3.4, this reconciler simply deletes the stateful
|
||||||
|
* set, provided that the VM is not running.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
|
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
|
||||||
/* default */ class StatefulSetReconciler {
|
/* default */ class StatefulSetReconciler {
|
||||||
|
|
@ -46,7 +48,7 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
private final Configuration fmConfig;
|
private final Configuration fmConfig;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiates a new config map reconciler.
|
* Instantiates a new stateful set reconciler.
|
||||||
*
|
*
|
||||||
* @param fmConfig the fm config
|
* @param fmConfig the fm config
|
||||||
*/
|
*/
|
||||||
|
|
@ -64,12 +66,34 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
* @throws TemplateException the template exception
|
* @throws TemplateException the template exception
|
||||||
* @throws ApiException the api exception
|
* @throws ApiException the api exception
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
|
||||||
public void reconcile(VmDefChanged event, Map<String, Object> model,
|
public void reconcile(VmDefChanged event, Map<String, Object> model,
|
||||||
VmChannel channel)
|
VmChannel channel)
|
||||||
throws IOException, TemplateException, ApiException {
|
throws IOException, TemplateException, ApiException {
|
||||||
var metadata = event.vmDefinition().getMetadata();
|
var metadata = event.vmDefinition().getMetadata();
|
||||||
|
model.put("usingSts", false);
|
||||||
|
|
||||||
// Combine template and data and parse result
|
// If exists, delete when not running or supposed to be not running.
|
||||||
|
var stsStub = K8sV1StatefulSetStub.get(channel.client(),
|
||||||
|
metadata.getNamespace(), metadata.getName());
|
||||||
|
if (stsStub.model().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stateful set still exists, check if replicas is 0 so we can
|
||||||
|
// delete it.
|
||||||
|
var stsModel = stsStub.model().get();
|
||||||
|
if (stsModel.getSpec().getReplicas() == 0) {
|
||||||
|
stsStub.delete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cannot yet delete the stateful set.
|
||||||
|
model.put("usingSts", true);
|
||||||
|
|
||||||
|
// Check if VM is supposed to be stopped. If so,
|
||||||
|
// set replicas to 0. This is the first step of the transition,
|
||||||
|
// the stateful set will be deleted when the VM is restarted.
|
||||||
var fmTemplate = fmConfig.getTemplate("runnerSts.ftl.yaml");
|
var fmTemplate = fmConfig.getTemplate("runnerSts.ftl.yaml");
|
||||||
StringWriter out = new StringWriter();
|
StringWriter out = new StringWriter();
|
||||||
fmTemplate.process(model, out);
|
fmTemplate.process(model, out);
|
||||||
|
|
@ -77,22 +101,13 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
// https://github.com/kubernetes-client/java/issues/2741
|
// https://github.com/kubernetes-client/java/issues/2741
|
||||||
var stsDef = Dynamics.newFromYaml(
|
var stsDef = Dynamics.newFromYaml(
|
||||||
new Yaml(new SafeConstructor(new LoaderOptions())), out.toString());
|
new Yaml(new SafeConstructor(new LoaderOptions())), out.toString());
|
||||||
|
var desired = GsonPtr.to(stsDef.getRaw())
|
||||||
// If exists apply changes only when transitioning state
|
.to("spec").getAsInt("replicas").orElse(1);
|
||||||
// or not running.
|
if (desired == 1) {
|
||||||
var stsStub = K8sV1StatefulSetStub.get(channel.client(),
|
return;
|
||||||
metadata.getNamespace(), metadata.getName());
|
|
||||||
var stsModel = stsStub.model().orElse(null);
|
|
||||||
if (stsModel != null) {
|
|
||||||
var current = stsModel.getSpec().getReplicas();
|
|
||||||
var desired = GsonPtr.to(stsDef.getRaw())
|
|
||||||
.to("spec").getAsInt("replicas").orElse(1);
|
|
||||||
if (current == 1 && desired == 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do apply changes
|
// Do apply changes (set replicas to 0)
|
||||||
PatchOptions opts = new PatchOptions();
|
PatchOptions opts = new PatchOptions();
|
||||||
opts.setForce(true);
|
opts.setForce(true);
|
||||||
opts.setFieldManager("kubernetes-java-kubectl-apply");
|
opts.setFieldManager("kubernetes-java-kubectl-apply");
|
||||||
|
|
|
||||||
|
|
@ -43,10 +43,12 @@ import org.jdrupes.vmoperator.common.VmDefinitionStub;
|
||||||
import static org.jdrupes.vmoperator.manager.Constants.APP_NAME;
|
import static org.jdrupes.vmoperator.manager.Constants.APP_NAME;
|
||||||
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_KIND_VM;
|
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_KIND_VM;
|
||||||
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME;
|
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME;
|
||||||
|
import org.jdrupes.vmoperator.manager.events.ChannelManager;
|
||||||
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;
|
||||||
import org.jdrupes.vmoperator.util.GsonPtr;
|
import org.jdrupes.vmoperator.util.GsonPtr;
|
||||||
import org.jgrapes.core.Channel;
|
import org.jgrapes.core.Channel;
|
||||||
|
import org.jgrapes.core.Event;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Watches for changes of VM definitions.
|
* Watches for changes of VM definitions.
|
||||||
|
|
@ -55,14 +57,19 @@ import org.jgrapes.core.Channel;
|
||||||
public class VmMonitor extends
|
public class VmMonitor extends
|
||||||
AbstractMonitor<VmDefinitionModel, VmDefinitionModels, VmChannel> {
|
AbstractMonitor<VmDefinitionModel, VmDefinitionModels, VmChannel> {
|
||||||
|
|
||||||
|
private final ChannelManager<String, VmChannel, ?> channelManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiates a new VM definition watcher.
|
* Instantiates a new VM definition watcher.
|
||||||
*
|
*
|
||||||
* @param componentChannel the component channel
|
* @param componentChannel the component channel
|
||||||
|
* @param channelManager the channel manager
|
||||||
*/
|
*/
|
||||||
public VmMonitor(Channel componentChannel) {
|
public VmMonitor(Channel componentChannel,
|
||||||
|
ChannelManager<String, VmChannel, ?> channelManager) {
|
||||||
super(componentChannel, VmDefinitionModel.class,
|
super(componentChannel, VmDefinitionModel.class,
|
||||||
VmDefinitionModels.class);
|
VmDefinitionModels.class);
|
||||||
|
this.channelManager = channelManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -107,10 +114,7 @@ public class VmMonitor extends
|
||||||
protected void handleChange(K8sClient client,
|
protected void handleChange(K8sClient client,
|
||||||
Watch.Response<VmDefinitionModel> response) {
|
Watch.Response<VmDefinitionModel> response) {
|
||||||
V1ObjectMeta metadata = response.object.getMetadata();
|
V1ObjectMeta metadata = response.object.getMetadata();
|
||||||
VmChannel channel = channel(metadata.getName()).orElse(null);
|
VmChannel channel = channelManager.channelGet(metadata.getName());
|
||||||
if (channel == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get full definition and associate with channel as backup
|
// Get full definition and associate with channel as backup
|
||||||
var vmDef = response.object;
|
var vmDef = response.object;
|
||||||
|
|
@ -132,13 +136,24 @@ public class VmMonitor extends
|
||||||
() -> "Cannot get model for " + response.object.getMetadata());
|
() -> "Cannot get model for " + response.object.getMetadata());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (ResponseType.valueOf(response.type) == ResponseType.DELETED) {
|
||||||
|
channelManager.remove(metadata.getName());
|
||||||
|
}
|
||||||
|
|
||||||
// Create and fire event
|
// Create and fire changed event. Remove channel from channel
|
||||||
|
// manager on completion.
|
||||||
channel.pipeline()
|
channel.pipeline()
|
||||||
.fire(new VmDefChanged(ResponseType.valueOf(response.type),
|
.fire(Event.onCompletion(
|
||||||
channel.setGeneration(
|
new VmDefChanged(ResponseType.valueOf(response.type),
|
||||||
response.object.getMetadata().getGeneration()),
|
channel.setGeneration(
|
||||||
vmDef), channel);
|
response.object.getMetadata().getGeneration()),
|
||||||
|
vmDef),
|
||||||
|
e -> {
|
||||||
|
if (e.type() == ResponseType.DELETED) {
|
||||||
|
channelManager
|
||||||
|
.remove(e.vmDefinition().metadata().getName());
|
||||||
|
}
|
||||||
|
}), channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
private VmDefinitionModel getModel(K8sClient client,
|
private VmDefinitionModel getModel(K8sClient client,
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,24 @@ package org.jdrupes.vmoperator.manager;
|
||||||
|
|
||||||
import io.kubernetes.client.Discovery.APIResource;
|
import io.kubernetes.client.Discovery.APIResource;
|
||||||
import io.kubernetes.client.openapi.ApiException;
|
import io.kubernetes.client.openapi.ApiException;
|
||||||
|
import io.kubernetes.client.util.generic.options.ListOptions;
|
||||||
|
|
||||||
import java.io.FileReader;
|
import java.io.FileReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
|
||||||
import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP;
|
import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP;
|
||||||
import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM;
|
import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM;
|
||||||
|
import static org.jdrupes.vmoperator.common.Constants.VM_OP_NAME;
|
||||||
|
|
||||||
import org.jdrupes.vmoperator.common.K8s;
|
import org.jdrupes.vmoperator.common.K8s;
|
||||||
import org.jdrupes.vmoperator.common.K8sClient;
|
import org.jdrupes.vmoperator.common.K8sClient;
|
||||||
import org.jdrupes.vmoperator.common.K8sDynamicStub;
|
import org.jdrupes.vmoperator.common.K8sDynamicStub;
|
||||||
import org.jdrupes.vmoperator.common.K8sV1ConfigMapStub;
|
import org.jdrupes.vmoperator.common.K8sV1ConfigMapStub;
|
||||||
import org.jdrupes.vmoperator.common.K8sV1DeploymentStub;
|
import org.jdrupes.vmoperator.common.K8sV1DeploymentStub;
|
||||||
import org.jdrupes.vmoperator.common.K8sV1StatefulSetStub;
|
import org.jdrupes.vmoperator.common.K8sV1PodStub;
|
||||||
|
import org.jdrupes.vmoperator.common.K8sV1PvcStub;
|
||||||
import org.junit.jupiter.api.AfterAll;
|
import org.junit.jupiter.api.AfterAll;
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import org.junit.jupiter.api.BeforeAll;
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
|
@ -78,7 +85,7 @@ class BasicTests {
|
||||||
|
|
||||||
// Wait for created resources
|
// Wait for created resources
|
||||||
assertTrue(waitForConfigMap(client));
|
assertTrue(waitForConfigMap(client));
|
||||||
assertTrue(waitForStatefulSet(client));
|
assertTrue(waitForPvc(client));
|
||||||
|
|
||||||
// Check config map
|
// Check config map
|
||||||
var config = K8sV1ConfigMapStub.get(client, "vmop-dev", "unittest-vm")
|
var config = K8sV1ConfigMapStub.get(client, "vmop-dev", "unittest-vm")
|
||||||
|
|
@ -93,6 +100,15 @@ class BasicTests {
|
||||||
// Cleanup
|
// Cleanup
|
||||||
K8sDynamicStub.get(client, vmsContext, "vmop-dev", "unittest-vm")
|
K8sDynamicStub.get(client, vmsContext, "vmop-dev", "unittest-vm")
|
||||||
.delete();
|
.delete();
|
||||||
|
ListOptions listOpts = new ListOptions();
|
||||||
|
listOpts.setLabelSelector(
|
||||||
|
"app.kubernetes.io/managed-by=" + VM_OP_NAME + ","
|
||||||
|
+ "app.kubernetes.io/name=" + APP_NAME + ","
|
||||||
|
+ "app.kubernetes.io/instance=unittest-vm");
|
||||||
|
var knownPvcs = K8sV1PvcStub.list(client, "vmop-dev", listOpts);
|
||||||
|
for (var pvc : knownPvcs) {
|
||||||
|
pvc.delete();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean waitForConfigMap(K8sClient client)
|
private boolean waitForConfigMap(K8sClient client)
|
||||||
|
|
@ -107,9 +123,10 @@ class BasicTests {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean waitForStatefulSet(K8sClient client)
|
private boolean waitForPvc(K8sClient client)
|
||||||
throws InterruptedException, ApiException {
|
throws InterruptedException, ApiException {
|
||||||
var stub = K8sV1StatefulSetStub.get(client, "vmop-dev", "unittest-vm");
|
var stub
|
||||||
|
= K8sV1PvcStub.get(client, "vmop-dev", "unittest-vm-runner-data");
|
||||||
for (int i = 0; i < 10; i++) {
|
for (int i = 0; i < 10; i++) {
|
||||||
if (stub.model().isPresent()) {
|
if (stub.model().isPresent()) {
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,9 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'org.jgrapes:org.jgrapes.core:[1.21.0,2)'
|
implementation 'org.jgrapes:org.jgrapes.core:[1.22.1,2)'
|
||||||
implementation 'org.jgrapes:org.jgrapes.util:[1.36.0,2)'
|
implementation 'org.jgrapes:org.jgrapes.util:[1.38.1,2)'
|
||||||
implementation 'org.jgrapes:org.jgrapes.io:[2.11.0,3)'
|
implementation 'org.jgrapes:org.jgrapes.io:[2.12.1,3)'
|
||||||
implementation 'org.jgrapes:org.jgrapes.http:[3.5.0,4)'
|
implementation 'org.jgrapes:org.jgrapes.http:[3.5.0,4)'
|
||||||
implementation project(':org.jdrupes.vmoperator.common')
|
implementation project(':org.jdrupes.vmoperator.common')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,8 @@ public class TimeSeries {
|
||||||
* @param numbers the numbers
|
* @param numbers the numbers
|
||||||
* @return the time series
|
* @return the time series
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
|
@SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition",
|
||||||
|
"PMD.AvoidSynchronizedStatement" })
|
||||||
public TimeSeries add(Instant time, Number... numbers) {
|
public TimeSeries add(Instant time, Number... numbers) {
|
||||||
var newEntry = new Entry(time, numbers);
|
var newEntry = new Entry(time, numbers);
|
||||||
boolean nothingNew = false;
|
boolean nothingNew = false;
|
||||||
|
|
@ -83,6 +84,7 @@ public class TimeSeries {
|
||||||
*
|
*
|
||||||
* @return the list
|
* @return the list
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("PMD.AvoidSynchronizedStatement")
|
||||||
public List<Entry> entries() {
|
public List<Entry> entries() {
|
||||||
synchronized (data) {
|
synchronized (data) {
|
||||||
return new ArrayList<>(data);
|
return new ArrayList<>(data);
|
||||||
|
|
|
||||||
|
|
@ -30,14 +30,14 @@ import java.math.BigDecimal;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.HashSet;
|
import java.util.EnumSet;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import org.jdrupes.json.JsonBeanDecoder;
|
import org.jdrupes.json.JsonBeanDecoder;
|
||||||
import org.jdrupes.json.JsonDecodeException;
|
import org.jdrupes.json.JsonDecodeException;
|
||||||
import org.jdrupes.vmoperator.common.K8sObserver;
|
import org.jdrupes.vmoperator.common.K8sObserver;
|
||||||
import org.jdrupes.vmoperator.common.VmDefinitionModel;
|
import org.jdrupes.vmoperator.common.VmDefinitionModel;
|
||||||
import org.jdrupes.vmoperator.manager.events.ChannelCache;
|
import org.jdrupes.vmoperator.manager.events.ChannelTracker;
|
||||||
import org.jdrupes.vmoperator.manager.events.ModifyVm;
|
import org.jdrupes.vmoperator.manager.events.ModifyVm;
|
||||||
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;
|
||||||
|
|
@ -68,8 +68,8 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
|
|
||||||
private static final Set<RenderMode> MODES = RenderMode.asSet(
|
private static final Set<RenderMode> MODES = RenderMode.asSet(
|
||||||
RenderMode.Preview, RenderMode.View);
|
RenderMode.Preview, RenderMode.View);
|
||||||
private final ChannelCache<String, VmChannel,
|
private final ChannelTracker<String, VmChannel,
|
||||||
VmDefinitionModel> channelManager = new ChannelCache<>();
|
VmDefinitionModel> channelTracker = new ChannelTracker<>();
|
||||||
private final TimeSeries summarySeries = new TimeSeries(Duration.ofDays(1));
|
private final TimeSeries summarySeries = new TimeSeries(Duration.ofDays(1));
|
||||||
private Summary cachedSummary;
|
private Summary cachedSummary;
|
||||||
|
|
||||||
|
|
@ -128,7 +128,7 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
protected Set<RenderMode> doRenderConlet(RenderConletRequestBase<?> event,
|
protected Set<RenderMode> doRenderConlet(RenderConletRequestBase<?> event,
|
||||||
ConsoleConnection channel, String conletId, VmsModel conletState)
|
ConsoleConnection channel, String conletId, VmsModel conletState)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
Set<RenderMode> renderedAs = new HashSet<>();
|
Set<RenderMode> renderedAs = EnumSet.noneOf(RenderMode.class);
|
||||||
boolean sendVmInfos = false;
|
boolean sendVmInfos = false;
|
||||||
if (event.renderAs().contains(RenderMode.Preview)) {
|
if (event.renderAs().contains(RenderMode.Preview)) {
|
||||||
Template tpl
|
Template tpl
|
||||||
|
|
@ -160,7 +160,7 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
sendVmInfos = true;
|
sendVmInfos = true;
|
||||||
}
|
}
|
||||||
if (sendVmInfos) {
|
if (sendVmInfos) {
|
||||||
for (var vmDef : channelManager.associated()) {
|
for (var vmDef : channelTracker.associated()) {
|
||||||
var def
|
var def
|
||||||
= JsonBeanDecoder.create(vmDef.data().toString())
|
= JsonBeanDecoder.create(vmDef.data().toString())
|
||||||
.readObject();
|
.readObject();
|
||||||
|
|
@ -188,7 +188,7 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
throws JsonDecodeException, IOException {
|
throws JsonDecodeException, IOException {
|
||||||
var vmName = event.vmDefinition().getMetadata().getName();
|
var vmName = event.vmDefinition().getMetadata().getName();
|
||||||
if (event.type() == K8sObserver.ResponseType.DELETED) {
|
if (event.type() == K8sObserver.ResponseType.DELETED) {
|
||||||
channelManager.remove(vmName);
|
channelTracker.remove(vmName);
|
||||||
for (var entry : conletIdsByConsoleConnection().entrySet()) {
|
for (var entry : conletIdsByConsoleConnection().entrySet()) {
|
||||||
for (String conletId : entry.getValue()) {
|
for (String conletId : entry.getValue()) {
|
||||||
entry.getKey().respond(new NotifyConletView(type(),
|
entry.getKey().respond(new NotifyConletView(type(),
|
||||||
|
|
@ -198,7 +198,7 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
} else {
|
} else {
|
||||||
var vmDef = new VmDefinitionModel(channel.client().getJSON()
|
var vmDef = new VmDefinitionModel(channel.client().getJSON()
|
||||||
.getGson(), cleanup(event.vmDefinition().data()));
|
.getGson(), cleanup(event.vmDefinition().data()));
|
||||||
channelManager.put(vmName, channel, vmDef);
|
channelTracker.put(vmName, channel, vmDef);
|
||||||
var def = JsonBeanDecoder.create(vmDef.data().toString())
|
var def = JsonBeanDecoder.create(vmDef.data().toString())
|
||||||
.readObject();
|
.readObject();
|
||||||
for (var entry : conletIdsByConsoleConnection().entrySet()) {
|
for (var entry : conletIdsByConsoleConnection().entrySet()) {
|
||||||
|
|
@ -321,7 +321,7 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
return cachedSummary;
|
return cachedSummary;
|
||||||
}
|
}
|
||||||
Summary summary = new Summary();
|
Summary summary = new Summary();
|
||||||
for (var vmDef : channelManager.associated()) {
|
for (var vmDef : channelTracker.associated()) {
|
||||||
summary.totalVms += 1;
|
summary.totalVms += 1;
|
||||||
var status = GsonPtr.to(vmDef.data()).to("status");
|
var status = GsonPtr.to(vmDef.data()).to("status");
|
||||||
summary.usedCpus += status.getAsInt("cpus").orElse(0);
|
summary.usedCpus += status.getAsInt("cpus").orElse(0);
|
||||||
|
|
@ -347,7 +347,7 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
throws Exception {
|
throws Exception {
|
||||||
event.stop();
|
event.stop();
|
||||||
var vmName = event.params().asString(0);
|
var vmName = event.params().asString(0);
|
||||||
var vmChannel = channelManager.channel(vmName).orElse(null);
|
var vmChannel = channelTracker.channel(vmName).orElse(null);
|
||||||
if (vmChannel == null) {
|
if (vmChannel == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ import org.jdrupes.vmoperator.common.K8sDynamicModel;
|
||||||
import org.jdrupes.vmoperator.common.K8sObserver;
|
import org.jdrupes.vmoperator.common.K8sObserver;
|
||||||
import org.jdrupes.vmoperator.common.VmDefinitionModel;
|
import org.jdrupes.vmoperator.common.VmDefinitionModel;
|
||||||
import org.jdrupes.vmoperator.common.VmDefinitionModel.Permission;
|
import org.jdrupes.vmoperator.common.VmDefinitionModel.Permission;
|
||||||
import org.jdrupes.vmoperator.manager.events.ChannelCache;
|
import org.jdrupes.vmoperator.manager.events.ChannelTracker;
|
||||||
import org.jdrupes.vmoperator.manager.events.GetDisplayPassword;
|
import org.jdrupes.vmoperator.manager.events.GetDisplayPassword;
|
||||||
import org.jdrupes.vmoperator.manager.events.ModifyVm;
|
import org.jdrupes.vmoperator.manager.events.ModifyVm;
|
||||||
import org.jdrupes.vmoperator.manager.events.ResetVm;
|
import org.jdrupes.vmoperator.manager.events.ResetVm;
|
||||||
|
|
@ -122,8 +122,8 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
RenderMode.Preview, RenderMode.Edit);
|
RenderMode.Preview, RenderMode.Edit);
|
||||||
private static final Set<RenderMode> MODES_FOR_GENERATED = RenderMode.asSet(
|
private static final Set<RenderMode> MODES_FOR_GENERATED = RenderMode.asSet(
|
||||||
RenderMode.Preview, RenderMode.StickyPreview);
|
RenderMode.Preview, RenderMode.StickyPreview);
|
||||||
private final ChannelCache<String, VmChannel,
|
private final ChannelTracker<String, VmChannel,
|
||||||
VmDefinitionModel> channelManager = new ChannelCache<>();
|
VmDefinitionModel> channelTracker = new ChannelTracker<>();
|
||||||
private static ObjectMapper objectMapper
|
private static ObjectMapper objectMapper
|
||||||
= new ObjectMapper().registerModule(new JavaTimeModule());
|
= new ObjectMapper().registerModule(new JavaTimeModule());
|
||||||
private Class<?> preferredIpVersion = Inet4Address.class;
|
private Class<?> preferredIpVersion = Inet4Address.class;
|
||||||
|
|
@ -349,7 +349,7 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
|
|
||||||
// Remove conlet if definition has been removed
|
// Remove conlet if definition has been removed
|
||||||
if (model.vmName() != null
|
if (model.vmName() != null
|
||||||
&& !channelManager.associated(model.vmName()).isPresent()) {
|
&& !channelTracker.associated(model.vmName()).isPresent()) {
|
||||||
channel.respond(
|
channel.respond(
|
||||||
new DeleteConlet(conletId, Collections.emptySet()));
|
new DeleteConlet(conletId, Collections.emptySet()));
|
||||||
return Collections.emptySet();
|
return Collections.emptySet();
|
||||||
|
|
@ -357,7 +357,7 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
|
|
||||||
// Don't render if user has not at least one permission
|
// Don't render if user has not at least one permission
|
||||||
if (model.vmName() != null
|
if (model.vmName() != null
|
||||||
&& channelManager.associated(model.vmName())
|
&& channelTracker.associated(model.vmName())
|
||||||
.map(d -> permissions(d, channel.session()).isEmpty())
|
.map(d -> permissions(d, channel.session()).isEmpty())
|
||||||
.orElse(true)) {
|
.orElse(true)) {
|
||||||
return Collections.emptySet();
|
return Collections.emptySet();
|
||||||
|
|
@ -395,7 +395,7 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> accessibleVms(ConsoleConnection channel) {
|
private List<String> accessibleVms(ConsoleConnection channel) {
|
||||||
return channelManager.associated().stream()
|
return channelTracker.associated().stream()
|
||||||
.filter(d -> !permissions(d, channel.session()).isEmpty())
|
.filter(d -> !permissions(d, channel.session()).isEmpty())
|
||||||
.map(d -> d.getMetadata().getName()).sorted().toList();
|
.map(d -> d.getMetadata().getName()).sorted().toList();
|
||||||
}
|
}
|
||||||
|
|
@ -419,7 +419,7 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
if (Strings.isNullOrEmpty(model.vmName())) {
|
if (Strings.isNullOrEmpty(model.vmName())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
channelManager.associated(model.vmName()).ifPresent(vmDef -> {
|
channelTracker.associated(model.vmName()).ifPresent(vmDef -> {
|
||||||
try {
|
try {
|
||||||
var def = JsonBeanDecoder.create(vmDef.data().toString())
|
var def = JsonBeanDecoder.create(vmDef.data().toString())
|
||||||
.readObject();
|
.readObject();
|
||||||
|
|
@ -465,9 +465,9 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
.remove("managedFields");
|
.remove("managedFields");
|
||||||
var vmName = vmDef.getMetadata().getName();
|
var vmName = vmDef.getMetadata().getName();
|
||||||
if (event.type() == K8sObserver.ResponseType.DELETED) {
|
if (event.type() == K8sObserver.ResponseType.DELETED) {
|
||||||
channelManager.remove(vmName);
|
channelTracker.remove(vmName);
|
||||||
} else {
|
} else {
|
||||||
channelManager.put(vmName, channel, vmDef);
|
channelTracker.put(vmName, channel, vmDef);
|
||||||
}
|
}
|
||||||
for (var entry : conletIdsByConsoleConnection().entrySet()) {
|
for (var entry : conletIdsByConsoleConnection().entrySet()) {
|
||||||
var connection = entry.getKey();
|
var connection = entry.getKey();
|
||||||
|
|
@ -502,12 +502,12 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
|
|
||||||
// Handle command for selected VM
|
// Handle command for selected VM
|
||||||
var both = Optional.ofNullable(model.vmName())
|
var both = Optional.ofNullable(model.vmName())
|
||||||
.flatMap(vm -> channelManager.both(vm));
|
.flatMap(vm -> channelTracker.value(vm));
|
||||||
if (both.isEmpty()) {
|
if (both.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var vmChannel = both.get().channel;
|
var vmChannel = both.get().channel();
|
||||||
var vmDef = both.get().associated;
|
var vmDef = both.get().associated();
|
||||||
var vmName = vmDef.metadata().getName();
|
var vmName = vmDef.metadata().getName();
|
||||||
var perms = permissions(vmDef, channel.session());
|
var perms = permissions(vmDef, channel.session());
|
||||||
var resourceBundle = resourceBundle(channel.locale());
|
var resourceBundle = resourceBundle(channel.locale());
|
||||||
|
|
@ -556,7 +556,7 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
|
|
||||||
private void openConsole(String vmName, ConsoleConnection connection,
|
private void openConsole(String vmName, ConsoleConnection connection,
|
||||||
ViewerModel model, String password) {
|
ViewerModel model, String password) {
|
||||||
var vmDef = channelManager.associated(vmName).orElse(null);
|
var vmDef = channelTracker.associated(vmName).orElse(null);
|
||||||
if (vmDef == null) {
|
if (vmDef == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,16 +61,20 @@ spec:
|
||||||
## Pod management
|
## Pod management
|
||||||
|
|
||||||
The central resource created by the controller is a
|
The central resource created by the controller is a
|
||||||
[stateful set](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/)
|
[`Pod`](https://kubernetes.io/docs/concepts/workloads/pods/)
|
||||||
with the same name as the VM (metadata.name). Its number of replicas is
|
with the same name as the VM (`metadata.name`). The pod is created only
|
||||||
set to 1 if `spec.vm.state` is "Running" (default is "Stopped" which sets
|
if `spec.vm.state` is "Running" (default is "Stopped" which deletes the
|
||||||
replicas to 0).
|
pod)[^oldSts].
|
||||||
|
|
||||||
Property `spec.guestShutdownStops` (since 2.2.0) controls the effect of a
|
Property `spec.guestShutdownStops` (since 2.2.0) controls the effect of a
|
||||||
shutdown initiated by the guest. If set to `false` (default) a new pod
|
shutdown initiated by the guest. If set to `false` (default) the pod
|
||||||
is automatically created by the stateful set controller and the VM thus
|
and thus the VM is automatically restarted. If set to `true`, the
|
||||||
restarted. If set to `true`, the runner sets `spec.vm.state` to "Stopped"
|
VM's state is set to "Stopped" when the VM terminates and the pod is
|
||||||
before terminating and by this prevents the creation of a new pod.
|
deleted.
|
||||||
|
|
||||||
|
[^oldSts]: Before version 3.4, the operator created a
|
||||||
|
[stateful set](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/)
|
||||||
|
that in turn created the pod and the PVCs (see below).
|
||||||
|
|
||||||
## Defining the basics
|
## Defining the basics
|
||||||
|
|
||||||
|
|
@ -84,7 +88,8 @@ running VMs.
|
||||||
Maybe the most interesting part is the definition of the VM's disks.
|
Maybe the most interesting part is the definition of the VM's disks.
|
||||||
This is done by adding one or more `volumeClaimTemplate`s to the
|
This is done by adding one or more `volumeClaimTemplate`s to the
|
||||||
list of disks. As its name suggests, such a template is used by the
|
list of disks. As its name suggests, such a template is used by the
|
||||||
controller to generate a PVC.
|
controller to generate a
|
||||||
|
[`PVC`](https://kubernetes.io/docs/concepts/storage/persistent-volumes/).
|
||||||
|
|
||||||
The example template does not define any storage. Rather it references
|
The example template does not define any storage. Rather it references
|
||||||
some PV that you must have created first. This may be your first approach
|
some PV that you must have created first. This may be your first approach
|
||||||
|
|
@ -110,24 +115,28 @@ as shown in this example:
|
||||||
The disk will be available as "/dev/*name*-disk" in the VM,
|
The disk will be available as "/dev/*name*-disk" in the VM,
|
||||||
using the string from `.volumeClaimTemplate.metadata.name` as *name*.
|
using the string from `.volumeClaimTemplate.metadata.name` as *name*.
|
||||||
If no name is defined in the metadata, then "/dev/disk-*n*"
|
If no name is defined in the metadata, then "/dev/disk-*n*"
|
||||||
is used instead, with *n* being the index of the disk
|
is used instead, with *n* being the index of the volume claim
|
||||||
definition in the list of disks.
|
template in the list of disks.
|
||||||
|
|
||||||
Apart from appending "-disk" to the name (or generating the name) the
|
The name of the generated PVC is the VM's name with "-*name*-disk"
|
||||||
`volumeClaimTemplate` is simply copied into the stateful set definition
|
(or the generated name) appended: "*vmName*-*name*-disk"
|
||||||
for the VM (with some additional labels, see below). The controller
|
(or "*vmName*-disk-*n*"). The definition of the PVC is simply a copy
|
||||||
for stateful sets appends the started pod's name to the name of the
|
of the information from the `volumeClaimTemplate` (with some additional
|
||||||
volume claim templates when it creates the PVCs. Therefore you'll
|
labels, see below)[^oldStsDisks].
|
||||||
eventually find the PVCs as "*name*-disk-*vmName*-0"
|
|
||||||
(or "disk-*n*-*vmName*-0").
|
|
||||||
|
|
||||||
PVCs generated from stateful set definitions are considered "precious"
|
[^oldStsDisks]: Before version 3.4 the `volumeClaimTemplate`s were
|
||||||
and never removed automatically. This behavior fits perfectly for VMs.
|
copied in the definition of the stateful set. As a stateful set
|
||||||
Usually, you do not want the disks to be removed automatically when
|
appends the started pod's name to the name of the volume claim
|
||||||
you (maybe accidentally) remove the CR for the VM. To simplify the lookup
|
templates when it creates the PVCs, the PVCs' name were
|
||||||
for an eventual (manual) removal, all PVCs are labeled with
|
"*name*-disk-*vmName*-0" (or "disk-*n*-*vmName*-0").
|
||||||
"app.kubernetes.io/name: vm-runner", "app.kubernetes.io/instance: *vmName*",
|
|
||||||
and "app.kubernetes.io/managed-by: vm-operator".
|
PVCs are never removed automatically. Usually, you do not want your
|
||||||
|
VMs disks to be removed when you (maybe accidentally) remove the CR
|
||||||
|
for the VM. To simplify the lookup for an eventual (manual) removal,
|
||||||
|
all PVCs are labeled with "app.kubernetes.io/name: vm-runner",
|
||||||
|
"app.kubernetes.io/instance: *vmName*", and
|
||||||
|
"app.kubernetes.io/managed-by: vm-operator", making it easy to select
|
||||||
|
the PVCs by label in a delete command.
|
||||||
|
|
||||||
## Choosing an image for the runner
|
## Choosing an image for the runner
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,25 @@ layout: vm-operator
|
||||||
|
|
||||||
# Upgrading
|
# Upgrading
|
||||||
|
|
||||||
|
## To version 3.4.0
|
||||||
|
|
||||||
|
Starting with this version, the VM-Operator no longer uses a stateful set
|
||||||
|
with replica set to 1 to (indirectly) start the pod with the VM. Rather
|
||||||
|
it creates the pod directly. This implies that the PVCs must also be created
|
||||||
|
by the VM-Operator, which needs additional permissions to do so (update of
|
||||||
|
`deploy/vmop-role.yaml). As it would be ridiculous to keep the naming scheme
|
||||||
|
used by the stateful set when generating PVCs, the VM-Operator uses a
|
||||||
|
[different pattern](controller.html#defining-disks) for creating new PVCs.
|
||||||
|
|
||||||
|
The change is backward compatible:
|
||||||
|
|
||||||
|
* Running pods created by a stateful set are left alone until stopped.
|
||||||
|
Only then will the stateful set be removed.
|
||||||
|
|
||||||
|
* The VM-Operator looks for existing PVCs generated by a stateful
|
||||||
|
set in the pre 3.4 versions (naming pattern "*name*-disk-*vmName*-0")
|
||||||
|
and reuses them. Only new PVCs are generated using the new pattern.
|
||||||
|
|
||||||
## To version 3.0.0
|
## To version 3.0.0
|
||||||
|
|
||||||
All configuration files are backward compatible to version 2.3.0.
|
All configuration files are backward compatible to version 2.3.0.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue