Feature/status (#5)

Update CR's status (condition Running, current cpu and ram allocation).
This commit is contained in:
Michael N. Lipp 2023-09-16 12:15:23 +02:00 committed by GitHub
commit d8882359a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 776 additions and 115 deletions

View file

@ -2,37 +2,41 @@
<projectDescription> <projectDescription>
<name>VM-Operator</name> <name>VM-Operator</name>
<comment></comment> <comment></comment>
<projects/> <projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>net.sf.eclipsecs.core.CheckstyleBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>ch.acanda.eclipse.pmd.builder.PMDBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures> <natures>
<nature>org.eclipse.jdt.core.javanature</nature> <nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature> <nature>org.eclipse.buildship.core.gradleprojectnature</nature>
<nature>net.sf.eclipsecs.core.CheckstyleNature</nature> <nature>net.sf.eclipsecs.core.CheckstyleNature</nature>
<nature>ch.acanda.eclipse.pmd.builder.PMDNature</nature> <nature>ch.acanda.eclipse.pmd.builder.PMDNature</nature>
</natures> </natures>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments/>
</buildCommand>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments/>
</buildCommand>
<buildCommand>
<name>net.sf.eclipsecs.core.CheckstyleBuilder</name>
<arguments/>
</buildCommand>
<buildCommand>
<name>ch.acanda.eclipse.pmd.builder.PMDBuilder</name>
<arguments/>
</buildCommand>
</buildSpec>
<linkedResources/>
<filteredResources> <filteredResources>
<filter> <filter>
<id>1</id> <id>1</id>
<name></name>
<type>30</type> <type>30</type>
<name/>
<matcher> <matcher>
<id>org.eclipse.core.resources.regexFilterMatcher</id> <id>org.eclipse.core.resources.regexFilterMatcher</id>
<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments> <arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>

View file

@ -5,7 +5,7 @@ connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
connection.project.dir= connection.project.dir=
eclipse.preferences.version=1 eclipse.preferences.version=1
gradle.user.home= gradle.user.home=
java.home=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.362.b09-2.fc37.x86_64 java.home=
jvm.arguments= jvm.arguments=
offline.mode=false offline.mode=false
override.workspace.settings=true override.workspace.settings=true

View file

@ -5,7 +5,20 @@
<projects> <projects>
</projects> </projects>
<buildSpec> <buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec> </buildSpec>
<natures> <natures>
<nature>org.eclipse.jdt.groovy.core.groovyNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures> </natures>
</projectDescription> </projectDescription>

View file

@ -0,0 +1,2 @@
eclipse.preferences.version=1
encoding/<project>=UTF-8

View file

@ -0,0 +1,2 @@
eclipse.preferences.version=1
line.separator=\n

View file

@ -1,24 +1,22 @@
#
#Sun May 07 20:03:15 CEST 2023
org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.nullReference=warning
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
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.methodParameters=do not generate
org.eclipse.jdt.core.compiler.codegen.targetPlatform=21
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=21
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled 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.annotation.nullable=org.eclipse.jdt.annotation.Nullable
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 org.eclipse.jdt.core.compiler.problem.nullReference=warning
org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore
org.eclipse.jdt.core.compiler.source=17
org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull
org.eclipse.jdt.core.compiler.release=disabled
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled org.eclipse.jdt.core.compiler.release=disabled
org.eclipse.jdt.core.compiler.debug.lineNumber=generate org.eclipse.jdt.core.compiler.source=21
org.eclipse.jdt.core.compiler.compliance=17

View file

@ -9,6 +9,9 @@ plugins {
// are build scripts in 'src/main' that automatically become available // are build scripts in 'src/main' that automatically become available
// as plugins in the main build. // as plugins in the main build.
id 'groovy-gradle-plugin' id 'groovy-gradle-plugin'
// Apply eclipse plugin
id 'eclipse'
} }
repositories { repositories {
@ -30,10 +33,35 @@ sourceSets {
} }
} }
/* eclipse {
java {
toolchain { project {
languageVersion = JavaLanguageVersion.of(17) 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'
project.buildCommand 'org.eclipse.buildship.core.gradleprojectbuilder'
}
}
classpath {
downloadJavadoc = true
downloadSources = true
}
jdt {
file {
withProperties { properties ->
def formatterPrefs = new Properties()
rootProject.file("gradle/org.eclipse.jdt.core.formatter.prefs")
.withInputStream { formatterPrefs.load(it) }
properties.putAll(formatterPrefs)
}
}
} }
} }
*/

View file

@ -9,6 +9,8 @@ spec:
- name: v1 - name: v1
served: true served: true
storage: true storage: true
subresources:
status: {}
schema: schema:
openAPIV3Schema: openAPIV3Schema:
type: object type: object
@ -1372,10 +1374,28 @@ spec:
- vm - vm
status: status:
type: object type: object
default: {}
properties: properties:
cpus:
description: >-
Number of CPUs currently in use.
type: integer
default: 0
ram:
description: >-
Amount of memory in use.
type: string
default: "0"
conditions: conditions:
description: >- description: >-
List of component conditions observed List of component conditions observed
default:
- type: Running
status: "False"
observedGeneration: 1
lastTransitionTime: "1970-01-01T00:00:00Z"
reason: Creation
message: "Creation of CR"
type: array type: array
items: items:
type: object type: object
@ -1383,6 +1403,12 @@ spec:
Information about the condition of a component. See Information about the condition of a component. See
https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#spec-and-status https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
and https://github.com/kubernetes/apimachinery/blob/release-1.23/pkg/apis/meta/v1/types.go#L1432-L1492 and https://github.com/kubernetes/apimachinery/blob/release-1.23/pkg/apis/meta/v1/types.go#L1432-L1492
required:
- type
- status
- lastTransitionTime
- reason
- message
properties: properties:
type: type:
type: string type: string
@ -1428,12 +1454,6 @@ spec:
Message is a human readable message indicating Message is a human readable message indicating
details about the transition. This may be an empty string. details about the transition. This may be an empty string.
default: "" default: ""
required:
- type
- status
- lastTransitionTime
- reason
- message
# either Namespaced or Cluster # either Namespaced or Cluster
scope: Namespaced scope: Namespaced
names: names:

View file

@ -8,3 +8,6 @@ resources:
- vmop-image-repository-pvc.yaml - vmop-image-repository-pvc.yaml
- vmop-config-map.yaml - vmop-config-map.yaml
- vmop-deployment.yaml - vmop-deployment.yaml
- vmrunner-role.yaml
- vmrunner-service-account.yaml
- vmrunner-role-binding.yaml

View file

@ -0,0 +1,13 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: vm-runner
labels:
app.kubernetes.io/name: vm-operator
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: vm-runner
subjects:
- kind: ServiceAccount
name: vm-runner

20
deploy/vmrunner-role.yaml Normal file
View file

@ -0,0 +1,20 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: vm-runner
labels:
app.kubernetes.io/name: vm-operator
rules:
- apiGroups:
- vmoperator.jdrupes.org
resources:
- vms
verbs:
- list
- get
- apiGroups:
- vmoperator.jdrupes.org
resources:
- vms/status
verbs:
- patch

View file

@ -0,0 +1,6 @@
kind: ServiceAccount
apiVersion: v1
metadata:
name: vm-runner
labels:
app.kubernetes.io/name: vm-operator

View file

@ -16,8 +16,6 @@ dependencies {
implementation project(':org.jdrupes.vmoperator.util') implementation project(':org.jdrupes.vmoperator.util')
implementation 'commons-cli:commons-cli:1.5.0' implementation 'commons-cli:commons-cli:1.5.0'
implementation 'org.freemarker:freemarker:[2.3.32,2.4)'
implementation 'io.kubernetes:client-java:[18.0.0,19)'
runtimeOnly 'com.electronwill.night-config:yaml:[3.6.7,3.7)' runtimeOnly 'com.electronwill.night-config:yaml:[3.6.7,3.7)'
runtimeOnly 'org.slf4j:slf4j-jdk14:[2.0.7,3)' runtimeOnly 'org.slf4j:slf4j-jdk14:[2.0.7,3)'

View file

@ -140,6 +140,7 @@ spec:
<#if cr.spec.affinity??> <#if cr.spec.affinity??>
affinity: ${ cr.spec.affinity.toString() } affinity: ${ cr.spec.affinity.toString() }
</#if> </#if>
serviceAccountName: vm-runner
volumeClaimTemplates: volumeClaimTemplates:
- metadata: - metadata:
namespace: ${ cr.metadata.namespace.asString } namespace: ${ cr.metadata.namespace.asString }

View file

@ -32,6 +32,9 @@ import java.io.IOException;
import java.io.StringWriter; import java.io.StringWriter;
import java.util.Map; import java.util.Map;
import java.util.logging.Logger; import java.util.logging.Logger;
import static org.jdrupes.vmoperator.manager.Constants.APP_NAME;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME;
import org.jdrupes.vmoperator.util.K8s;
import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor; import org.yaml.snakeyaml.constructor.SafeConstructor;
@ -98,8 +101,8 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
DynamicKubernetesObject newCm) { DynamicKubernetesObject newCm) {
ListOptions listOpts = new ListOptions(); ListOptions listOpts = new ListOptions();
listOpts.setLabelSelector( listOpts.setLabelSelector(
"app.kubernetes.io/managed-by=" + Constants.VM_OP_NAME + "," "app.kubernetes.io/managed-by=" + VM_OP_NAME + ","
+ "app.kubernetes.io/name=" + Constants.APP_NAME); + "app.kubernetes.io/name=" + APP_NAME);
// Get pod, selected by label // Get pod, selected by label
var podApi = new DynamicKubernetesApi("", "v1", "pods", client); var podApi = new DynamicKubernetesApi("", "v1", "pods", client);
var pods = podApi var pods = podApi

View file

@ -21,16 +21,7 @@ package org.jdrupes.vmoperator.manager;
/** /**
* Some constants. * Some constants.
*/ */
public class Constants { public class Constants extends org.jdrupes.vmoperator.util.Constants {
/** The Constant VM_OP_NAME. */
public static final String VM_OP_NAME = "vm-operator";
/** The Constant VM_OP_GROUP. */
public static final String VM_OP_GROUP = "vmoperator.jdrupes.org";
/** The Constant VM_OP_KIND_VM. */
public static final String VM_OP_KIND_VM = "VirtualMachine";
/** The Constant APP_NAME. */ /** The Constant APP_NAME. */
public static final String APP_NAME = "vm-runner"; public static final String APP_NAME = "vm-runner";

View file

@ -21,9 +21,11 @@ package org.jdrupes.vmoperator.manager;
import io.kubernetes.client.openapi.ApiException; import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.Configuration; import io.kubernetes.client.openapi.Configuration;
import java.io.IOException; import java.io.IOException;
import java.util.logging.Level;
import org.jgrapes.core.Channel; import org.jgrapes.core.Channel;
import org.jgrapes.core.Component; import org.jgrapes.core.Component;
import org.jgrapes.core.annotation.Handler; import org.jgrapes.core.annotation.Handler;
import org.jgrapes.core.events.HandlingError;
import org.jgrapes.core.events.Start; import org.jgrapes.core.events.Start;
/** /**
@ -74,6 +76,20 @@ public class Controller extends Component {
attach(new Reconciler(channel())); attach(new Reconciler(channel()));
} }
/**
* Special handling of {@link ApiException} thrown by handlers.
*
* @param event the event
*/
@Handler(channels = Channel.class)
public void onHandlingError(HandlingError event) {
if (event.throwable() instanceof ApiException exc) {
logger.log(Level.WARNING, exc,
() -> "Problem accessing kubernetes: " + exc.getResponseBody());
event.stop();
}
}
/** /**
* Handle the start event. Has higher priority because it configures * Handle the start event. Has higher priority because it configures
* the default Kubernetes client. * the default Kubernetes client.

View file

@ -33,6 +33,8 @@ import java.util.Collections;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.logging.Logger; import java.util.logging.Logger;
import org.jdrupes.vmoperator.util.GsonPtr;
import org.jdrupes.vmoperator.util.K8s;
import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor; import org.yaml.snakeyaml.constructor.SafeConstructor;

View file

@ -32,9 +32,11 @@ import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options; import org.apache.commons.cli.Options;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME; import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME;
import org.jdrupes.vmoperator.util.FsdUtils; import org.jdrupes.vmoperator.util.FsdUtils;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Component; import org.jgrapes.core.Component;
import org.jgrapes.core.Components; import org.jgrapes.core.Components;
import org.jgrapes.core.annotation.Handler; import org.jgrapes.core.annotation.Handler;
import org.jgrapes.core.events.HandlingError;
import org.jgrapes.core.events.Stop; import org.jgrapes.core.events.Stop;
import org.jgrapes.io.NioDispatcher; import org.jgrapes.io.NioDispatcher;
import org.jgrapes.util.FileSystemWatcher; import org.jgrapes.util.FileSystemWatcher;
@ -95,6 +97,20 @@ public class Manager extends Component {
fire(new WatchFile(config.toPath())); fire(new WatchFile(config.toPath()));
} }
/**
* Log the exception when a handling error is reported.
*
* @param event the event
*/
@Handler(channels = Channel.class, priority = -10_000)
@SuppressWarnings("PMD.GuardLogStatement")
public void onHandlingError(HandlingError event) {
logger.log(Level.WARNING, event.throwable(),
() -> "Problem invoking handler with " + event.event() + ": "
+ event.message());
event.stop();
}
/** /**
* On stop. * On stop.
* *

View file

@ -47,6 +47,8 @@ import static org.jdrupes.vmoperator.manager.Constants.VM_OP_GROUP;
import org.jdrupes.vmoperator.manager.VmDefChanged.Type; import org.jdrupes.vmoperator.manager.VmDefChanged.Type;
import org.jdrupes.vmoperator.util.Convertions; import org.jdrupes.vmoperator.util.Convertions;
import org.jdrupes.vmoperator.util.ExtendedObjectWrapper; import org.jdrupes.vmoperator.util.ExtendedObjectWrapper;
import org.jdrupes.vmoperator.util.GsonPtr;
import org.jdrupes.vmoperator.util.K8s;
import org.jgrapes.core.Channel; import org.jgrapes.core.Channel;
import org.jgrapes.core.Component; import org.jgrapes.core.Component;
import org.jgrapes.core.annotation.Handler; import org.jgrapes.core.annotation.Handler;

View file

@ -29,6 +29,8 @@ import java.io.IOException;
import java.io.StringWriter; import java.io.StringWriter;
import java.util.Map; import java.util.Map;
import java.util.logging.Logger; import java.util.logging.Logger;
import org.jdrupes.vmoperator.util.GsonPtr;
import org.jdrupes.vmoperator.util.K8s;
import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor; import org.yaml.snakeyaml.constructor.SafeConstructor;

View file

@ -44,8 +44,10 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level; import java.util.logging.Level;
import static org.jdrupes.vmoperator.manager.Constants.APP_NAME;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_GROUP; import static org.jdrupes.vmoperator.manager.Constants.VM_OP_GROUP;
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 org.jdrupes.vmoperator.manager.VmDefChanged.Type; import org.jdrupes.vmoperator.manager.VmDefChanged.Type;
import org.jgrapes.core.Channel; import org.jgrapes.core.Channel;
import org.jgrapes.core.Component; import org.jgrapes.core.Component;
@ -132,7 +134,7 @@ public class VmWatcher extends Component {
for (var version : vmOpApiVersions) { for (var version : vmOpApiVersions) {
coa.getAPIResources(VM_OP_GROUP, version) coa.getAPIResources(VM_OP_GROUP, version)
.getResources().stream() .getResources().stream()
.filter(r -> Constants.VM_OP_KIND_VM.equals(r.getKind())) .filter(r -> VM_OP_KIND_VM.equals(r.getKind()))
.findFirst() .findFirst()
.ifPresent(crd -> watchVmDefs(crd, version)); .ifPresent(crd -> watchVmDefs(crd, version));
} }
@ -148,15 +150,15 @@ public class VmWatcher extends Component {
// Get all known CR instances. // Get all known CR instances.
coa.getAPIResources(VM_OP_GROUP, version) coa.getAPIResources(VM_OP_GROUP, version)
.getResources().stream() .getResources().stream()
.filter(r -> Constants.VM_OP_KIND_VM.equals(r.getKind())) .filter(r -> VM_OP_KIND_VM.equals(r.getKind()))
.findFirst() .findFirst()
.ifPresent(crd -> known.addAll(getKnown(client, crd, version))); .ifPresent(crd -> known.addAll(getKnown(client, crd, version)));
} }
ListOptions opts = new ListOptions(); ListOptions opts = new ListOptions();
opts.setLabelSelector( opts.setLabelSelector(
"app.kubernetes.io/managed-by=" + Constants.VM_OP_NAME + "," "app.kubernetes.io/managed-by=" + VM_OP_NAME + ","
+ "app.kubernetes.io/name=" + Constants.APP_NAME); + "app.kubernetes.io/name=" + APP_NAME);
for (String resource : List.of("apps/v1/statefulsets", for (String resource : List.of("apps/v1/statefulsets",
"v1/configmaps", "v1/secrets")) { "v1/configmaps", "v1/secrets")) {
var resParts = new LinkedList<>(List.of(resource.split("/"))); var resParts = new LinkedList<>(List.of(resource.split("/")));

View file

@ -16,8 +16,9 @@ dependencies {
implementation project(':org.jdrupes.vmoperator.util') implementation project(':org.jdrupes.vmoperator.util')
implementation 'commons-cli:commons-cli:1.5.0' implementation 'commons-cli:commons-cli:1.5.0'
implementation 'org.freemarker:freemarker:[2.3.32,2.4)'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:[2.15.1,3]' implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:[2.15.1,3]'
runtimeOnly 'org.slf4j:slf4j-jdk14:[2.0.7,3)'
} }
application { application {

View file

@ -22,6 +22,11 @@
# the first time. Subsequent starts use the copy unless this option is set. # the first time. Subsequent starts use the copy unless this option is set.
# "updateTemplate": false # "updateTemplate": false
# The namespace that this runner runs in. Usually obtained from
# /var/run/secrets/kubernetes.io/serviceaccount/namespace. Should only
# be set when starting the runner during development e.g. from the IDE.
# "namespace": ...
# Define the VM (required) # Define the VM (required)
"vm": "vm":
# The VM's name (required) # The VM's name (required)

View file

@ -19,8 +19,8 @@
package org.jdrupes.vmoperator.runner.qemu; package org.jdrupes.vmoperator.runner.qemu;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
@ -81,34 +81,28 @@ public class CpuController extends Component {
/** /**
* On monitor result. * On monitor result.
* *
* @param result the result * @param event the result
*/ */
@Handler @Handler
public void onHotpluggableCpuStatus(HotpluggableCpuStatus result) { public void onHotpluggableCpuStatus(HotpluggableCpuStatus event) {
if (!result.successful()) { if (!event.successful()) {
logger.warning(() -> "Failed to get hotpluggable CPU status " logger.warning(() -> "Failed to get hotpluggable CPU status "
+ "(won't adjust number of CPUs.): " + result.errorMessage()); + "(won't adjust number of CPUs.): " + event.errorMessage());
} }
// Sort
List<ObjectNode> used = new ArrayList<>();
List<ObjectNode> unused = new ArrayList<>();
for (var itr = result.values().iterator(); itr.hasNext();) {
ObjectNode cpu = (ObjectNode) itr.next();
if (cpu.has("qom-path")) {
used.add(cpu);
} else {
unused.add(cpu);
}
}
currentCpus = used.size();
if (desiredCpus == null) { if (desiredCpus == null) {
return; return;
} }
// Process // Process
int diff = used.size() - desiredCpus; currentCpus = event.usedCpus().size();
diff = addCpus(used, unused, diff); int diff = currentCpus - desiredCpus;
deleteCpus(used, diff); if (diff == 0) {
return;
}
diff = addCpus(event.usedCpus(), event.unusedCpus(), diff);
removeCpus(event.usedCpus(), diff);
// Report result
fire(new MonitorCommand(new QmpQueryHotpluggableCpus()));
} }
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
@ -123,22 +117,24 @@ public class CpuController extends Component {
} }
} }
int nextId = 1; int nextId = 1;
while (diff < 0 && !unused.isEmpty()) { List<ObjectNode> remaining = new LinkedList<>(unused);
while (diff < 0 && !remaining.isEmpty()) {
String id; String id;
do { do {
id = "cpu-" + nextId++; id = "cpu-" + nextId++;
} while (usedIds.contains(id)); } while (usedIds.contains(id));
fire(new MonitorCommand(new QmpAddCpu(unused.get(0), id))); fire(new MonitorCommand(new QmpAddCpu(remaining.get(0), id)));
unused.remove(0); remaining.remove(0);
diff += 1; diff += 1;
} }
return diff; return diff;
} }
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
private int deleteCpus(List<ObjectNode> used, int diff) { private int removeCpus(List<ObjectNode> used, int diff) {
while (diff > 0 && !used.isEmpty()) { List<ObjectNode> removable = new LinkedList<>(used);
ObjectNode cpu = used.remove(0); while (diff > 0 && !removable.isEmpty()) {
ObjectNode cpu = removable.remove(0);
String qomPath = cpu.get("qom-path").asText(); String qomPath = cpu.get("qom-path").asText();
if (!qomPath.startsWith("/machine/peripheral/cpu-")) { if (!qomPath.startsWith("/machine/peripheral/cpu-")) {
continue; continue;

View file

@ -58,14 +58,17 @@ import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State; import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State;
import org.jdrupes.vmoperator.util.ExtendedObjectWrapper; import org.jdrupes.vmoperator.util.ExtendedObjectWrapper;
import org.jdrupes.vmoperator.util.FsdUtils; import org.jdrupes.vmoperator.util.FsdUtils;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Component; import org.jgrapes.core.Component;
import org.jgrapes.core.Components; import org.jgrapes.core.Components;
import org.jgrapes.core.EventPipeline; import org.jgrapes.core.EventPipeline;
import org.jgrapes.core.TypedIdKey; import org.jgrapes.core.TypedIdKey;
import org.jgrapes.core.annotation.Handler; import org.jgrapes.core.annotation.Handler;
import org.jgrapes.core.events.HandlingError;
import org.jgrapes.core.events.Start; import org.jgrapes.core.events.Start;
import org.jgrapes.core.events.Started; import org.jgrapes.core.events.Started;
import org.jgrapes.core.events.Stop; import org.jgrapes.core.events.Stop;
import org.jgrapes.core.internal.EventProcessor;
import org.jgrapes.io.NioDispatcher; import org.jgrapes.io.NioDispatcher;
import org.jgrapes.io.events.Input; import org.jgrapes.io.events.Input;
import org.jgrapes.io.events.ProcessExited; import org.jgrapes.io.events.ProcessExited;
@ -90,6 +93,13 @@ import org.jgrapes.util.events.WatchFile;
* *
* ![Runner state diagram](RunnerStates.svg) * ![Runner state diagram](RunnerStates.svg)
* *
* The {@link Runner} associates an {@link EventProcessor} with the
* {@link Start} event. This "runner event processor" must be used
* for all events related to the application level function. Components
* that handle events from other sources (and thus event processors)
* must fire any resulting events on the runner event processor in order
* to maintain synchronization.
*
* @startuml RunnerStates.svg * @startuml RunnerStates.svg
* [*] --> Initializing * [*] --> Initializing
* Initializing -> Initializing: InitialConfiguration/configure Runner * Initializing -> Initializing: InitialConfiguration/configure Runner
@ -149,8 +159,13 @@ import org.jgrapes.util.events.WatchFile;
* error --> terminate * error --> terminate
* StartingProcess --> terminate: ProcessExited * StartingProcess --> terminate: ProcessExited
* *
* state Stopped {
* state stopped <<entryPoint>>
* *
* terminated --> [*] * stopped --> [*]
* }
*
* terminated --> stopped
* *
* @enduml * @enduml
* *
@ -211,6 +226,7 @@ public class Runner extends Component {
attach(new ProcessManager(channel())); attach(new ProcessManager(channel()));
attach(new SocketConnector(channel())); attach(new SocketConnector(channel()));
attach(qemuMonitor = new QemuMonitor(channel())); attach(qemuMonitor = new QemuMonitor(channel()));
attach(new StatusUpdater(channel()));
// Configuration store with file in /etc/opt (default) // Configuration store with file in /etc/opt (default)
File config = new File(cmdLine.getOptionValue('c', File config = new File(cmdLine.getOptionValue('c',
@ -224,6 +240,20 @@ public class Runner extends Component {
fire(new WatchFile(config.toPath())); fire(new WatchFile(config.toPath()));
} }
/**
* Log the exception when a handling error is reported.
*
* @param event the event
*/
@Handler(channels = Channel.class, priority = -10_000)
@SuppressWarnings("PMD.GuardLogStatement")
public void onHandlingError(HandlingError event) {
logger.log(Level.WARNING, event.throwable(),
() -> "Problem invoking handler with " + event.event() + ": "
+ event.message());
event.stop();
}
/** /**
* On configuration update. * On configuration update.
* *
@ -352,6 +382,11 @@ public class Runner extends Component {
return; return;
} }
// Make sure to use thread specific client
// https://github.com/kubernetes-client/java/issues/100
io.kubernetes.client.openapi.Configuration.setDefaultApiClient(null);
// Prepare specific event pipeline to avoid concurrency.
rep = newEventPipeline(); rep = newEventPipeline();
event.setAssociated(EventPipeline.class, rep); event.setAssociated(EventPipeline.class, rep);
try { try {
@ -387,7 +422,8 @@ public class Runner extends Component {
@Handler @Handler
public void onStarted(Started event) { public void onStarted(Started event) {
state = State.STARTING; state = State.STARTING;
fire(new RunnerStateChange(state)); rep.fire(new RunnerStateChange(state, "RunnerStarted",
"Runner has been started"));
// Start first process // Start first process
if (config.vm.useTpm && swtpmDefinition != null) { if (config.vm.useTpm && swtpmDefinition != null) {
startProcess(swtpmDefinition); startProcess(swtpmDefinition);
@ -476,7 +512,7 @@ public class Runner extends Component {
*/ */
@Handler @Handler
public void onMonitorReady(MonitorReady event) { public void onMonitorReady(MonitorReady event) {
fire(new RunnerConfigurationUpdate(config, state)); rep.fire(new RunnerConfigurationUpdate(config, state));
} }
/** /**
@ -489,7 +525,8 @@ public class Runner extends Component {
if (state == State.STARTING) { if (state == State.STARTING) {
fire(new MonitorCommand(new QmpCont())); fire(new MonitorCommand(new QmpCont()));
state = State.RUNNING; state = State.RUNNING;
fire(new RunnerStateChange(state)); rep.fire(new RunnerStateChange(state, "VmStarted",
"Qemu has been configured and is continuing"));
} }
} }
@ -524,9 +561,22 @@ public class Runner extends Component {
* @param event the event * @param event the event
*/ */
@Handler(priority = 10_000) @Handler(priority = 10_000)
public void onStop(Stop event) { public void onStopFirst(Stop event) {
state = State.TERMINATING; state = State.TERMINATING;
fire(new RunnerStateChange(state)); rep.fire(new RunnerStateChange(state, "VmTerminating",
"The VM is being shut down"));
}
/**
* On stop.
*
* @param event the event
*/
@Handler(priority = -10_000)
public void onStopLast(Stop event) {
state = State.STOPPED;
rep.fire(new RunnerStateChange(state, "VmStopped",
"The VM has been shut down"));
} }
private void shutdown() { private void shutdown() {

View file

@ -0,0 +1,304 @@
/*
* 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.runner.qemu;
import com.google.gson.JsonObject;
import io.kubernetes.client.custom.Quantity;
import io.kubernetes.client.custom.Quantity.Format;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.apis.ApisApi;
import io.kubernetes.client.openapi.apis.CustomObjectsApi;
import io.kubernetes.client.openapi.models.V1APIGroup;
import io.kubernetes.client.openapi.models.V1GroupVersionForDiscovery;
import io.kubernetes.client.util.Config;
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi;
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.logging.Level;
import org.jdrupes.vmoperator.runner.qemu.events.BalloonChangeEvent;
import org.jdrupes.vmoperator.runner.qemu.events.HotpluggableCpuStatus;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerConfigurationUpdate;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State;
import static org.jdrupes.vmoperator.util.Constants.VM_OP_GROUP;
import static org.jdrupes.vmoperator.util.Constants.VM_OP_KIND_VM;
import org.jdrupes.vmoperator.util.GsonPtr;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Component;
import org.jgrapes.core.annotation.Handler;
import org.jgrapes.core.events.HandlingError;
import org.jgrapes.core.events.Start;
import org.jgrapes.util.events.ConfigurationUpdate;
import org.jgrapes.util.events.InitialConfiguration;
/**
* Updates the CR status.
*/
public class StatusUpdater extends Component {
private static final Set<State> RUNNING_STATES
= Set.of(State.RUNNING, State.TERMINATING);
private String namespace;
private String vmName;
private DynamicKubernetesApi vmCrApi;
private long observedGeneration;
/**
* Instantiates a new status updater.
*
* @param componentChannel the component channel
*/
public StatusUpdater(Channel componentChannel) {
super(componentChannel);
}
/**
* On handling error.
*
* @param event the event
*/
@Handler(channels = Channel.class)
public void onHandlingError(HandlingError event) {
if (event.throwable() instanceof ApiException exc) {
logger.log(Level.WARNING, exc,
() -> "Problem accessing kubernetes: " + exc.getResponseBody());
event.stop();
}
}
/**
* On configuration update.
*
* @param event the event
*/
@Handler
@SuppressWarnings("unchecked")
public void onConfigurationUpdate(ConfigurationUpdate event) {
event.structured("/Runner").ifPresent(c -> {
if (event instanceof InitialConfiguration) {
namespace = (String) c.get("namespace");
updateNamespace();
vmName = Optional.ofNullable((Map<String, String>) c.get("vm"))
.map(vm -> vm.get("name")).orElse(null);
}
});
}
private void updateNamespace() {
if (namespace == null) {
var path = Path
.of("/var/run/secrets/kubernetes.io/serviceaccount/namespace");
if (Files.isReadable(path)) {
try {
namespace = Files.lines(path).findFirst().orElse(null);
} catch (IOException e) {
logger.log(Level.WARNING, e,
() -> "Cannot read namespace.");
}
}
}
if (namespace == null) {
logger.warning(() -> "Namespace is unknown, some functions"
+ " won't be available.");
}
}
/**
* Handle the start event.
*
* @param event the event
* @throws IOException
* @throws ApiException
*/
@Handler
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis",
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.AvoidDuplicateLiterals" })
public void onStart(Start event) throws IOException, ApiException {
var client = Config.defaultClient();
var apis = new ApisApi(client).getAPIVersions();
var crdVersions = apis.getGroups().stream()
.filter(g -> g.getName().equals(VM_OP_GROUP)).findFirst()
.map(V1APIGroup::getVersions).stream().flatMap(l -> l.stream())
.map(V1GroupVersionForDiscovery::getVersion).toList();
var coa = new CustomObjectsApi(client);
for (var crdVersion : crdVersions) {
var crdApiRes = coa.getAPIResources(VM_OP_GROUP,
crdVersion).getResources().stream()
.filter(r -> VM_OP_KIND_VM.equals(r.getKind())).findFirst();
if (crdApiRes.isEmpty()) {
continue;
}
var crApi = new DynamicKubernetesApi(VM_OP_GROUP,
crdVersion, crdApiRes.get().getName(), client);
var vmCr = crApi.get(namespace, vmName).throwsApiException();
if (vmCr.isSuccess()) {
vmCrApi = crApi;
observedGeneration
= vmCr.getObject().getMetadata().getGeneration();
break;
}
}
if (vmCrApi == null) {
logger.warning(() -> "Cannot find VM's CR, status will not"
+ " be updated.");
}
}
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
private JsonObject currentStatus(DynamicKubernetesObject vmCr) {
return vmCr.getRaw().getAsJsonObject("status").deepCopy();
}
/**
* On runner configuration update.
*
* @param event the event
* @throws ApiException
*/
@Handler
public void onRunnerConfigurationUpdate(RunnerConfigurationUpdate event)
throws ApiException {
if (vmCrApi == null) {
return;
}
// A change of the runner configuration is typically caused
// by a new version of the CR. So we observe the new CR.
var vmCr = vmCrApi.get(namespace, vmName).throwsApiException()
.getObject();
if (vmCr.getMetadata().getGeneration() == observedGeneration) {
return;
}
vmCrApi.updateStatus(vmCr, from -> {
JsonObject status = currentStatus(from);
status.getAsJsonArray("conditions").asList().stream()
.map(cond -> (JsonObject) cond).filter(cond -> "Running"
.equals(cond.get("type").getAsString()))
.forEach(cond -> cond.addProperty("observedGeneration",
from.getMetadata().getGeneration()));
return status;
});
}
/**
* On runner state changed.
*
* @param event the event
* @throws ApiException
*/
@Handler
public void onRunnerStateChanged(RunnerStateChange event)
throws ApiException {
if (vmCrApi == null) {
return;
}
var vmCr = vmCrApi.get(namespace, vmName).throwsApiException()
.getObject();
vmCrApi.updateStatus(vmCr, from -> {
JsonObject status = currentStatus(from);
status.getAsJsonArray("conditions").asList().stream()
.map(cond -> (JsonObject) cond)
.forEach(cond -> {
if ("Running".equals(cond.get("type").getAsString())) {
updateRunningCondition(event, from, cond);
}
});
if (event.state() == State.STARTING) {
status.addProperty("ram", GsonPtr.to(from.getRaw())
.getAsString("spec", "vm", "maximumRam").orElse("0"));
status.addProperty("cpus", 1);
} else if (event.state() == State.STOPPED) {
status.addProperty("ram", "0");
status.addProperty("cpus", 0);
}
return status;
}).throwsApiException();
}
private void updateRunningCondition(RunnerStateChange event,
DynamicKubernetesObject from, JsonObject cond) {
boolean reportedRunning
= "True".equals(cond.get("status").getAsString());
if (RUNNING_STATES.contains(event.state())
&& !reportedRunning) {
cond.addProperty("status", "True");
cond.addProperty("lastTransitionTime",
Instant.now().toString());
}
if (!RUNNING_STATES.contains(event.state())
&& reportedRunning) {
cond.addProperty("status", "False");
cond.addProperty("lastTransitionTime",
Instant.now().toString());
}
cond.addProperty("reason", event.reason());
cond.addProperty("message", event.message());
cond.addProperty("observedGeneration",
from.getMetadata().getGeneration());
}
/**
* On ballon change.
*
* @param event the event
* @throws ApiException
*/
@Handler
public void onBallonChange(BalloonChangeEvent event) throws ApiException {
if (vmCrApi == null) {
return;
}
var vmCr = vmCrApi.get(namespace, vmName).throwsApiException()
.getObject();
vmCrApi.updateStatus(vmCr, from -> {
JsonObject status = currentStatus(from);
status.addProperty("ram",
new Quantity(new BigDecimal(event.size()), Format.BINARY_SI)
.toSuffixedString());
return status;
}).throwsApiException();
}
/**
* On ballon change.
*
* @param event the event
* @throws ApiException
*/
@Handler
public void onCpuChange(HotpluggableCpuStatus event) throws ApiException {
if (vmCrApi == null) {
return;
}
var vmCr = vmCrApi.get(namespace, vmName).throwsApiException()
.getObject();
vmCrApi.updateStatus(vmCr, from -> {
JsonObject status = currentStatus(from);
status.addProperty("cpus", event.usedCpus().size());
return status;
}).throwsApiException();
}
}

View file

@ -0,0 +1,47 @@
/*
* 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.runner.qemu.events;
import com.fasterxml.jackson.databind.JsonNode;
import java.math.BigInteger;
/**
* Signals a change of the balloon.
*/
public class BalloonChangeEvent extends MonitorEvent {
/**
* Instantiates a new tray moved.
*
* @param kind the kind
* @param data the data
*/
public BalloonChangeEvent(Kind kind, JsonNode data) {
super(kind, data);
}
/**
* Returns the actual value.
*
* @return the actual value
*/
public BigInteger size() {
return new BigInteger(data().get("actual").asText());
}
}

View file

@ -19,6 +19,10 @@
package org.jdrupes.vmoperator.runner.qemu.events; package org.jdrupes.vmoperator.runner.qemu.events;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.jdrupes.vmoperator.runner.qemu.commands.QmpCommand; import org.jdrupes.vmoperator.runner.qemu.commands.QmpCommand;
/** /**
@ -26,6 +30,9 @@ import org.jdrupes.vmoperator.runner.qemu.commands.QmpCommand;
*/ */
public class HotpluggableCpuStatus extends MonitorResult { public class HotpluggableCpuStatus extends MonitorResult {
private List<ObjectNode> usedCpus = new ArrayList<>();
private List<ObjectNode> unusedCpus = new ArrayList<>();
/** /**
* Instantiates a new hotpluggable cpu result. * Instantiates a new hotpluggable cpu result.
* *
@ -34,6 +41,39 @@ public class HotpluggableCpuStatus extends MonitorResult {
*/ */
public HotpluggableCpuStatus(QmpCommand command, JsonNode response) { public HotpluggableCpuStatus(QmpCommand command, JsonNode response) {
super(command, response); super(command, response);
if (!successful()) {
return;
}
// Sort
for (var itr = values().iterator(); itr.hasNext();) {
ObjectNode cpu = (ObjectNode) itr.next();
if (cpu.has("qom-path")) {
usedCpus.add(cpu);
} else {
unusedCpus.add(cpu);
}
}
usedCpus = Collections.unmodifiableList(usedCpus);
unusedCpus = Collections.unmodifiableList(unusedCpus);
}
/**
* Gets the used cpus.
*
* @return the usedCpus
*/
public List<ObjectNode> usedCpus() {
return usedCpus;
}
/**
* Gets the unused cpus.
*
* @return the unusedCpus
*/
public List<ObjectNode> unusedCpus() {
return unusedCpus;
} }
} }

View file

@ -32,7 +32,7 @@ public class MonitorEvent extends Event<Void> {
* The kind of monitor event. * The kind of monitor event.
*/ */
public enum Kind { public enum Kind {
READY, POWERDOWN, DEVICE_TRAY_MOVED READY, POWERDOWN, DEVICE_TRAY_MOVED, BALLOON_CHANGE
} }
private final Kind kind; private final Kind kind;
@ -55,6 +55,9 @@ public class MonitorEvent extends Event<Void> {
case DEVICE_TRAY_MOVED: case DEVICE_TRAY_MOVED:
return Optional return Optional
.of(new TrayMovedEvent(kind, response.get("data"))); .of(new TrayMovedEvent(kind, response.get("data")));
case BALLOON_CHANGE:
return Optional
.of(new BalloonChangeEvent(kind, response.get("data")));
default: default:
return Optional return Optional
.of(new MonitorEvent(kind, response.get("data"))); .of(new MonitorEvent(kind, response.get("data")));

View file

@ -19,6 +19,7 @@
package org.jdrupes.vmoperator.runner.qemu.events; package org.jdrupes.vmoperator.runner.qemu.events;
import org.jgrapes.core.Channel; import org.jgrapes.core.Channel;
import org.jgrapes.core.Components;
import org.jgrapes.core.Event; import org.jgrapes.core.Event;
/** /**
@ -30,19 +31,24 @@ public class RunnerStateChange extends Event<Void> {
* The state. * The state.
*/ */
public enum State { public enum State {
INITIALIZING, STARTING, RUNNING, TERMINATING INITIALIZING, STARTING, RUNNING, TERMINATING, STOPPED
} }
private final State state; private final State state;
private final String reason;
private final String message;
/** /**
* Instantiates a new runner state change. * Instantiates a new runner state change.
* *
* @param channels the channels * @param channels the channels
*/ */
public RunnerStateChange(State state, Channel... channels) { public RunnerStateChange(State state, String reason, String message,
Channel... channels) {
super(channels); super(channels);
this.state = state; this.state = state;
this.reason = reason;
this.message = message;
} }
/** /**
@ -53,4 +59,36 @@ public class RunnerStateChange extends Event<Void> {
public State state() { public State state() {
return state; return state;
} }
/**
* Gets the reason.
*
* @return the reason
*/
public String reason() {
return reason;
}
/**
* Gets the message.
*
* @return the message
*/
public String message() {
return message;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append(Components.objectName(this))
.append(" [").append(state).append(": ").append(reason);
if (channels() != null) {
builder.append(", channels=");
builder.append(Channel.toString(channels()));
}
builder.append(']');
return builder.toString();
}
} }

View file

@ -1,5 +1,5 @@
# #
#Fri Sep 01 16:56:14 CEST 2023 #Thu Sep 14 22:05:28 CEST 2023
org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert
org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert
@ -22,12 +22,12 @@ org.eclipse.jdt.core.formatter.blank_lines_after_package=1
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert
org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true
org.eclipse.jdt.core.formatter.comment.indent_root_tags=false org.eclipse.jdt.core.formatter.comment.indent_root_tags=false
org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true
org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on
org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position=false org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position=false
org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert
org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16
org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert
org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert
@ -43,8 +43,8 @@ org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invoc
org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert
org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=true org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=true
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert
org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16 org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16
org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0 org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0
@ -63,8 +63,8 @@ org.eclipse.jdt.core.formatter.alignment_for_type_parameters=16
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false
org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert
org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert
org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=false org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=false
org.eclipse.jdt.core.formatter.parentheses_positions_in_annotation=common_lines org.eclipse.jdt.core.formatter.parentheses_positions_in_annotation=common_lines
org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert

View file

@ -9,5 +9,6 @@ plugins {
} }
dependencies { dependencies {
implementation 'org.freemarker:freemarker:[2.3.32,2.4)' api 'org.freemarker:freemarker:[2.3.32,2.4)'
api 'io.kubernetes:client-java:[18.0.0,19)'
} }

View file

@ -0,0 +1,34 @@
/*
* 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.util;
/**
* Some constants.
*/
public class Constants {
/** The Constant VM_OP_NAME. */
public static final String VM_OP_NAME = "vm-operator";
/** The Constant VM_OP_GROUP. */
public static final String VM_OP_GROUP = "vmoperator.jdrupes.org";
/** The Constant VM_OP_KIND_VM. */
public static final String VM_OP_KIND_VM = "VirtualMachine";
}

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.jdrupes.vmoperator.manager; package org.jdrupes.vmoperator.util;
import com.google.gson.JsonArray; import com.google.gson.JsonArray;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.jdrupes.vmoperator.manager; package org.jdrupes.vmoperator.util;
import io.kubernetes.client.common.KubernetesListObject; import io.kubernetes.client.common.KubernetesListObject;
import io.kubernetes.client.common.KubernetesObject; import io.kubernetes.client.common.KubernetesObject;