Compare commits

..

58 commits

Author SHA1 Message Date
c6c6358426 Fix warnings.
Some checks failed
ci/woodpecker/push/build Pipeline was successful
CodeQL / Analyze (push) Has been cancelled
Java CI with Gradle / build (push) Has been cancelled
Deploy Jekyll site to Pages / build (push) Has been cancelled
Deploy Jekyll site to Pages / deploy (push) Has been cancelled
2025-08-11 20:32:08 +02:00
470c266157 Build with woodpecker (#1)
Some checks are pending
CodeQL / Analyze (push) Waiting to run
Java CI with Gradle / build (push) Waiting to run
Deploy Jekyll site to Pages / build (push) Waiting to run
Deploy Jekyll site to Pages / deploy (push) Blocked by required conditions
ci/woodpecker/push/build Pipeline was successful
Reviewed-on: #1
Co-authored-by: Michael N. Lipp <mnl@mnl.de>
Co-committed-by: Michael N. Lipp <mnl@mnl.de>
2025-08-11 12:50:28 +02:00
7b8df80828 Use display manager for login.
Some checks are pending
CodeQL / Analyze (push) Waiting to run
Java CI with Gradle / build (push) Waiting to run
Deploy Jekyll site to Pages / build (push) Waiting to run
Deploy Jekyll site to Pages / deploy (push) Blocked by required conditions
2025-08-01 17:41:03 +02:00
fccf2a6b65 Fix branch evaluation.
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2025-07-26 22:43:45 +02:00
00e9affee4 Fix evaluation of template source. 2025-07-26 15:18:22 +02:00
fa84110e1d Handle configuration value properly. 2025-07-14 17:34:09 +02:00
76b579c404 Add key, allowing vue to optimize.
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2025-05-04 11:36:55 +02:00
a5433c869b Upgrade webconsole base library. 2025-05-03 22:29:42 +02:00
10f3028f06 Increase concurrency and avoid race condition. 2025-04-30 16:27:15 +02:00
b7fad4614d Improve debug messages.
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2025-04-29 14:02:12 +02:00
7d298ce24b Clarify intend. 2025-04-14 21:39:35 +02:00
6ef4c2aaa2 Fix return value. 2025-04-14 12:08:48 +02:00
5bcf0ba051 Add umami to javadoc. 2025-04-13 17:01:38 +02:00
d67f374de7 Try umami. 2025-04-13 16:48:42 +02:00
2b3420c801 Update component picture.
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2025-03-31 12:21:47 +02:00
bd54d293eb Update picture. 2025-03-30 21:58:22 +02:00
cb2ae7c33e Update. 2025-03-30 21:32:55 +02:00
85a9b41046 Update picture. 2025-03-30 21:12:18 +02:00
fb976802cf Minor edit. 2025-03-30 13:05:33 +02:00
af112bb66b Editorial changes. 2025-03-30 12:37:49 +02:00
592b30f6c5 Update state diagram. 2025-03-30 12:17:14 +02:00
c716e32534 Make tests work again. 2025-03-30 11:42:03 +02:00
c79d678a2a More consistent logging. 2025-03-29 18:38:09 +01:00
f30ea79abb Minor editorial changes. 2025-03-29 17:41:01 +01:00
d7d5c860a2 Merge branch 'wip/optimize' 2025-03-29 15:10:32 +01:00
991763f228 Optimize state change handling.
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2025-03-29 15:09:38 +01:00
db49f5ba2f Restart non-deleted pods. 2025-03-28 21:28:55 +01:00
2e70ef2b98 Merge branch 'feature/pod-restart' 2025-03-28 18:03:54 +01:00
e8097d87d9 Let the operator manage pod restarts. 2025-03-28 18:03:09 +01:00
7a70d73330 Merge branch 'feature/pools'
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2025-03-24 15:05:31 +01:00
3143a1be93 Remove no longer valid optimization.
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2025-03-24 15:04:39 +01:00
4bcbafb4f1 Improve label. 2025-03-22 11:25:02 +01:00
331b6d8d61 Minor edit. 2025-03-21 09:18:38 +01:00
725fb663c8 Merge branch 'feature/pools'
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2025-03-20 18:47:46 +01:00
d9799df861 Delete assignments when pool is deleted. 2025-03-20 18:46:45 +01:00
fe1d56517b Reorganize handlers. 2025-03-20 18:29:45 +01:00
359b1fdb84 Clarify pipeline usage. 2025-03-20 18:02:14 +01:00
16a15bc9ad Document memory allocation. 2025-03-20 09:33:10 +01:00
7644e65ab0 Merge branch 'wip/optimize' 2025-03-19 22:59:14 +01:00
dbc89e6e09 Avoid unnecessary processing. 2025-03-19 22:57:58 +01:00
9baf9b7673 Reset runner info when pod is deleted. 2025-03-18 21:48:10 +01:00
3686629a28 Fix race condition. 2025-03-18 16:44:15 +01:00
5991fe0d2d Merge branch 'wip/optimize' 2025-03-17 16:50:29 +01:00
3b0a4c8a23 Rate limit for RAM size updates. 2025-03-17 16:49:10 +01:00
5ca45d7620 Minor edit.
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2025-03-16 23:12:22 +01:00
efd489b22f Allow VM operator to watch pods. 2025-03-16 23:03:19 +01:00
9644e5fd83 Improve debug message. 2025-03-16 23:02:59 +01:00
fe18bb3cdf Consolidate debug messages. 2025-03-16 23:02:44 +01:00
9a557f5019 Merge branch 'wip/optimize' 2025-03-16 17:08:40 +01:00
fd0f4f8eb2 Fetch display secret only when needed. 2025-03-16 17:01:36 +01:00
9bd17e8899 Update. 2025-03-16 15:44:27 +01:00
227c097c01 Actively add pod info, don't run queries. 2025-03-16 15:13:16 +01:00
ce4d0bfb72 More features, more resources. 2025-03-16 14:53:01 +01:00
017607f2e2 Prune not required data before transfer. 2025-03-15 15:21:44 +01:00
fcdb537f35 Merge branch 'fix/condition-update' 2025-03-15 12:35:23 +01:00
5309460fbf Prevent update of lastTransitionTime if we have no transition. 2025-03-15 12:33:20 +01:00
dc228295d1 Update. 2025-03-15 11:25:13 +01:00
1b1e5ffb8c Reduce default logging. 2025-03-14 21:16:01 +01:00
114 changed files with 1252 additions and 950 deletions

View file

@ -1,78 +0,0 @@
stages:
- build
- test
- publish
- deploy
.any-job:
rules:
- if: $CI_SERVER_HOST == "gitlab.mnl.de"
.gradle-job:
extends: .any-job
image: registry.mnl.de/org/jgrapes/jdk21-builder:v2
cache:
- key: dependencies-${CI_COMMIT_BRANCH}
policy: pull-push
paths:
- .gradle
- node_modules
- key: "$CI_COMMIT_SHA"
policy: pull-push
paths:
- build
- "*/build"
before_script:
- echo -n $CI_REGISTRY_PASSWORD | podman login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY
- git switch $(git branch -r --sort="authordate" --contains $CI_COMMIT_SHA | head -1 | sed -e 's#[^/]*/##')
- git pull
- git reset --hard $CI_COMMIT_SHA
build-jars:
stage: build
extends: .gradle-job
script:
- ./gradlew -Pdocker.registry=$CI_REGISTRY_IMAGE build apidocs
publish-images:
stage: publish
extends: .gradle-job
dependencies:
- build-jars
script:
- ./gradlew -Pdocker.registry=$CI_REGISTRY_IMAGE publishImage
.pages-job:
extends: .any-job
image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/ruby:3.2
variables:
JEKYLL_ENV: production
LC_ALL: C.UTF-8
before_script:
- git fetch origin gh-pages
- git checkout gh-pages
- gem install bundler
- bundle install
test-pages:
stage: test
extends: .pages-job
rules:
- if: $CI_COMMIT_BRANCH == "gh-pages"
script:
- bundle exec jekyll build -d test
artifacts:
paths:
- test
#publish-pages:
# stage: publish
# extends: .pages-job
# rules:
# - if: $CI_COMMIT_BRANCH == "gh-pages"
# script:
# - bundle exec jekyll build -d public
# artifacts:
# paths:
# - public
# environment: production

38
.woodpecker/build.yaml Normal file
View file

@ -0,0 +1,38 @@
when:
- event: push
evaluate: 'CI_SYSTEM_HOST == "woodpecker.mnl.de"'
clone:
- name: git
image: woodpeckerci/plugin-git
settings:
partial: false
tags: true
depth: 0
steps:
- name: prepare
image: alpine
commands:
# Because we run the next step as user 1000 to make podman work:
- mkdir /woodpecker/workflow
- chown 1000:1000 /woodpecker/workflow
- chown -R 1000:1000 $CI_WORKSPACE
- name: build-jars
image: registry.mnl.de/mnl/jdk21-builder:v4
environment:
HOME: /woodpecker/workflow
REGISTRY: registry.mnl.de
REGISTRY_USER: mnl
REGISTRY_TOKEN:
from_secret: REGISTRY_TOKEN
commands:
- echo $REGISTRY_TOKEN | podman login -u $REGISTRY_USER --password-stdin $REGISTRY
- ./gradlew -Pdocker.registry=$REGISTRY/$REGISTRY_USER build apidocs publishImage
backend_options:
kubernetes:
securityContext:
privileged: true
runAsUser: 1000
runAsGroup: 1000

View file

@ -3,10 +3,23 @@
![Latest Manager](https://img.shields.io/github/v/tag/mnlipp/vm-operator?filter=manager*&label=latest)
![Latest Runner](https://img.shields.io/github/v/tag/mnlipp/vm-operator?filter=runner-qemu*&label=latest)
# Run Qemu in Kubernetes Pods
# Run QEMU/KVM in Kubernetes Pods
The goal of this project is to provide simply to use and flexible components
for running Qemu based VMs in Kubernetes pods.
![Overview picture](webpages/index-pic.svg)
This project provides an easy to use and flexible solution for running
QEMU/KVM based VMs in Kubernetes pods.
The central component of this solution is the kubernetes operator that
manages "runners". These run in pods and are used to start and manage
the QEMU/KVM process for the VMs (optionally together with a SW-TPM).
A web GUI for administrators provides an overview of the VMs together
with some basic control over the VMs. A web GUI for users provides an
interface to access and optionally start, stop and reset the VMs.
Advanced features of the operator include pooling of VMs and automatic
login.
See the [project's home page](https://vm-operator.jdrupes.org/)
for details.

View file

@ -21,22 +21,31 @@ spec:
- name: vm-operator
image: >-
ghcr.io/mnlipp/org.jdrupes.vmoperator.manager:latest
imagePullPolicy: Always
env:
- name: JAVA_OPTS
# The VM operator needs about 25 MB of memory, plus 1 MB for
# each VM. The reason is that for the sake of effeciency, we
# have to keep a parsed representation of the CRD in memory,
# which requires about 512 KB per VM. While handling updates,
# we temporarily have the old and the new version of the CRD
# in memory, so we need another 512 KB per VM.
value: "-Xmx128m"
resources:
requests:
cpu: 100m
memory: 128Mi
volumeMounts:
- name: config
mountPath: /etc/opt/vmoperator
- name: vmop-image-repository
mountPath: /var/local/vmop-image-repository
imagePullPolicy: Always
securityContext:
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
resources:
requests:
cpu: 100m
memory: 128Mi
volumes:
- name: config
configMap:

View file

@ -38,6 +38,7 @@ rules:
- persistentvolumeclaims
- pods
verbs:
- watch
- list
- get
- create

View file

@ -3,9 +3,9 @@
The CRD must be deployed independently. Apart from that, the
`kustomize.yaml`
* creates a small cdrom image repository and
* creates a small cdrom image repository and
* deploys the operator in namespace `vmop-dev` with a replica of 0.
* deploys the operator in namespace `vmop-dev` with a replica of 0.
This allows you to run the manager in your IDE.

View file

@ -0,0 +1,3 @@
#!/bin/sh
sed -i '/AutomaticLogin/d' /etc/gdm/custom.conf

View file

@ -72,8 +72,8 @@ doLogin() {
return
fi
# Check if this user is already logged in on tty1
curUser=$(loginctl -j | jq -r '.[] | select(.tty=="tty1") | .user')
# Check if this user is already logged in on tty2
curUser=$(loginctl -j | jq -r '.[] | select(.tty=="tty2") | .user')
if [ "$curUser" = "$user" ]; then
echo >&${con} "201 User already logged in"
return
@ -97,16 +97,13 @@ doLogin() {
fi
fi
# Start the desktop for the user
systemd-run 2>$temperr \
--unit vmop-user-desktop --uid=$uid --gid=$uid \
--working-directory="/home/$user" -p TTYPath=/dev/tty1 \
-p PAMName=login -p StandardInput=tty -p StandardOutput=journal \
-p Conflicts="gdm.service getty@tty1.service" \
-E XDG_RUNTIME_DIR="/run/user/$uid" \
-E XDG_CURRENT_DESKTOP=GNOME \
-p ExecStartPre="/usr/bin/chvt 1" \
dbus-run-session -- gnome-shell --display-server --wayland
# Configure user as auto login user
sed -i '/AutomaticLogin/d' /etc/gdm/custom.conf
sed -i '/\[daemon\]/a AutomaticLoginEnable=true\nAutomaticLogin='$user \
/etc/gdm/custom.conf
# Activate user
systemctl restart gdm
if [ $? -eq 0 ]; then
echo >&${con} "201 User logged in successfully"
else
@ -117,14 +114,8 @@ doLogin() {
# Attempt to log out a user currently using tty1. This is an intermediate
# operation that can be invoked from other operations
attemptLogout() {
systemctl status vmop-user-desktop > /dev/null 2>&1
if [ $? = 0 ]; then
systemctl stop vmop-user-desktop
fi
loginctl -j | jq -r '.[] | select(.tty=="tty1") | .session' \
| while read sid; do
loginctl kill-session $sid
done
sed -i '/AutomaticLogin/d' /etc/gdm/custom.conf
systemctl stop gdm
echo >&${con} "102 Desktop stopped"
}
@ -133,15 +124,7 @@ attemptLogout() {
# Also try to restart gdm, if it is not running.
doLogout() {
attemptLogout
systemctl status gdm >/dev/null 2>&1
if [ $? != 0 ]; then
systemctl restart gdm 2>$temperr
if [ $? -eq 0 ]; then
echo >&${con} "102 gdm restarted"
else
echo >&${con} "102 Restarting gdm failed: $(tr '\n' ' ' <${temperr})"
fi
fi
systemctl restart gdm
echo >&${con} "202 User logged out"
}
@ -153,7 +136,7 @@ while read line <&${con}; do
done
onExit() {
attemptLogout
doLogout
if [ -n "$temperr" ]; then
rm -f $temperr
fi

View file

@ -32,4 +32,5 @@
<noscript><p><img referrerpolicy="no-referrer-when-downgrade"
src="//piwik.mnl.de/matomo.php?idsite=17&amp;rec=1&amp;action_name=VM-Operator" style="border:0;" alt="" /></p></noscript>
<!-- End Matomo Code -->
<script defer src="https://gotit.mnl.de/script.js" data-website-id="14b277ad-d330-4a54-82f1-a77d111240ac"></script>
</div>

View file

@ -18,7 +18,6 @@
package org.jdrupes.vmoperator.common;
// TODO: Auto-generated Javadoc
/**
* Some constants.
*/

View file

@ -32,13 +32,11 @@ import java.util.regex.Pattern;
public class Convertions {
@SuppressWarnings({ "PMD.UseConcurrentHashMap",
"PMD.FieldNamingConventions", "PMD.VariableNamingConventions" })
"PMD.FieldNamingConventions" })
private static final Map<String, BigInteger> unitMap = new HashMap<>();
@SuppressWarnings({ "PMD.FieldNamingConventions",
"PMD.VariableNamingConventions" })
@SuppressWarnings({ "PMD.FieldNamingConventions" })
private static final List<Map.Entry<String, BigInteger>> unitMappings;
@SuppressWarnings({ "PMD.FieldNamingConventions",
"PMD.VariableNamingConventions" })
@SuppressWarnings({ "PMD.FieldNamingConventions" })
private static final Pattern memorySize
= Pattern.compile("^\\s*(\\d+(\\.\\d+)?)\\s*([A-Za-z]*)\\s*");
@ -69,7 +67,6 @@ public class Convertions {
* @param amount the amount
* @return the big integer
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public static BigInteger parseMemory(Object amount) {
if (amount == null) {
return (BigInteger) amount;

View file

@ -47,8 +47,7 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
/**
* Helpers for K8s API.
*/
@SuppressWarnings({ "PMD.ShortClassName", "PMD.UseUtilityClass",
"PMD.DataflowAnomalyAnalysis" })
@SuppressWarnings({ "PMD.ShortClassName", "PMD.UseUtilityClass" })
public class K8s {
/**
@ -113,7 +112,6 @@ public class K8s {
public static JsonObject yamlToJson(ApiClient client, Reader yaml) {
// Avoid Yaml.load due to
// https://github.com/kubernetes-client/java/issues/2741
@SuppressWarnings("PMD.UseConcurrentHashMap")
Map<String, Object> yamlData
= new Yaml(new SafeConstructor(new LoaderOptions())).load(yaml);

View file

@ -48,8 +48,7 @@ import okhttp3.Response;
* A client with some additional properties.
*/
@SuppressWarnings({ "PMD.ExcessivePublicCount", "PMD.TooManyMethods",
"PMD.LinguisticNaming", "checkstyle:LineLength",
"PMD.CouplingBetweenObjects", "PMD.GodClass" })
"checkstyle:LineLength", "PMD.CouplingBetweenObjects", "PMD.GodClass" })
public class K8sClient extends ApiClient {
private ApiClient apiClient;
@ -231,7 +230,6 @@ public class K8sClient extends ApiClient {
* @return the api client
* @see ApiClient#setKeyManagers(javax.net.ssl.KeyManager[])
*/
@SuppressWarnings("PMD.UseVarargs")
@Override
public ApiClient setKeyManagers(KeyManager[] managers) {
return apiClient().setKeyManagers(managers);
@ -638,7 +636,6 @@ public class K8sClient extends ApiClient {
* @return the string
* @see ApiClient#selectHeaderAccept(java.lang.String[])
*/
@SuppressWarnings("PMD.UseVarargs")
@Override
public String selectHeaderAccept(String[] accepts) {
return apiClient().selectHeaderAccept(accepts);
@ -651,7 +648,6 @@ public class K8sClient extends ApiClient {
* @return the string
* @see ApiClient#selectHeaderContentType(java.lang.String[])
*/
@SuppressWarnings("PMD.UseVarargs")
@Override
public String selectHeaderContentType(String[] contentTypes) {
return apiClient().selectHeaderContentType(contentTypes);
@ -818,7 +814,7 @@ public class K8sClient extends ApiClient {
* @throws ApiException the api exception
* @see ApiClient#buildCall(java.lang.String, java.lang.String, java.util.List, java.util.List, java.lang.Object, java.util.Map, java.util.Map, java.util.Map, java.lang.String[], io.kubernetes.client.openapi.ApiCallback)
*/
@SuppressWarnings({ "rawtypes", "PMD.ExcessiveParameterList" })
@SuppressWarnings({ "rawtypes" })
@Override
public Call buildCall(String path, String method, List<Pair> queryParams,
List<Pair> collectionQueryParams, Object body,
@ -847,7 +843,7 @@ public class K8sClient extends ApiClient {
* @throws ApiException the api exception
* @see ApiClient#buildRequest(java.lang.String, java.lang.String, java.util.List, java.util.List, java.lang.Object, java.util.Map, java.util.Map, java.util.Map, java.lang.String[], io.kubernetes.client.openapi.ApiCallback)
*/
@SuppressWarnings({ "rawtypes", "PMD.ExcessiveParameterList" })
@SuppressWarnings({ "rawtypes" })
@Override
public Request buildRequest(String path, String method,
List<Pair> queryParams, List<Pair> collectionQueryParams,

View file

@ -45,8 +45,7 @@ import java.util.function.Function;
* @param <O> the generic type
* @param <L> the generic type
*/
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis",
"PMD.CouplingBetweenObjects" })
@SuppressWarnings({ "PMD.CouplingBetweenObjects" })
public class K8sClusterGenericStub<O extends KubernetesObject,
L extends KubernetesListObject> {
protected final K8sClient client;
@ -240,6 +239,7 @@ public class K8sClusterGenericStub<O extends KubernetesObject,
* @param <L> the object list type
* @param <R> the result type
*/
@FunctionalInterface
public interface GenericSupplier<O extends KubernetesObject,
L extends KubernetesListObject,
R extends K8sClusterGenericStub<O, L>> {
@ -254,7 +254,6 @@ public class K8sClusterGenericStub<O extends KubernetesObject,
* @param name the name
* @return the result
*/
@SuppressWarnings("PMD.UseObjectForClearerAPI")
R get(Class<O> objectClass, Class<L> objectListClass, K8sClient client,
APIResource context, String name);
}
@ -283,7 +282,6 @@ public class K8sClusterGenericStub<O extends KubernetesObject,
* @return the stub if the object exists
* @throws ApiException the api exception
*/
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop" })
public static <O extends KubernetesObject, L extends KubernetesListObject,
R extends K8sClusterGenericStub<O, L>>
R get(Class<O> objectClass, Class<L> objectListClass,
@ -314,8 +312,6 @@ public class K8sClusterGenericStub<O extends KubernetesObject,
* @return the stub if the object exists
* @throws ApiException the api exception
*/
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
"PMD.UseObjectForClearerAPI" })
public static <O extends KubernetesObject, L extends KubernetesListObject,
R extends K8sClusterGenericStub<O, L>>
R get(Class<O> objectClass, Class<L> objectListClass,
@ -340,8 +336,6 @@ public class K8sClusterGenericStub<O extends KubernetesObject,
* @return the stub if the object exists
* @throws ApiException the api exception
*/
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
public static <O extends KubernetesObject, L extends KubernetesListObject,
R extends K8sClusterGenericStub<O, L>>
R create(Class<O> objectClass, Class<L> objectListClass,

View file

@ -29,7 +29,6 @@ import io.kubernetes.client.openapi.models.V1ObjectMeta;
* notably the metadata, is made available through the methods
* defined by {@link KubernetesObject}.
*/
@SuppressWarnings("PMD.DataClass")
public class K8sDynamicModel implements KubernetesObject {
private final V1ObjectMeta metadata;

View file

@ -62,7 +62,7 @@ public class K8sDynamicModelsBase<T extends K8sDynamicModel>
} catch (InstantiationException | IllegalAccessException
| IllegalArgumentException | InvocationTargetException
| NoSuchMethodException | SecurityException exc) {
throw new IllegalArgumentException(exc); // NOPMD
throw new IllegalArgumentException(exc);
}
}
}

View file

@ -31,7 +31,6 @@ import java.util.Collection;
* state and can therefore be used for any kind of object, especially
* custom objects.
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class K8sDynamicStub
extends K8sDynamicStubBase<K8sDynamicModel, K8sDynamicModels> {
@ -64,8 +63,6 @@ public class K8sDynamicStub
* @return the stub if the object exists
* @throws ApiException the api exception
*/
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
public static K8sDynamicStub get(K8sClient client,
GroupVersionKind gvk, String namespace, String name)
throws ApiException {
@ -83,8 +80,6 @@ public class K8sDynamicStub
* @return the stub if the object exists
* @throws ApiException the api exception
*/
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
public static K8sDynamicStub get(K8sClient client,
APIResource context, String namespace, String name) {
return new K8sDynamicStub(client, context, namespace, name);

View file

@ -26,7 +26,6 @@ import io.kubernetes.client.Discovery.APIResource;
* state and can therefore be used for any kind of object, especially
* custom objects.
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public abstract class K8sDynamicStubBase<O extends K8sDynamicModel,
L extends K8sDynamicModelsBase<O>> extends K8sGenericStub<O, L> {
@ -40,7 +39,6 @@ public abstract class K8sDynamicStubBase<O extends K8sDynamicModel,
* @param namespace the namespace
* @param name the name
*/
@SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
public K8sDynamicStubBase(Class<O> objectClass,
Class<L> objectListClass, DynamicTypeAdapterFactory<O, L> taf,
K8sClient client, APIResource context, String namespace,

View file

@ -48,7 +48,7 @@ import java.util.function.Function;
* @param <O> the generic type
* @param <L> the generic type
*/
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.TooManyMethods" })
@SuppressWarnings({ "PMD.TooManyMethods" })
public class K8sGenericStub<O extends KubernetesObject,
L extends KubernetesListObject> {
protected final K8sClient client;
@ -200,7 +200,6 @@ public class K8sGenericStub<O extends KubernetesObject,
* @return the updated model or empty if the object was not found
* @throws ApiException the api exception
*/
@SuppressWarnings("PMD.AssignmentInOperand")
public Optional<O> updateStatus(O object, Function<O, Object> updater)
throws ApiException {
return K8s.optional(api.updateStatus(object, updater));
@ -218,7 +217,7 @@ public class K8sGenericStub<O extends KubernetesObject,
* @return the updated model or empty if the object was not found
* @throws ApiException the api exception
*/
@SuppressWarnings({ "PMD.AssignmentInOperand", "PMD.UnusedAssignment" })
@SuppressWarnings({ "PMD.AssignmentInOperand" })
public Optional<O> updateStatus(Function<O, Object> updater, O current,
int retries) throws ApiException {
while (true) {
@ -248,7 +247,6 @@ public class K8sGenericStub<O extends KubernetesObject,
* @return the updated model or empty if the object was not found
* @throws ApiException the api exception
*/
@SuppressWarnings({ "PMD.AssignmentInOperand", "PMD.UnusedAssignment" })
public Optional<O> updateStatus(Function<O, Object> updater, int retries)
throws ApiException {
return updateStatus(updater, null, retries);
@ -359,6 +357,7 @@ public class K8sGenericStub<O extends KubernetesObject,
* @param <L> the object list type
* @param <R> the result type
*/
@FunctionalInterface
public interface GenericSupplier<O extends KubernetesObject,
L extends KubernetesListObject, R extends K8sGenericStub<O, L>> {
@ -370,7 +369,6 @@ public class K8sGenericStub<O extends KubernetesObject,
* @param name the name
* @return the result
*/
@SuppressWarnings("PMD.UseObjectForClearerAPI")
R get(K8sClient client, String namespace, String name);
}
@ -396,8 +394,6 @@ public class K8sGenericStub<O extends KubernetesObject,
* @return the stub if the object exists
* @throws ApiException the api exception
*/
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
public static <O extends KubernetesObject, L extends KubernetesListObject,
R extends K8sGenericStub<O, L>>
R create(Class<O> objectClass, Class<L> objectListClass,

View file

@ -27,6 +27,7 @@ import io.kubernetes.client.util.generic.GenericKubernetesApi;
import io.kubernetes.client.util.generic.options.ListOptions;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -49,7 +50,6 @@ public class K8sObserver<O extends KubernetesObject,
ADDED, MODIFIED, DELETED
}
@SuppressWarnings("PMD.FieldNamingConventions")
protected final Logger logger = Logger.getLogger(getClass().getName());
protected final K8sClient client;
@ -72,8 +72,7 @@ public class K8sObserver<O extends KubernetesObject,
* @param namespace the namespace
* @param options the options
*/
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
"PMD.UseObjectForClearerAPI", "PMD.AvoidCatchingThrowable",
@SuppressWarnings({ "PMD.AvoidCatchingThrowable",
"PMD.CognitiveComplexity", "PMD.AvoidCatchingGenericException" })
public K8sObserver(Class<O> objectClass, Class<L> objectListClass,
K8sClient client, APIResource context, String namespace,
@ -89,23 +88,29 @@ public class K8sObserver<O extends KubernetesObject,
thread = (Components.useVirtualThreads() ? Thread.ofVirtual()
: Thread.ofPlatform()).unstarted(() -> {
try {
logger
.config(() -> "Watching " + context.getResourcePlural()
logger.fine(() -> "Observing " + context.getResourcePlural()
+ " (" + context.getPreferredVersion() + ")"
+ Optional.ofNullable(options.getLabelSelector())
.map(ls -> " with labels " + ls).orElse("")
+ " in " + namespace);
// Watch sometimes terminates without apparent reason.
while (!Thread.currentThread().isInterrupted()) {
Instant startedAt = Instant.now();
try {
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
var changed
= api.watch(namespace, options).iterator();
while (changed.hasNext()) {
handler.accept(client, changed.next());
var response = changed.next();
logger.fine(() -> "Resource "
+ context.getKind() + "/"
+ response.object.getMetadata().getName()
+ " " + response.type);
handler.accept(client, response);
}
} catch (ApiException | RuntimeException e) {
logger.log(Level.FINE, e, () -> "Problem watching"
+ " resource " + context.getKind()
+ " (will retry): " + e.getMessage());
delayRestart(startedAt);
}
@ -225,7 +230,6 @@ public class K8sObserver<O extends KubernetesObject,
}
@Override
@SuppressWarnings("PMD.UseLocaleWithCaseConversions")
public String toString() {
return "Observer for " + K8s.toString(context) + " " + namespace;
}

View file

@ -26,7 +26,6 @@ import java.util.List;
/**
* A stub for config maps (v1).
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class K8sV1ConfigMapStub
extends K8sGenericStub<V1ConfigMap, V1ConfigMapList> {

View file

@ -29,7 +29,6 @@ import java.util.Optional;
/**
* A stub for pods (v1).
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class K8sV1DeploymentStub
extends K8sGenericStub<V1Deployment, V1DeploymentList> {

View file

@ -29,7 +29,6 @@ import java.util.List;
/**
* A stub for nodes (v1).
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class K8sV1NodeStub extends K8sClusterGenericStub<V1Node, V1NodeList> {
public static final APIResource CONTEXT = new APIResource("", List.of("v1"),
@ -74,8 +73,7 @@ public class K8sV1NodeStub extends K8sClusterGenericStub<V1Node, V1NodeList> {
/**
* Provide {@link GenericSupplier}.
*/
@SuppressWarnings({ "PMD.UnusedFormalParameter",
"PMD.UnusedPrivateMethod" })
@SuppressWarnings({ "PMD.UnusedFormalParameter" })
private static K8sV1NodeStub getGeneric(Class<V1Node> objectClass,
Class<V1NodeList> objectListClass, K8sClient client,
APIResource context, String name) {

View file

@ -29,7 +29,6 @@ import java.util.List;
/**
* A stub for pods (v1).
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class K8sV1PodStub extends K8sGenericStub<V1Pod, V1PodList> {
/** The pods' context. */

View file

@ -29,7 +29,6 @@ import java.util.List;
/**
* A stub for pods (v1).
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class K8sV1PvcStub extends
K8sGenericStub<V1PersistentVolumeClaim, V1PersistentVolumeClaimList> {

View file

@ -29,7 +29,6 @@ import java.util.List;
/**
* A stub for secrets (v1).
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class K8sV1SecretStub extends K8sGenericStub<V1Secret, V1SecretList> {
public static final APIResource CONTEXT = new APIResource("", List.of("v1"),

View file

@ -29,7 +29,6 @@ import java.util.List;
/**
* A stub for secrets (v1).
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class K8sV1ServiceStub extends K8sGenericStub<V1Service, V1ServiceList> {
public static final APIResource CONTEXT = new APIResource("", List.of("v1"),

View file

@ -26,7 +26,6 @@ import java.util.List;
/**
* A stub for stateful sets (v1).
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class K8sV1StatefulSetStub
extends K8sGenericStub<V1StatefulSet, V1StatefulSetList> {

View file

@ -46,11 +46,10 @@ import org.jdrupes.vmoperator.util.DataPath;
/**
* Represents a VM definition.
*/
@SuppressWarnings({ "PMD.DataClass", "PMD.TooManyMethods",
"PMD.CouplingBetweenObjects" })
@SuppressWarnings({ "PMD.DataClass", "PMD.TooManyMethods" })
public class VmDefinition extends K8sDynamicModel {
@SuppressWarnings({ "PMD.FieldNamingConventions", "unused" })
@SuppressWarnings({ "unused" })
private static final Logger logger
= Logger.getLogger(VmDefinition.class.getName());
@SuppressWarnings("PMD.FieldNamingConventions")
@ -300,8 +299,8 @@ public class VmDefinition extends K8sDynamicModel {
*
* @return the data
*/
public Optional<VmExtraData> extra() {
return Optional.ofNullable(extraData);
public VmExtraData extra() {
return extraData;
}
/**

View file

@ -31,7 +31,6 @@ import java.util.Collection;
* state and can therefore be used for any kind of object, especially
* custom objects.
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class VmDefinitionStub
extends K8sDynamicStubBase<VmDefinition, VmDefinitions> {
@ -64,8 +63,6 @@ public class VmDefinitionStub
* @return the stub if the object exists
* @throws ApiException the api exception
*/
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
public static VmDefinitionStub get(K8sClient client,
GroupVersionKind gvk, String namespace, String name)
throws ApiException {
@ -83,8 +80,6 @@ public class VmDefinitionStub
* @return the stub if the object exists
* @throws ApiException the api exception
*/
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
public static VmDefinitionStub get(K8sClient client,
APIResource context, String namespace, String name) {
return new VmDefinitionStub(client, context, namespace, name);

View file

@ -34,7 +34,6 @@ import java.util.logging.Logger;
*/
public class VmExtraData {
@SuppressWarnings("PMD.FieldNamingConventions")
private static final Logger logger
= Logger.getLogger(VmExtraData.class.getName());
@ -75,6 +74,15 @@ public class VmExtraData {
return nodeName;
}
/**
* Gets the node addresses.
*
* @return the nodeAddresses
*/
public List<String> nodeAddresses() {
return nodeAddresses;
}
/**
* Sets the reset count.
*
@ -103,20 +111,20 @@ public class VmExtraData {
* @param deleteConnectionFile the delete connection file
* @return the string
*/
public String connectionFile(String password,
public Optional<String> connectionFile(String password,
Class<?> preferredIpVersion, boolean deleteConnectionFile) {
var addr = displayIp(preferredIpVersion);
if (addr.isEmpty()) {
logger
.severe(() -> "Failed to find display IP for " + vmDef.name());
return null;
return Optional.empty();
}
var port = vmDef.<Number> fromVm("display", "spice", "port")
.map(Number::longValue);
if (port.isEmpty()) {
logger
.severe(() -> "No port defined for display of " + vmDef.name());
return null;
return Optional.empty();
}
StringBuffer data = new StringBuffer(100)
.append("[virt-viewer]\ntype=spice\nhost=")
@ -135,7 +143,7 @@ public class VmExtraData {
if (deleteConnectionFile) {
data.append("delete-this-file=1\n");
}
return data.toString();
return Optional.of(data.toString());
}
private Optional<InetAddress> displayIp(Class<?> preferredIpVersion) {

View file

@ -35,7 +35,6 @@ import org.jdrupes.vmoperator.util.DataPath;
/**
* Represents a VM pool.
*/
@SuppressWarnings({ "PMD.DataClass" })
public class VmPool {
private final String name;
@ -177,7 +176,7 @@ public class VmPool {
}
// Additional check in case lastUsed has not been updated
// by PoolMonitor#onVmDefChanged() yet ("race condition")
// by PoolMonitor#onVmResourceChanged() yet ("race condition")
if (vmDef.condition("ConsoleConnected")
.map(cc -> cc.getLastTransitionTime().toInstant())
.map(this::retainUntil)

View file

@ -24,7 +24,6 @@ import org.jgrapes.core.Event;
/**
* Assign a VM from a pool to a user.
*/
@SuppressWarnings("PMD.DataClass")
public class AssignVm extends Event<VmData> {
private final String fromPool;

View file

@ -43,7 +43,6 @@ public interface ChannelDictionary<K, C extends Channel, A> {
* @param channel the channel
* @param associated the associated
*/
@SuppressWarnings("PMD.ShortClassName")
public record Value<C extends Channel, A>(C channel, A associated) {
}

View file

@ -149,8 +149,6 @@ public class ChannelManager<K, C extends Channel, A>
* @param supplier the supplier
* @return the channel
*/
@SuppressWarnings({ "PMD.AssignmentInOperand",
"PMD.DataflowAnomalyAnalysis" })
public C computeIfAbsent(K key, Function<K, C> supplier) {
return entries.computeIfAbsent(key,
k -> new Value<>(supplier.apply(k), null)).channel();

View file

@ -24,7 +24,6 @@ import org.jgrapes.core.Event;
/**
* Gets the current display secret and optionally updates it.
*/
@SuppressWarnings("PMD.DataClass")
public class GetDisplaySecret extends Event<String> {
private final VmDefinition vmDef;

View file

@ -27,7 +27,6 @@ import org.jgrapes.core.Event;
/**
* Gets the known pools' definitions.
*/
@SuppressWarnings("PMD.DataClass")
public class GetPools extends Event<List<VmPool>> {
private String name;

View file

@ -27,7 +27,6 @@ import org.jgrapes.core.Event;
/**
* Gets the known VMs' definitions and channels.
*/
@SuppressWarnings("PMD.DataClass")
public class GetVms extends Event<List<GetVms.VmData>> {
private String name;

View file

@ -24,7 +24,6 @@ import org.jgrapes.core.Event;
/**
* Modifies a VM.
*/
@SuppressWarnings("PMD.DataClass")
public class ModifyVm extends Event<Void> {
private final String name;

View file

@ -0,0 +1,75 @@
/*
* 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.events;
import io.kubernetes.client.openapi.models.V1Pod;
import org.jdrupes.vmoperator.common.K8sObserver;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Components;
import org.jgrapes.core.Event;
/**
* Indicates a change in a pod that runs a VM.
*/
public class PodChanged extends Event<Void> {
private final V1Pod pod;
private final K8sObserver.ResponseType type;
/**
* Instantiates a new VM changed event.
*
* @param pod the pod
* @param type the type
*/
public PodChanged(V1Pod pod, K8sObserver.ResponseType type) {
this.pod = pod;
this.type = type;
}
/**
* Gets the pod.
*
* @return the pod
*/
public V1Pod pod() {
return pod;
}
/**
* Returns the type.
*
* @return the type
*/
public K8sObserver.ResponseType type() {
return type;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append(Components.objectName(this)).append(" [")
.append(pod.getMetadata().getName()).append(' ').append(type);
if (channels() != null) {
builder.append(", channels=").append(Channel.toString(channels()));
}
builder.append(']');
return builder.toString();
}
}

View file

@ -23,7 +23,6 @@ import org.jgrapes.core.Event;
/**
* Triggers a reset of the VM.
*/
@SuppressWarnings("PMD.DataClass")
public class ResetVm extends Event<String> {
private final String vmName;

View file

@ -24,7 +24,6 @@ import org.jgrapes.core.Event;
/**
* Note the assignment to a user in the VM status.
*/
@SuppressWarnings("PMD.DataClass")
public class UpdateAssignment extends Event<Boolean> {
private final VmPool fromPool;

View file

@ -21,13 +21,13 @@ package org.jdrupes.vmoperator.manager.events;
import org.jdrupes.vmoperator.common.K8sClient;
import org.jdrupes.vmoperator.common.VmDefinition;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Event;
import org.jgrapes.core.EventPipeline;
import org.jgrapes.core.Subchannel.DefaultSubchannel;
/**
* A subchannel used to send the events related to a specific VM.
*/
@SuppressWarnings("PMD.DataClass")
public class VmChannel extends DefaultSubchannel {
private final EventPipeline pipeline;
@ -55,7 +55,6 @@ public class VmChannel extends DefaultSubchannel {
* @param definition the definition
* @return the watch channel
*/
@SuppressWarnings("PMD.LinguisticNaming")
public VmChannel setVmDefinition(VmDefinition definition) {
this.definition = definition;
return this;
@ -86,7 +85,6 @@ public class VmChannel extends DefaultSubchannel {
* @param generation the generation to set
* @return true if value has changed
*/
@SuppressWarnings("PMD.LinguisticNaming")
public boolean setGeneration(long generation) {
if (this.generation == generation) {
return false;
@ -104,6 +102,19 @@ public class VmChannel extends DefaultSubchannel {
return pipeline;
}
/**
* Fire the given event on this channel, using the associated
* {@link #pipeline()}.
*
* @param <T> the generic type
* @param event the event
* @return the t
*/
public <T extends Event<?>> T fire(T event) {
pipeline.fire(event, this);
return event;
}
/**
* Returns the API client.
*

View file

@ -26,7 +26,6 @@ import org.jgrapes.core.Event;
/**
* Indicates a change in a pool configuration.
*/
@SuppressWarnings("PMD.DataClass")
public class VmPoolChanged extends Event<Void> {
private final VmPool vmPool;

View file

@ -25,31 +25,35 @@ import org.jgrapes.core.Components;
import org.jgrapes.core.Event;
/**
* Indicates a change in a VM definition. Note that the definition
* consists of the metadata (mostly immutable), the "spec" and the
* "status" parts. Consumers that are only interested in "spec"
* changes should check {@link #specChanged()} before processing
* the event any further.
* Indicates a change in a VM "resource". Note that the resource
* combines the VM CR's metadata (mostly immutable), the VM CR's
* "spec" part, the VM CR's "status" subresource and state information
* from the pod. Consumers that are only interested in "spec" changes
* should check {@link #specChanged()} before processing the event any
* further.
*/
@SuppressWarnings("PMD.DataClass")
public class VmDefChanged extends Event<Void> {
public class VmResourceChanged extends Event<Void> {
private final K8sObserver.ResponseType type;
private final boolean specChanged;
private final VmDefinition vmDefinition;
private final boolean specChanged;
private final boolean podChanged;
/**
* Instantiates a new VM changed event.
*
* @param type the type
* @param specChanged the spec part changed
* @param vmDefinition the VM definition
* @param specChanged the spec part changed
*/
public VmDefChanged(K8sObserver.ResponseType type, boolean specChanged,
VmDefinition vmDefinition) {
public VmResourceChanged(K8sObserver.ResponseType type,
VmDefinition vmDefinition, boolean specChanged,
boolean podChanged) {
this.type = type;
this.specChanged = specChanged;
this.vmDefinition = vmDefinition;
this.specChanged = specChanged;
this.podChanged = podChanged;
}
/**
@ -61,6 +65,15 @@ public class VmDefChanged extends Event<Void> {
return type;
}
/**
* Return the VM definition.
*
* @return the VM definition
*/
public VmDefinition vmDefinition() {
return vmDefinition;
}
/**
* Indicates if the "spec" part changed.
*/
@ -69,12 +82,10 @@ public class VmDefChanged extends Event<Void> {
}
/**
* Return the VM definition.
*
* @return the VM definition
* Indicates if the pod status changed.
*/
public VmDefinition vmDefinition() {
return vmDefinition;
public boolean podChanged() {
return podChanged;
}
@Override

View file

@ -0,0 +1 @@
/logging.properties

View file

@ -17,7 +17,7 @@ dependencies {
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.webconsole.base:[2.2.0,3)'
implementation 'org.jgrapes:org.jgrapes.webconsole.base:[2.3.0,3)'
implementation 'org.jgrapes:org.jgrapes.webconsole.vuejs:[1.8.0,2)'
implementation 'org.jgrapes:org.jgrapes.webconsole.rbac:[1.4.0,2)'
implementation 'org.jgrapes:org.jgrapes.webconlet.oidclogin:[1.7.0,2)'

View file

@ -1,3 +1,3 @@
<footer>
Copyright &copy; Michael N. Lipp 2023
Copyright &copy; Michael N. Lipp 2023, 2025
</footer>

View file

@ -1,6 +1,6 @@
#
# VM-Operator
# Copyright (C) 2023 Michael N. Lipp
# Copyright (C) 2025 Michael N. Lipp
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
@ -19,10 +19,7 @@
handlers=java.util.logging.ConsoleHandler, \
org.jgrapes.webconlet.logviewer.LogViewerHandler
org.jgrapes.level=FINE
org.jgrapes.core.handlerTracking.level=FINER
org.jdrupes.vmoperator.manager.level=FINE
org.jdrupes.vmoperator.level=FINE
java.util.logging.ConsoleHandler.level=ALL
java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter

View file

@ -37,7 +37,7 @@ data:
# The template to use. Resolved relative to /usr/share/vmrunner/templates.
# template: "Standard-VM-latest.ftl.yaml"
<#if spec.runnerTemplate?? && spec.runnerTemplate.source?? >
template: ${ cm.spec().runnerTemplate.source }
template: ${ spec.runnerTemplate.source }
</#if>
# The template is copied to the data diretory when the VM starts for
@ -53,7 +53,7 @@ data:
# i.e. if you start the VM without a value for this property, and
# decide to trigger a reset later, you have to first set the value
# and then inrement it.
resetCounter: ${ cr.extra().get().resetCount()?c }
resetCounter: ${ cr.extra().resetCount()?c }
# Forward the cloud-init data if provided
<#if spec.cloudInit??>

View file

@ -51,7 +51,6 @@ import org.jgrapes.util.events.ConfigurationUpdate;
* @param <O> the object type for the context
* @param <L> the object list type for the context
*/
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis" })
public abstract class AbstractMonitor<O extends KubernetesObject,
L extends KubernetesListObject, C extends Channel> extends Component {
@ -181,7 +180,6 @@ public abstract class AbstractMonitor<O extends KubernetesObject,
* @param event the event
*/
@Handler(priority = 10)
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
public void onStart(Start event) {
try {
// Get namespace
@ -199,8 +197,6 @@ public abstract class AbstractMonitor<O extends KubernetesObject,
assert client != null;
assert context != null;
assert namespace != null;
logger.fine(() -> "Observing " + K8s.toString(context)
+ " objects in " + namespace);
// Monitor all versions
for (var version : context.getVersions()) {
@ -219,12 +215,7 @@ public abstract class AbstractMonitor<O extends KubernetesObject,
observerCounter.incrementAndGet();
new K8sObserver<>(objectClass, objectListClass, client,
K8s.preferred(context, version), namespace, options)
.handler((c, r) -> {
logger.fine(() -> "Resource " + context.getKind()
+ "/" + r.object.getMetadata().getName() + " "
+ r.type);
handleChange(c, r);
}).onTerminated((o, t) -> {
.handler(this::handleChange).onTerminated((o, t) -> {
if (observerCounter.decrementAndGet() == 0) {
unregisterAsGenerator();
}

View file

@ -56,7 +56,6 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
/**
* Delegee for reconciling the config map
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
/* default */ class ConfigMapReconciler {
protected final Logger logger = Logger.getLogger(getClass().getName());
@ -76,15 +75,31 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
*
* @param model the model
* @param channel the channel
* @param modelChanged the model has changed
* @throws IOException Signals that an I/O exception has occurred.
* @throws TemplateException the template exception
* @throws ApiException the api exception
* @throws ApiException the API exception
*/
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
public void reconcile(Map<String, Object> model, VmChannel channel)
public void reconcile(Map<String, Object> model, VmChannel channel,
boolean modelChanged)
throws IOException, TemplateException, ApiException {
// Check if an update is needed
var prevData = channel.associated(PrevData.class)
.orElseGet(() -> new PrevData(null, new HashMap<>()));
Object newInputs = model.get("loginRequestedFor");
if (!modelChanged && Objects.equals(prevData.inputs, newInputs)) {
// Make added data available in new model
model.putAll(prevData.added);
return;
}
prevData = new PrevData(newInputs, prevData.added);
channel.setAssociated(PrevData.class, prevData);
// Combine template and data and parse result
logger.fine(() -> "Create/update configmap "
+ DataPath.<String> get(model, "cr", "name").orElse("unknown"));
model.put("adjustCloudInitMeta", adjustCloudInitMetaModel);
prevData.added.put("adjustCloudInitMeta", adjustCloudInitMetaModel);
var fmTemplate = fmConfig.getTemplate("runnerConfig.ftl.yaml");
StringWriter out = new StringWriter();
fmTemplate.process(model, out);
@ -107,19 +122,6 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
.get().addProperty("logging.properties", props);
});
// Look for changes
var oldCm = channel
.associated(getClass(), DynamicKubernetesObject.class).orElse(null);
channel.setAssociated(getClass(), newCm);
if (oldCm != null && Objects.equals(oldCm.getRaw().get("data"),
newCm.getRaw().get("data"))) {
logger.finer(() -> "No changes in config map for "
+ DataPath.<String> get(model, "cr", "name").get());
model.put("configMapResourceVersion",
oldCm.getMetadata().getResourceVersion());
return;
}
// Get API and update
DynamicKubernetesApi cmApi = new DynamicKubernetesApi("", "v1",
"configmaps", channel.client());
@ -129,6 +131,14 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
maybeForceUpdate(channel.client(), updatedCm);
model.put("configMapResourceVersion",
updatedCm.getMetadata().getResourceVersion());
prevData.added.put("configMapResourceVersion",
updatedCm.getMetadata().getResourceVersion());
}
/**
* Key for association.
*/
private record PrevData(Object inputs, Map<String, Object> added) {
}
/**
@ -177,7 +187,6 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
private final TemplateMethodModelEx adjustCloudInitMetaModel
= new TemplateMethodModelEx() {
@Override
@SuppressWarnings("PMD.PreserveStackTrace")
public Object exec(@SuppressWarnings("rawtypes") List arguments)
throws TemplateModelException {
@SuppressWarnings("unchecked")

View file

@ -21,7 +21,6 @@ package org.jdrupes.vmoperator.manager;
/**
* Some constants.
*/
@SuppressWarnings("PMD.DataClass")
public class Constants extends org.jdrupes.vmoperator.common.Constants {
/** The Constant STATE_RUNNING. */

View file

@ -1,6 +1,6 @@
/*
* VM-Operator
* Copyright (C) 2023 Michael N. Lipp
* Copyright (C) 2023, 2025 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
@ -20,29 +20,37 @@ package org.jdrupes.vmoperator.manager;
import com.google.gson.JsonObject;
import io.kubernetes.client.apimachinery.GroupVersionKind;
import io.kubernetes.client.custom.V1Patch;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.Configuration;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.Comparator;
import java.util.Optional;
import java.util.logging.Level;
import org.jdrupes.vmoperator.common.Constants.Crd;
import org.jdrupes.vmoperator.common.Constants.Status;
import org.jdrupes.vmoperator.common.K8sClient;
import org.jdrupes.vmoperator.common.K8sDynamicStub;
import org.jdrupes.vmoperator.common.K8sObserver.ResponseType;
import org.jdrupes.vmoperator.common.VmDefinition.Assignment;
import org.jdrupes.vmoperator.common.VmDefinitionStub;
import org.jdrupes.vmoperator.common.VmPool;
import org.jdrupes.vmoperator.manager.events.AssignVm;
import org.jdrupes.vmoperator.manager.events.ChannelManager;
import org.jdrupes.vmoperator.manager.events.Exit;
import org.jdrupes.vmoperator.manager.events.GetPools;
import org.jdrupes.vmoperator.manager.events.GetVms;
import org.jdrupes.vmoperator.manager.events.GetVms.VmData;
import org.jdrupes.vmoperator.manager.events.ModifyVm;
import org.jdrupes.vmoperator.manager.events.PodChanged;
import org.jdrupes.vmoperator.manager.events.UpdateAssignment;
import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jdrupes.vmoperator.util.GsonPtr;
import org.jdrupes.vmoperator.manager.events.VmPoolChanged;
import org.jdrupes.vmoperator.manager.events.VmResourceChanged;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Component;
import org.jgrapes.core.EventPipeline;
import org.jgrapes.core.annotation.Handler;
import org.jgrapes.core.events.HandlingError;
import org.jgrapes.core.events.Start;
@ -54,7 +62,7 @@ import org.jgrapes.util.events.ConfigurationUpdate;
*
* The implementation splits the controller in two components. The
* {@link VmMonitor} and the {@link Reconciler}. The former watches
* the VM definitions (CRs) and generates {@link VmDefChanged} events
* the VM definitions (CRs) and generates {@link VmResourceChanged} events
* when they change. The latter handles the changes and reconciles the
* resources in the cluster.
*
@ -87,6 +95,7 @@ import org.jgrapes.util.events.ConfigurationUpdate;
public class Controller extends Component {
private String namespace;
private final ChannelManager<String, VmChannel, EventPipeline> chanMgr;
/**
* Creates a new instance.
@ -95,8 +104,7 @@ public class Controller extends Component {
public Controller(Channel componentChannel) {
super(componentChannel);
// Prepare component tree
ChannelManager<String, VmChannel, ?> chanMgr
= new ChannelManager<>(name -> {
chanMgr = new ChannelManager<>(name -> {
try {
return new VmChannel(channel(), newEventPipeline(),
new K8sClient());
@ -113,6 +121,7 @@ public class Controller extends Component {
// attach(new ServiceMonitor(channel()).channelManager(chanMgr));
attach(new Reconciler(channel()));
attach(new PoolMonitor(channel()));
attach(new PodMonitor(channel(), chanMgr));
}
/**
@ -174,77 +183,146 @@ public class Controller extends Component {
fire(new Exit(2));
return;
}
logger.fine(() -> "Controlling namespace \"" + namespace + "\".");
logger.config(() -> "Controlling namespace \"" + namespace + "\".");
}
/**
* On modify vm.
* Returns the VM data.
*
* @param event the event
*/
@Handler
public void onGetVms(GetVms event) {
event.setResult(chanMgr.channels().stream()
.filter(c -> event.name().isEmpty()
|| c.vmDefinition().name().equals(event.name().get()))
.filter(c -> event.user().isEmpty() && event.roles().isEmpty()
|| !c.vmDefinition().permissionsFor(event.user().orElse(null),
event.roles()).isEmpty())
.filter(c -> event.fromPool().isEmpty()
|| c.vmDefinition().assignment().map(Assignment::pool)
.map(p -> p.equals(event.fromPool().get())).orElse(false))
.filter(c -> event.toUser().isEmpty()
|| c.vmDefinition().assignment().map(Assignment::user)
.map(u -> u.equals(event.toUser().get())).orElse(false))
.map(c -> new VmData(c.vmDefinition(), c))
.toList());
}
/**
* Assign a VM if not already assigned.
*
* @param event the event
* @throws ApiException the api exception
* @throws IOException Signals that an I/O exception has occurred.
* @throws InterruptedException
*/
@Handler
public void onModifyVm(ModifyVm event, VmChannel channel)
throws ApiException, IOException {
patchVmDef(channel.client(), event.name(), "spec/vm/" + event.path(),
event.value());
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
public void onAssignVm(AssignVm event)
throws ApiException, InterruptedException {
while (true) {
// Search for existing assignment.
var vmQuery = chanMgr.channels().stream()
.filter(c -> c.vmDefinition().assignment().map(Assignment::pool)
.map(p -> p.equals(event.fromPool())).orElse(false))
.filter(c -> c.vmDefinition().assignment().map(Assignment::user)
.map(u -> u.equals(event.toUser())).orElse(false))
.findFirst();
if (vmQuery.isPresent()) {
var vmDef = vmQuery.get().vmDefinition();
event.setResult(new VmData(vmDef, vmQuery.get()));
return;
}
private void patchVmDef(K8sClient client, String name, String path,
Object value) throws ApiException, IOException {
var vmStub = K8sDynamicStub.get(client,
new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM), namespace,
name);
// Get the pool definition for checking possible assignment
VmPool vmPool = newEventPipeline().fire(new GetPools()
.withName(event.fromPool())).get().stream().findFirst()
.orElse(null);
if (vmPool == null) {
return;
}
// Patch running
String valueAsText = value instanceof String
? "\"" + value + "\""
: value.toString();
var res = vmStub.patch(V1Patch.PATCH_FORMAT_JSON_PATCH,
new V1Patch("[{\"op\": \"replace\", \"path\": \"/"
+ path + "\", \"value\": " + valueAsText + "}]"),
client.defaultPatchOptions());
if (!res.isPresent()) {
logger.warning(
() -> "Cannot patch definition for Vm " + vmStub.name());
// Find available VM.
vmQuery = chanMgr.channels().stream()
.filter(c -> vmPool.isAssignable(c.vmDefinition()))
.sorted(Comparator.comparing((VmChannel c) -> c.vmDefinition()
.assignment().map(Assignment::lastUsed)
.orElse(Instant.ofEpochSecond(0)))
.thenComparing(preferRunning))
.findFirst();
// None found
if (vmQuery.isEmpty()) {
return;
}
// Assign to user
var chosenVm = vmQuery.get();
if (Optional.ofNullable(chosenVm.fire(new UpdateAssignment(
vmPool, event.toUser())).get()).orElse(false)) {
var vmDef = chosenVm.vmDefinition();
event.setResult(new VmData(vmDef, chosenVm));
// Make sure that a newly assigned VM is running.
chosenVm.fire(new ModifyVm(vmDef.name(), "state", "Running"));
return;
}
}
}
private static Comparator<VmChannel> preferRunning
= new Comparator<>() {
@Override
public int compare(VmChannel ch1, VmChannel ch2) {
if (ch1.vmDefinition().conditionStatus("Running").orElse(false)
&& !ch2.vmDefinition().conditionStatus("Running")
.orElse(false)) {
return -1;
}
return 0;
}
};
/**
* When s pool is deleted, remove all related assignments.
*
* @param event the event
* @throws InterruptedException
*/
@Handler
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
public void onPoolChanged(VmPoolChanged event) throws InterruptedException {
if (!event.deleted()) {
return;
}
var vms = newEventPipeline()
.fire(new GetVms().assignedFrom(event.vmPool().name())).get();
for (var vm : vms) {
vm.channel().fire(new UpdateAssignment(event.vmPool(), null));
}
}
/**
* Attempt to Update the assignment information in the status of the
* VM CR. Returns true if successful. The handler does not attempt
* retries, because in case of failure it will be necessary to
* re-evaluate the chosen VM.
* Remove runner version from status when pod is deleted
*
* @param event the event
* @param channel the channel
* @throws ApiException the api exception
*/
@Handler
public void onUpdatedAssignment(UpdateAssignment event, VmChannel channel)
public void onPodChange(PodChanged event, VmChannel channel)
throws ApiException {
try {
if (event.type() == ResponseType.DELETED) {
// Remove runner info from status
var vmDef = channel.vmDefinition();
var vmStub = VmDefinitionStub.get(channel.client(),
new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM),
vmDef.namespace(), vmDef.name());
if (vmStub.updateStatus(vmDef, from -> {
vmStub.updateStatus(from -> {
JsonObject status = from.statusJson();
var assignment = GsonPtr.to(status).to(Status.ASSIGNMENT);
assignment.set("pool", event.fromPool().name());
assignment.set("user", event.toUser());
assignment.set("lastUsed", Instant.now().toString());
status.remove(Status.RUNNER_VERSION);
return status;
}).isPresent()) {
event.setResult(true);
});
}
} catch (ApiException e) {
// Log exceptions except for conflict, which can be expected
if (HttpURLConnection.HTTP_CONFLICT != e.getCode()) {
throw e;
}
}
event.setResult(false);
}
}

View file

@ -42,7 +42,6 @@ import org.jgrapes.core.Channel;
* of the pod running the VM in response to force an update of the files
* in the pod that reflect the information from the secret.
*/
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.TooManyStaticImports" })
public class DisplaySecretMonitor
extends AbstractMonitor<V1Secret, V1SecretList, VmChannel> {

View file

@ -45,7 +45,7 @@ import org.jdrupes.vmoperator.common.VmDefinition;
import org.jdrupes.vmoperator.common.VmDefinitionStub;
import org.jdrupes.vmoperator.manager.events.GetDisplaySecret;
import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jdrupes.vmoperator.manager.events.VmResourceChanged;
import org.jdrupes.vmoperator.util.DataPath;
import org.jgrapes.core.Channel;
import org.jgrapes.core.CompletionLock;
@ -66,7 +66,6 @@ import org.jose4j.base64url.Base64;
* * `passwordValidity`: the validity of the random password in seconds.
* Used to calculate the password expiry time in the generated secret.
*/
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.TooManyStaticImports" })
public class DisplaySecretReconciler extends Component {
protected final Logger logger = Logger.getLogger(getClass().getName());
@ -104,12 +103,15 @@ public class DisplaySecretReconciler extends Component {
return oldConfig;
}).ifPresent(c -> {
try {
if (c.containsKey("passwordValidity")) {
passwordValidity = Integer
.parseInt((String) c.get("passwordValidity"));
}
} catch (ClassCastException e) {
logger.config("Malformed configuration: " + e.getMessage());
Optional.ofNullable(c.get("passwordValidity"))
.map(p -> p instanceof Integer ? (Integer) p
: Integer.valueOf((String) p))
.ifPresent(p -> {
passwordValidity = p;
});
} catch (NumberFormatException e) {
logger.warning(
() -> "Malformed configuration: " + e.getMessage());
}
});
}
@ -120,25 +122,30 @@ public class DisplaySecretReconciler extends Component {
* secret with a random password and immediate expiration, thus
* preventing access to the display.
*
* @param event the event
* @param vmDef the VM definition
* @param model the model
* @param channel the channel
* @param specChanged the spec changed
* @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)
public void reconcile(VmDefinition vmDef, Map<String, Object> model,
VmChannel channel, boolean specChanged)
throws IOException, TemplateException, ApiException {
// Nothing to do unless spec changed
if (!specChanged) {
return;
}
// Secret needed at all?
var display = event.vmDefinition().fromVm("display").get();
var display = vmDef.fromVm("display").get();
if (!DataPath.<Boolean> get(display, "spice", "generateSecret")
.orElse(true)) {
return;
}
// Check if exists
var vmDef = event.vmDefinition();
ListOptions options = new ListOptions();
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
+ "app.kubernetes.io/component=" + DisplaySecret.NAME + ","
@ -150,9 +157,11 @@ public class DisplaySecretReconciler extends Component {
}
// Create secret
var secretName = vmDef.name() + "-" + DisplaySecret.NAME;
logger.fine(() -> "Create/update secret " + secretName);
var secret = new V1Secret();
secret.setMetadata(new V1ObjectMeta().namespace(vmDef.namespace())
.name(vmDef.name() + "-" + DisplaySecret.NAME)
.name(secretName)
.putLabelsItem("app.kubernetes.io/name", APP_NAME)
.putLabelsItem("app.kubernetes.io/component", DisplaySecret.NAME)
.putLabelsItem("app.kubernetes.io/instance", vmDef.name()));
@ -183,7 +192,6 @@ public class DisplaySecretReconciler extends Component {
* @throws ApiException the api exception
*/
@Handler
@SuppressWarnings("PMD.StringInstantiation")
public void onGetDisplaySecret(GetDisplaySecret event, VmChannel channel)
throws ApiException {
// Get VM definition and check if running
@ -293,7 +301,7 @@ public class DisplaySecretReconciler extends Component {
*/
@Handler
@SuppressWarnings("PMD.AvoidSynchronizedStatement")
public void onVmDefChanged(VmDefChanged event, Channel channel) {
public void onVmResourceChanged(VmResourceChanged event, Channel channel) {
synchronized (pendingPrepares) {
String vmName = event.vmDefinition().name();
for (var pending : pendingPrepares) {
@ -312,7 +320,6 @@ public class DisplaySecretReconciler extends Component {
/**
* The Class PendingGet.
*/
@SuppressWarnings("PMD.DataClass")
private static class PendingRequest {
public final GetDisplaySecret event;
public final long expectedSerial;

View file

@ -36,7 +36,7 @@ import java.util.logging.Logger;
import org.jdrupes.vmoperator.common.K8sV1ServiceStub;
import org.jdrupes.vmoperator.common.VmDefinition;
import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jdrupes.vmoperator.util.DataPath;
import org.jdrupes.vmoperator.util.GsonPtr;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
@ -45,7 +45,6 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
/**
* Delegee for reconciling the service
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
/* default */ class LoadBalancerReconciler {
private static final String LOAD_BALANCER_SERVICE = "loadBalancerService";
@ -69,18 +68,24 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
/**
* Reconcile.
*
* @param event the event
* @param vmDef the VM definition
* @param model the model
* @param channel the channel
* @param specChanged the spec changed
* @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)
public void reconcile(VmDefinition vmDef, Map<String, Object> model,
VmChannel channel, boolean specChanged)
throws IOException, TemplateException, ApiException {
// Nothing to do unless spec changed
if (!specChanged) {
return;
}
// Check if to be generated
@SuppressWarnings({ "PMD.AvoidDuplicateLiterals", "unchecked" })
@SuppressWarnings({ "unchecked" })
var lbsDef = Optional.of(model)
.map(m -> (Map<String, Object>) m.get("reconciler"))
.map(c -> c.get(LOAD_BALANCER_SERVICE)).orElse(Boolean.FALSE);
@ -95,7 +100,6 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
}
// Load balancer can also be turned off for VM
var vmDef = event.vmDefinition();
if (vmDef
.<Map<String, Map<String, String>>> fromSpec(LOAD_BALANCER_SERVICE)
.map(m -> m.isEmpty()).orElse(false)) {
@ -103,6 +107,8 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
}
// Combine template and data and parse result
logger.fine(() -> "Create/update load balancer service for "
+ DataPath.<String> get(model, "cr", "name").orElse("unknown"));
var fmTemplate = fmConfig.getTemplate("runnerLoadBalancer.ftl.yaml");
StringWriter out = new StringWriter();
fmTemplate.process(model, out);

View file

@ -81,7 +81,7 @@ import org.jgrapes.webconsole.vuejs.VueJsConsoleWeblet;
/**
* The application class.
*/
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports" })
@SuppressWarnings({ "PMD.ExcessiveImports" })
public class Manager extends Component {
private static String version;
@ -97,8 +97,8 @@ public class Manager extends Component {
* @throws IOException Signals that an I/O exception has occurred.
* @throws URISyntaxException
*/
@SuppressWarnings({ "PMD.TooFewBranchesForASwitchStatement",
"PMD.NcssCount", "PMD.ConstructorCallsOverridableMethod" })
@SuppressWarnings({ "PMD.NcssCount",
"PMD.ConstructorCallsOverridableMethod" })
public Manager(CommandLine cmdLine) throws IOException, URISyntaxException {
super(new NamedChannel("manager"));
// Prepare component tree
@ -217,7 +217,6 @@ public class Manager extends Component {
* @param event the event
*/
@Handler
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public void onConfigurationUpdate(ConfigurationUpdate event) {
event.structured(componentPath()).ifPresent(c -> {
if (c.containsKey("clusterName")) {
@ -264,7 +263,7 @@ public class Manager extends Component {
*/
@Handler(priority = -1000)
public void onStop(Stop event) {
logger.fine(() -> "Application stopped.");
logger.info(() -> "Application stopped.");
}
static {
@ -291,7 +290,6 @@ public class Manager extends Component {
* @param args the arguments
* @throws Exception the exception
*/
@SuppressWarnings("PMD.SignatureDeclareThrowsException")
public static void main(String[] args) {
try {
// Instance logger is not available yet.

View file

@ -0,0 +1,139 @@
/*
* VM-Operator
* Copyright (C) 2025 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.V1Pod;
import io.kubernetes.client.openapi.models.V1PodList;
import io.kubernetes.client.util.Watch.Response;
import io.kubernetes.client.util.generic.options.ListOptions;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_NAME;
import org.jdrupes.vmoperator.common.K8sClient;
import org.jdrupes.vmoperator.common.K8sObserver.ResponseType;
import org.jdrupes.vmoperator.common.K8sV1PodStub;
import org.jdrupes.vmoperator.manager.events.ChannelDictionary;
import org.jdrupes.vmoperator.manager.events.PodChanged;
import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmResourceChanged;
import org.jgrapes.core.Channel;
import org.jgrapes.core.annotation.Handler;
/**
* Watches for changes of pods that run VMs.
*/
public class PodMonitor extends AbstractMonitor<V1Pod, V1PodList, VmChannel> {
private final ChannelDictionary<String, VmChannel, ?> channelDictionary;
private final Map<String, PendingChange> pendingChanges
= new ConcurrentHashMap<>();
/**
* Instantiates a new pod monitor.
*
* @param componentChannel the component channel
* @param channelDictionary the channel dictionary
*/
public PodMonitor(Channel componentChannel,
ChannelDictionary<String, VmChannel, ?> channelDictionary) {
super(componentChannel, V1Pod.class, V1PodList.class);
this.channelDictionary = channelDictionary;
context(K8sV1PodStub.CONTEXT);
ListOptions options = new ListOptions();
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
+ "app.kubernetes.io/component=" + APP_NAME + ","
+ "app.kubernetes.io/managed-by=" + VM_OP_NAME);
options(options);
}
@Override
protected void prepareMonitoring() throws IOException, ApiException {
client(new K8sClient());
}
@Override
protected void handleChange(K8sClient client, Response<V1Pod> change) {
String vmName = change.object.getMetadata().getLabels()
.get("app.kubernetes.io/instance");
if (vmName == null) {
return;
}
var channel = channelDictionary.channel(vmName).orElse(null);
var responseType = ResponseType.valueOf(change.type);
if (channel != null && channel.vmDefinition() != null) {
pendingChanges.remove(vmName);
channel.fire(new PodChanged(change.object, responseType));
return;
}
// VM definition not available yet, may happen during startup
if (responseType == ResponseType.DELETED) {
return;
}
purgePendingChanges();
logger.finer(() -> "Add pending pod change for " + vmName);
pendingChanges.put(vmName, new PendingChange(Instant.now(), change));
}
private void purgePendingChanges() {
Instant tooOld = Instant.now().minus(Duration.ofMinutes(15));
for (var itr = pendingChanges.entrySet().iterator(); itr.hasNext();) {
var change = itr.next();
if (change.getValue().from().isBefore(tooOld)) {
itr.remove();
logger.finer(
() -> "Cleaned pending pod change for " + change.getKey());
}
}
}
/**
* Check for pending changes.
*
* @param event the event
* @param channel the channel
*/
@Handler
public void onVmResourceChanged(VmResourceChanged event,
VmChannel channel) {
Optional.ofNullable(pendingChanges.remove(event.vmDefinition().name()))
.map(PendingChange::change).ifPresent(change -> {
logger.finer(() -> "Firing pending pod change for "
+ event.vmDefinition().name());
channel.fire(new PodChanged(change.object,
ResponseType.valueOf(change.type)));
if (logger.isLoggable(Level.FINER)
&& pendingChanges.isEmpty()) {
logger.finer("No pending pod changes left.");
}
});
}
private record PendingChange(Instant from, Response<V1Pod> change) {
}
}

View file

@ -22,15 +22,20 @@ import freemarker.template.Configuration;
import freemarker.template.TemplateException;
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.logging.Logger;
import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
import org.jdrupes.vmoperator.common.Constants.DisplaySecret;
import org.jdrupes.vmoperator.common.K8sClient;
import org.jdrupes.vmoperator.common.K8sV1PodStub;
import org.jdrupes.vmoperator.common.K8sV1SecretStub;
import org.jdrupes.vmoperator.common.VmDefinition;
import org.jdrupes.vmoperator.common.VmDefinition.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;
@ -38,7 +43,6 @@ 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());
@ -56,23 +60,18 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
/**
* Reconcile the pod.
*
* @param event the event
* @param vmDef the vm def
* @param model the model
* @param channel the channel
* @param specChanged the spec changed
* @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)
public void reconcile(VmDefinition vmDef, Map<String, Object> model,
VmChannel channel, boolean specChanged)
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 vmDef = event.vmDefinition();
var podStub = K8sV1PodStub.get(channel.client(), vmDef.namespace(),
vmDef.name());
@ -92,6 +91,7 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
// Create pod. First combine template and data and parse result
logger.fine(() -> "Create/update pod " + podStub.name());
addDisplaySecret(channel.client(), model, vmDef);
var fmTemplate = fmConfig.getTemplate("runnerPod.ftl.yaml");
StringWriter out = new StringWriter();
fmTemplate.process(model, out);
@ -110,4 +110,19 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
}
}
private void addDisplaySecret(K8sClient client, Map<String, Object> model,
VmDefinition vmDef) throws ApiException {
ListOptions options = new ListOptions();
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
+ "app.kubernetes.io/component=" + DisplaySecret.NAME + ","
+ "app.kubernetes.io/instance=" + vmDef.name());
var dsStub = K8sV1SecretStub
.list(client, vmDef.namespace(), options).stream().findFirst();
if (dsStub.isPresent()) {
dsStub.get().model().ifPresent(m -> {
model.put("displaySecret", m.getMetadata().getName());
});
}
}
}

View file

@ -40,8 +40,8 @@ import org.jdrupes.vmoperator.common.VmDefinition.Assignment;
import org.jdrupes.vmoperator.common.VmDefinitionStub;
import org.jdrupes.vmoperator.common.VmPool;
import org.jdrupes.vmoperator.manager.events.GetPools;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jdrupes.vmoperator.manager.events.VmPoolChanged;
import org.jdrupes.vmoperator.manager.events.VmResourceChanged;
import org.jdrupes.vmoperator.util.GsonPtr;
import org.jgrapes.core.Channel;
import org.jgrapes.core.EventPipeline;
@ -53,7 +53,6 @@ import org.jgrapes.core.events.Attached;
* {@link VmPoolChanged} events fired on a special pipeline to
* avoid concurrent change informations.
*/
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports" })
public class PoolMonitor extends
AbstractMonitor<K8sDynamicModel, K8sDynamicModels, Channel> {
@ -142,7 +141,8 @@ public class PoolMonitor extends
* @throws ApiException
*/
@Handler
public void onVmDefChanged(VmDefChanged event) throws ApiException {
public void onVmResourceChanged(VmResourceChanged event)
throws ApiException {
final var vmDef = event.vmDefinition();
final String vmName = vmDef.name();
switch (event.type()) {

View file

@ -38,8 +38,8 @@ 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.common.VmDefinition;
import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jdrupes.vmoperator.util.DataPath;
import org.jdrupes.vmoperator.util.GsonPtr;
import org.yaml.snakeyaml.LoaderOptions;
@ -49,7 +49,6 @@ 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());
@ -67,32 +66,35 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
/**
* Reconcile the PVCs.
*
* @param event the event
* @param vmDef the VM definition
* @param model the model
* @param channel the channel
* @param specChanged the spec changed
* @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)
@SuppressWarnings({ "unchecked" })
public void reconcile(VmDefinition vmDef, Map<String, Object> model,
VmChannel channel, boolean specChanged)
throws IOException, TemplateException, ApiException {
var vmDef = event.vmDefinition();
// Existing disks
Set<String> knownPvcs;
if (!specChanged && channel.associated(this, Set.class).isPresent()) {
knownPvcs = (Set<String>) channel.associated(this, Set.class).get();
} else {
ListOptions listOpts = new ListOptions();
listOpts.setLabelSelector(
"app.kubernetes.io/managed-by=" + VM_OP_NAME + ","
+ "app.kubernetes.io/name=" + APP_NAME + ","
+ "app.kubernetes.io/instance=" + vmDef.name());
var knownDisks = K8sV1PvcStub.list(channel.client(),
vmDef.namespace(), listOpts);
var knownPvcs = knownDisks.stream().map(K8sV1PvcStub::name)
knownPvcs = K8sV1PvcStub.list(channel.client(),
vmDef.namespace(), listOpts).stream().map(K8sV1PvcStub::name)
.collect(Collectors.toSet());
channel.setAssociated(this, knownPvcs);
}
// Reconcile runner data pvc
reconcileRunnerDataPvc(event, model, channel, knownPvcs);
reconcileRunnerDataPvc(vmDef, model, channel, knownPvcs, specChanged);
// Reconcile pvcs for defined disks
var diskDefs = vmDef.<List<Map<String, Object>>> fromVm("disks")
@ -116,18 +118,15 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
}
// Update PVC
model.put("disk", diskDef);
reconcileRunnerDiskPvc(event, model, channel);
reconcileRunnerDiskPvc(vmDef, model, channel, specChanged, diskDef);
}
model.remove("disk");
}
private void reconcileRunnerDataPvc(VmDefChanged event,
private void reconcileRunnerDataPvc(VmDefinition vmDef,
Map<String, Object> model, VmChannel channel,
Set<String> knownPvcs)
Set<String> knownPvcs, boolean specChanged)
throws TemplateNotFoundException, MalformedTemplateNameException,
ParseException, IOException, TemplateException, ApiException {
var vmDef = event.vmDefinition();
// Look for old (sts generated) name.
var stsRunnerDataPvcName
@ -138,7 +137,13 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
}
// Generate PVC
model.put("runnerDataPvcName", vmDef.name() + "-runner-data");
var runnerDataPvcName = vmDef.name() + "-runner-data";
logger.fine(() -> "Create/update pvc " + runnerDataPvcName);
model.put("runnerDataPvcName", runnerDataPvcName);
if (!specChanged) {
// Augmenting the model is all we have to do
return;
}
var fmTemplate = fmConfig.getTemplate("runnerDataPvc.ftl.yaml");
StringWriter out = new StringWriter();
fmTemplate.process(model, out);
@ -161,20 +166,26 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
}
}
private void reconcileRunnerDiskPvc(VmDefChanged event,
Map<String, Object> model, VmChannel channel)
private void reconcileRunnerDiskPvc(VmDefinition vmDef,
Map<String, Object> model, VmChannel channel, boolean specChanged,
Map<String, Object> diskDef)
throws TemplateNotFoundException, MalformedTemplateNameException,
ParseException, IOException, TemplateException, ApiException {
var vmDef = event.vmDefinition();
// Generate PVC
@SuppressWarnings("unchecked")
var diskDef = (Map<String, Object>) model.get("disk");
var pvcName = vmDef.name() + "-" + diskDef.get("generatedDiskName");
diskDef.put("generatedPvcName", pvcName);
if (!specChanged) {
// Augmenting the model is all we have to do
return;
}
// Generate PVC
logger.fine(() -> "Create/update pvc " + pvcName);
model.put("disk", diskDef);
var fmTemplate = fmConfig.getTemplate("runnerDiskPvc.ftl.yaml");
StringWriter out = new StringWriter();
fmTemplate.process(model, out);
model.remove("disk");
// Avoid Yaml.load due to
// https://github.com/kubernetes-client/java/issues/2741
var pvcDef = Dynamics.newFromYaml(

View file

@ -30,7 +30,6 @@ import freemarker.template.TemplateMethodModelEx;
import freemarker.template.TemplateModelException;
import io.kubernetes.client.custom.Quantity;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.util.generic.options.ListOptions;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
@ -43,19 +42,15 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
import org.jdrupes.vmoperator.common.Constants.DisplaySecret;
import org.jdrupes.vmoperator.common.Convertions;
import org.jdrupes.vmoperator.common.K8sClient;
import org.jdrupes.vmoperator.common.K8sObserver;
import org.jdrupes.vmoperator.common.K8sV1SecretStub;
import org.jdrupes.vmoperator.common.VmDefinition;
import org.jdrupes.vmoperator.common.VmDefinition.Assignment;
import org.jdrupes.vmoperator.common.VmPool;
import org.jdrupes.vmoperator.manager.events.GetPools;
import org.jdrupes.vmoperator.manager.events.ResetVm;
import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jdrupes.vmoperator.manager.events.VmResourceChanged;
import org.jdrupes.vmoperator.util.ExtendedObjectWrapper;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Component;
@ -142,19 +137,16 @@ import org.jgrapes.util.events.ConfigurationUpdate;
*
* @see org.jdrupes.vmoperator.manager.DisplaySecretReconciler
*/
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis",
"PMD.AvoidDuplicateLiterals" })
@SuppressWarnings({ "PMD.AvoidDuplicateLiterals" })
public class Reconciler extends Component {
/** The Constant mapper. */
@SuppressWarnings("PMD.FieldNamingConventions")
protected static final ObjectMapper mapper = new ObjectMapper();
@SuppressWarnings("PMD.SingularField")
private final Configuration fmConfig;
private final ConfigMapReconciler cmReconciler;
private final DisplaySecretReconciler dsReconciler;
private final StatefulSetReconciler stsReconciler;
private final PvcReconciler pvcReconciler;
private final PodReconciler podReconciler;
private final LoadBalancerReconciler lbReconciler;
@ -182,7 +174,6 @@ public class Reconciler extends Component {
cmReconciler = new ConfigMapReconciler(fmConfig);
dsReconciler = attach(new DisplaySecretReconciler(componentChannel));
stsReconciler = new StatefulSetReconciler(fmConfig);
pvcReconciler = new PvcReconciler(fmConfig);
podReconciler = new PodReconciler(fmConfig);
lbReconciler = new LoadBalancerReconciler(fmConfig);
@ -210,8 +201,7 @@ public class Reconciler extends Component {
* @throws IOException Signals that an I/O exception has occurred.
*/
@Handler
@SuppressWarnings("PMD.ConfusingTernary")
public void onVmDefChanged(VmDefChanged event, VmChannel channel)
public void onVmResourceChanged(VmResourceChanged event, VmChannel channel)
throws ApiException, TemplateException, IOException {
// Ownership relationships takes care of deletions
if (event.type() == K8sObserver.ResponseType.DELETED) {
@ -219,20 +209,19 @@ public class Reconciler extends Component {
}
// Create model for processing templates
Map<String, Object> model
= prepareModel(channel.client(), event.vmDefinition());
cmReconciler.reconcile(model, channel);
var vmDef = event.vmDefinition();
Map<String, Object> model = prepareModel(vmDef);
cmReconciler.reconcile(model, channel, event.specChanged());
// The remaining reconcilers depend only on changes of the spec part.
if (!event.specChanged()) {
// The remaining reconcilers depend only on changes of the spec part
// or the pod state.
if (!event.specChanged() && !event.podChanged()) {
return;
}
dsReconciler.reconcile(event, model, channel);
// Manage (eventual) removal of stateful set.
stsReconciler.reconcile(event, model, channel);
pvcReconciler.reconcile(event, model, channel);
podReconciler.reconcile(event, model, channel);
lbReconciler.reconcile(event, model, channel);
dsReconciler.reconcile(vmDef, model, channel, event.specChanged());
pvcReconciler.reconcile(vmDef, model, channel, event.specChanged());
podReconciler.reconcile(vmDef, model, channel, event.specChanged());
lbReconciler.reconcile(vmDef, model, channel, event.specChanged());
}
/**
@ -249,15 +238,15 @@ public class Reconciler extends Component {
public void onResetVm(ResetVm event, VmChannel channel)
throws ApiException, IOException, TemplateException {
var vmDef = channel.vmDefinition();
vmDef.extra().ifPresent(e -> e.resetCount(e.resetCount() + 1));
var extra = vmDef.extra();
extra.resetCount(extra.resetCount() + 1);
Map<String, Object> model
= prepareModel(channel.client(), channel.vmDefinition());
cmReconciler.reconcile(model, channel);
= prepareModel(channel.vmDefinition());
cmReconciler.reconcile(model, channel, true);
}
@SuppressWarnings({ "PMD.CognitiveComplexity", "PMD.NPathComplexity" })
private Map<String, Object> prepareModel(K8sClient client,
VmDefinition vmDef) throws TemplateModelException, ApiException {
private Map<String, Object> prepareModel(VmDefinition vmDef)
throws TemplateModelException, ApiException {
@SuppressWarnings("PMD.UseConcurrentHashMap")
Map<String, Object> model = new HashMap<>();
model.put("managerVersion",
@ -267,7 +256,6 @@ public class Reconciler extends Component {
model.put("reconciler", config);
model.put("constants", constantsMap(Constants.class));
addLoginRequestedFor(model, vmDef);
addDisplaySecret(client, model, vmDef);
// Methods
model.put("parseQuantity", parseQuantityModel);
@ -325,21 +313,6 @@ public class Reconciler extends Component {
.ifPresent(u -> model.put("loginRequestedFor", u));
}
private void addDisplaySecret(K8sClient client, Map<String, Object> model,
VmDefinition vmDef) throws ApiException {
ListOptions options = new ListOptions();
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
+ "app.kubernetes.io/component=" + DisplaySecret.NAME + ","
+ "app.kubernetes.io/instance=" + vmDef.name());
var dsStub = K8sV1SecretStub
.list(client, vmDef.namespace(), options).stream().findFirst();
if (dsStub.isPresent()) {
dsStub.get().model().ifPresent(m -> {
model.put("displaySecret", m.getMetadata().getName());
});
}
}
private final TemplateMethodModelEx parseQuantityModel
= new TemplateMethodModelEx() {
@Override
@ -362,7 +335,6 @@ public class Reconciler extends Component {
private final TemplateMethodModelEx formatMemoryModel
= new TemplateMethodModelEx() {
@Override
@SuppressWarnings("PMD.PreserveStackTrace")
public Object exec(@SuppressWarnings("rawtypes") List arguments)
throws TemplateModelException {
var arg = arguments.get(0);
@ -392,8 +364,7 @@ public class Reconciler extends Component {
private final TemplateMethodModelEx imgageLocationModel
= new TemplateMethodModelEx() {
@Override
@SuppressWarnings({ "PMD.PreserveStackTrace",
"PMD.AvoidLiteralsInIfCondition" })
@SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition" })
public Object exec(@SuppressWarnings("rawtypes") List arguments)
throws TemplateModelException {
var image = ((SimpleScalar) arguments.get(0)).getAsString();
@ -418,7 +389,6 @@ public class Reconciler extends Component {
private final TemplateMethodModelEx toJsonModel
= new TemplateMethodModelEx() {
@Override
@SuppressWarnings("PMD.PreserveStackTrace")
public Object exec(@SuppressWarnings("rawtypes") List arguments)
throws TemplateModelException {
try {

View file

@ -1,107 +0,0 @@
/*
* 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.options.PatchOptions;
import java.io.IOException;
import java.util.Map;
import java.util.logging.Logger;
import org.jdrupes.vmoperator.common.K8sV1StatefulSetStub;
import org.jdrupes.vmoperator.common.VmDefinition.RequestedVmState;
import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
/**
* 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")
/* default */ class StatefulSetReconciler {
protected final Logger logger = Logger.getLogger(getClass().getName());
/**
* Instantiates a new stateful set reconciler.
*
* @param fmConfig the fm config
*/
@SuppressWarnings("PMD.UnusedFormalParameter")
public StatefulSetReconciler(Configuration fmConfig) {
// Nothing to do
}
/**
* Reconcile stateful set.
*
* @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.AvoidLiteralsInIfCondition")
public void reconcile(VmDefChanged event, Map<String, Object> model,
VmChannel channel)
throws IOException, TemplateException, ApiException {
model.put("usingSts", false);
// If exists, delete when not running or supposed to be not running.
var stsStub = K8sV1StatefulSetStub.get(channel.client(),
event.vmDefinition().namespace(), event.vmDefinition().name());
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.
if (event.vmDefinition().vmState() == RequestedVmState.RUNNING) {
return;
}
// Do apply changes (set replicas to 0)
PatchOptions opts = new PatchOptions();
opts.setForce(true);
opts.setFieldManager("kubernetes-java-kubectl-apply");
if (stsStub.patch(V1Patch.PATCH_FORMAT_JSON_PATCH,
new V1Patch("[{\"op\": \"replace\", \"path\": \"/spec/replicas"
+ "\", \"value\": 0}]"),
channel.client().defaultPatchOptions()).isEmpty()) {
logger.warning(
() -> "Could not patch stateful set for " + stsStub.name());
}
}
}

View file

@ -18,56 +18,64 @@
package org.jdrupes.vmoperator.manager;
import com.google.gson.JsonObject;
import io.kubernetes.client.apimachinery.GroupVersionKind;
import io.kubernetes.client.custom.V1Patch;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.models.V1ObjectMeta;
import io.kubernetes.client.util.Watch;
import io.kubernetes.client.util.generic.options.ListOptions;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.stream.Collectors;
import org.jdrupes.vmoperator.common.Constants.Crd;
import org.jdrupes.vmoperator.common.Constants.Status;
import org.jdrupes.vmoperator.common.K8s;
import org.jdrupes.vmoperator.common.K8sClient;
import org.jdrupes.vmoperator.common.K8sDynamicStub;
import org.jdrupes.vmoperator.common.K8sObserver.ResponseType;
import org.jdrupes.vmoperator.common.K8sV1ConfigMapStub;
import org.jdrupes.vmoperator.common.K8sV1PodStub;
import org.jdrupes.vmoperator.common.K8sV1StatefulSetStub;
import org.jdrupes.vmoperator.common.VmDefinition;
import org.jdrupes.vmoperator.common.VmDefinition.Assignment;
import org.jdrupes.vmoperator.common.VmDefinitionStub;
import org.jdrupes.vmoperator.common.VmDefinitions;
import org.jdrupes.vmoperator.common.VmExtraData;
import org.jdrupes.vmoperator.common.VmPool;
import static org.jdrupes.vmoperator.manager.Constants.APP_NAME;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME;
import org.jdrupes.vmoperator.manager.events.AssignVm;
import org.jdrupes.vmoperator.manager.events.ChannelManager;
import org.jdrupes.vmoperator.manager.events.GetPools;
import org.jdrupes.vmoperator.manager.events.GetVms;
import org.jdrupes.vmoperator.manager.events.GetVms.VmData;
import org.jdrupes.vmoperator.manager.events.ModifyVm;
import org.jdrupes.vmoperator.manager.events.PodChanged;
import org.jdrupes.vmoperator.manager.events.UpdateAssignment;
import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jdrupes.vmoperator.manager.events.VmResourceChanged;
import org.jdrupes.vmoperator.util.GsonPtr;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Event;
import org.jgrapes.core.EventPipeline;
import org.jgrapes.core.annotation.Handler;
/**
* Watches for changes of VM definitions.
* Watches for changes of VM definitions. When a VM definition (CR)
* becomes known, is is registered with a {@link ChannelManager} and thus
* gets an associated {@link VmChannel} and an associated
* {@link EventPipeline}.
*
* The {@link EventPipeline} is used for submitting an action that processes
* the change data from kubernetes, eventually transforming it to a
* {@link VmResourceChanged} event that is handled by another
* {@link EventPipeline} associated with the {@link VmChannel}. This
* event pipeline should be used for all events related to changes of
* a particular VM.
*/
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports" })
public class VmMonitor extends
AbstractMonitor<VmDefinition, VmDefinitions, VmChannel> {
private final ChannelManager<String, VmChannel, ?> channelManager;
private final ChannelManager<String, VmChannel,
EventPipeline> channelManager;
/**
* Instantiates a new VM definition watcher.
@ -76,7 +84,7 @@ public class VmMonitor extends
* @param channelManager the channel manager
*/
public VmMonitor(Channel componentChannel,
ChannelManager<String, VmChannel, ?> channelManager) {
ChannelManager<String, VmChannel, EventPipeline> channelManager) {
super(componentChannel, VmDefinition.class,
VmDefinitions.class);
this.channelManager = channelManager;
@ -98,7 +106,6 @@ public class VmMonitor extends
purge();
}
@SuppressWarnings("PMD.CognitiveComplexity")
private void purge() throws ApiException {
// Get existing CRs (VMs)
var known = K8sDynamicStub.list(client(), context(), namespace())
@ -123,14 +130,18 @@ public class VmMonitor extends
@Override
protected void handleChange(K8sClient client,
Watch.Response<VmDefinition> response) {
V1ObjectMeta metadata = response.object.getMetadata();
AtomicBoolean toBeAdded = new AtomicBoolean(false);
VmChannel channel = channelManager.channel(metadata.getName())
.orElseGet(() -> {
toBeAdded.set(true);
return channelManager.createChannel(metadata.getName());
});
var name = response.object.getMetadata().getName();
// Process the response data on a VM specific pipeline to
// increase concurrency when e.g. starting many VMs.
var preparing = channelManager.associated(name)
.orElseGet(() -> newEventPipeline());
preparing.submit("VmChange[" + name + "]",
() -> processChange(client, response, preparing));
}
private void processChange(K8sClient client,
Watch.Response<VmDefinition> response, EventPipeline preparing) {
// Get full definition and associate with channel as backup
var vmDef = response.object;
if (vmDef.data() == null) {
@ -138,9 +149,12 @@ public class VmMonitor extends
// https://github.com/kubernetes-client/java/issues/3215
vmDef = getModel(client, vmDef);
}
var name = response.object.getMetadata().getName();
var channel = channelManager.channel(name)
.orElseGet(() -> channelManager.createChannel(name));
if (vmDef.data() != null) {
// New data, augment and save
addExtraData(channel.client(), vmDef, channel.vmDefinition());
addExtraData(vmDef, channel.vmDefinition());
channel.setVmDefinition(vmDef);
} else {
// Reuse cached (e.g. if deleted)
@ -151,22 +165,20 @@ public class VmMonitor extends
+ response.object.getMetadata());
return;
}
if (toBeAdded.get()) {
channelManager.put(vmDef.name(), channel);
}
channelManager.put(name, channel, preparing);
// Create and fire changed event. Remove channel from channel
// manager on completion.
VmDefChanged chgEvt
= new VmDefChanged(ResponseType.valueOf(response.type),
VmResourceChanged chgEvt
= new VmResourceChanged(ResponseType.valueOf(response.type), vmDef,
channel.setGeneration(response.object.getMetadata()
.getGeneration()),
vmDef);
false);
if (ResponseType.valueOf(response.type) == ResponseType.DELETED) {
chgEvt = Event.onCompletion(chgEvt,
e -> channelManager.remove(e.vmDefinition().name()));
}
channel.pipeline().fire(chgEvt, channel);
channel.fire(chgEvt);
}
private VmDefinition getModel(K8sClient client, VmDefinition vmDef) {
@ -178,147 +190,137 @@ public class VmMonitor extends
}
}
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
private void addExtraData(K8sClient client, VmDefinition vmDef,
VmDefinition prevState) {
private void addExtraData(VmDefinition vmDef, VmDefinition prevState) {
var extra = new VmExtraData(vmDef);
var prevExtra = Optional.ofNullable(prevState).map(VmDefinition::extra);
// Maintain (or initialize) the resetCount
extra.resetCount(
Optional.ofNullable(prevState).flatMap(VmDefinition::extra)
.map(VmExtraData::resetCount).orElse(0L));
extra.resetCount(prevExtra.map(VmExtraData::resetCount).orElse(0L));
// VM definition status changes before the pod terminates.
// This results in pod information being shown for a stopped
// VM which is irritating. So check condition first.
if (!vmDef.conditionStatus("Running").orElse(false)) {
// Maintain node info
prevExtra
.ifPresent(e -> extra.nodeInfo(e.nodeName(), e.nodeAddresses()));
}
/**
* On pod changed.
*
* @param event the event
* @param channel the channel
*/
@Handler
public void onPodChanged(PodChanged event, VmChannel channel) {
var vmDef = channel.vmDefinition();
// Make sure that this is properly sync'd with VM CR changes.
channelManager.associated(vmDef.name())
.orElseGet(() -> activeEventPipeline())
.submit("NodeInfo[" + vmDef.name() + "]",
() -> {
updateNodeInfo(event, vmDef);
channel.fire(new VmResourceChanged(ResponseType.MODIFIED,
vmDef, false, true));
});
}
private void updateNodeInfo(PodChanged event, VmDefinition vmDef) {
var extra = vmDef.extra();
if (event.type() == ResponseType.DELETED) {
// The status of a deleted pod is the status before deletion,
// i.e. the node info is still cached and must be removed.
extra.nodeInfo("", Collections.emptyList());
return;
}
// Get pod and extract node information.
var podSearch = new ListOptions();
podSearch.setLabelSelector("app.kubernetes.io/name=" + APP_NAME
+ ",app.kubernetes.io/component=" + APP_NAME
+ ",app.kubernetes.io/instance=" + vmDef.name());
try {
var podList
= K8sV1PodStub.list(client, namespace(), podSearch);
for (var podStub : podList) {
var nodeName = podStub.model().get().getSpec().getNodeName();
// Get current node info from pod
var pod = event.pod();
var nodeName = Optional
.ofNullable(pod.getSpec().getNodeName()).orElse("");
logger.finer(() -> "Adding node name " + nodeName
+ " to VM info for " + vmDef.name());
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
var addrs = new ArrayList<String>();
podStub.model().get().getStatus().getPodIPs().stream()
Optional.ofNullable(pod.getStatus().getPodIPs())
.orElse(Collections.emptyList()).stream()
.map(ip -> ip.getIp()).forEach(addrs::add);
logger.finer(() -> "Adding node addresses " + addrs
+ " to VM info for " + vmDef.name());
extra.nodeInfo(nodeName, addrs);
}
} catch (ApiException e) {
logger.log(Level.WARNING, e,
() -> "Cannot access node information: " + e.getMessage());
}
}
/**
* Returns the VM data.
*
* @param event the event
*/
@Handler
public void onGetVms(GetVms event) {
event.setResult(channelManager.channels().stream()
.filter(c -> event.name().isEmpty()
|| c.vmDefinition().name().equals(event.name().get()))
.filter(c -> event.user().isEmpty() && event.roles().isEmpty()
|| !c.vmDefinition().permissionsFor(event.user().orElse(null),
event.roles()).isEmpty())
.filter(c -> event.fromPool().isEmpty()
|| c.vmDefinition().assignment().map(Assignment::pool)
.map(p -> p.equals(event.fromPool().get())).orElse(false))
.filter(c -> event.toUser().isEmpty()
|| c.vmDefinition().assignment().map(Assignment::user)
.map(u -> u.equals(event.toUser().get())).orElse(false))
.map(c -> new VmData(c.vmDefinition(), c))
.toList());
}
/**
* Assign a VM if not already assigned.
* On modify vm.
*
* @param event the event
* @throws ApiException the api exception
* @throws InterruptedException
* @throws IOException Signals that an I/O exception has occurred.
*/
@Handler
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
public void onAssignVm(AssignVm event)
throws ApiException, InterruptedException {
while (true) {
// Search for existing assignment.
var vmQuery = channelManager.channels().stream()
.filter(c -> c.vmDefinition().assignment().map(Assignment::pool)
.map(p -> p.equals(event.fromPool())).orElse(false))
.filter(c -> c.vmDefinition().assignment().map(Assignment::user)
.map(u -> u.equals(event.toUser())).orElse(false))
.findFirst();
if (vmQuery.isPresent()) {
var vmDef = vmQuery.get().vmDefinition();
event.setResult(new VmData(vmDef, vmQuery.get()));
return;
public void onModifyVm(ModifyVm event, VmChannel channel)
throws ApiException, IOException {
patchVmDef(channel.client(), event.name(), "spec/vm/" + event.path(),
event.value());
}
// Get the pool definition for checking possible assignment
VmPool vmPool = newEventPipeline().fire(new GetPools()
.withName(event.fromPool())).get().stream().findFirst()
.orElse(null);
if (vmPool == null) {
return;
}
private void patchVmDef(K8sClient client, String name, String path,
Object value) throws ApiException, IOException {
var vmStub = K8sDynamicStub.get(client,
new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM), namespace(),
name);
// Find available VM.
vmQuery = channelManager.channels().stream()
.filter(c -> vmPool.isAssignable(c.vmDefinition()))
.sorted(Comparator.comparing((VmChannel c) -> c.vmDefinition()
.assignment().map(Assignment::lastUsed)
.orElse(Instant.ofEpochSecond(0)))
.thenComparing(preferRunning))
.findFirst();
// None found
if (vmQuery.isEmpty()) {
return;
}
// Assign to user
var chosenVm = vmQuery.get();
var vmPipeline = chosenVm.pipeline();
if (Optional.ofNullable(vmPipeline.fire(new UpdateAssignment(
vmPool, event.toUser()), chosenVm).get())
.orElse(false)) {
var vmDef = chosenVm.vmDefinition();
event.setResult(new VmData(vmDef, chosenVm));
// Make sure that a newly assigned VM is running.
chosenVm.pipeline().fire(new ModifyVm(vmDef.name(),
"state", "Running", chosenVm));
return;
}
// Patch running
String valueAsText = value instanceof String
? "\"" + value + "\""
: value.toString();
var res = vmStub.patch(V1Patch.PATCH_FORMAT_JSON_PATCH,
new V1Patch("[{\"op\": \"replace\", \"path\": \"/"
+ path + "\", \"value\": " + valueAsText + "}]"),
client.defaultPatchOptions());
if (!res.isPresent()) {
logger.warning(
() -> "Cannot patch definition for Vm " + vmStub.name());
}
}
private static Comparator<VmChannel> preferRunning
= new Comparator<>() {
@Override
public int compare(VmChannel ch1, VmChannel ch2) {
if (ch1.vmDefinition().conditionStatus("Running").orElse(false)
&& !ch2.vmDefinition().conditionStatus("Running")
.orElse(false)) {
return -1;
/**
* Attempt to Update the assignment information in the status of the
* VM CR. Returns true if successful. The handler does not attempt
* retries, because in case of failure it will be necessary to
* re-evaluate the chosen VM.
*
* @param event the event
* @param channel the channel
* @throws ApiException the api exception
*/
@Handler
public void onUpdatedAssignment(UpdateAssignment event, VmChannel channel)
throws ApiException {
try {
var vmDef = channel.vmDefinition();
var vmStub = VmDefinitionStub.get(channel.client(),
new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM),
vmDef.namespace(), vmDef.name());
if (vmStub.updateStatus(vmDef, from -> {
JsonObject status = from.statusJson();
if (event.toUser() == null) {
((JsonObject) GsonPtr.to(status).get())
.remove(Status.ASSIGNMENT);
} else {
var assignment = GsonPtr.to(status).to(Status.ASSIGNMENT);
assignment.set("pool", event.fromPool().name());
assignment.set("user", event.toUser());
assignment.set("lastUsed", Instant.now().toString());
}
return 0;
return status;
}).isPresent()) {
event.setResult(true);
}
} catch (ApiException e) {
// Log exceptions except for conflict, which can be expected
if (HttpURLConnection.HTTP_CONFLICT != e.getCode()) {
throw e;
}
}
event.setResult(false);
}
};
}

View file

@ -1,6 +1,6 @@
/*
* VM-Operator
* Copyright (C) 2023 Michael N. Lipp
* Copyright (C) 2023,2025 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
@ -83,8 +83,18 @@
* [YamlConfigurationStore] *-right[hidden]- [Controller]
*
* [Manager] *-- [Controller]
* [Controller] *-- [VmWatcher]
* [Controller] *-- [Reconciler]
* Component VmMonitor as VmMonitor <<internal>>
* [Controller] *-- [VmMonitor]
* [VmMonitor] -right[hidden]- [PoolMonitor]
* Component PoolMonitor as PoolMonitor <<internal>>
* [Controller] *-- [PoolMonitor]
* Component PodMonitor as PodMonitor <<internal>>
* [Controller] *-- [PodMonitor]
* [PodMonitor] -up[hidden]- VmMonitor
* Component DisplaySecretMonitor as DisplaySecretMonitor <<internal>>
* [Controller] *-- [DisplaySecretMonitor]
* [DisplaySecretMonitor] -up[hidden]- VmMonitor
* [Controller] *-left- [Reconciler]
* [Controller] -right[hidden]- [GuiHttpServer]
*
* [Manager] *-down- [GuiSocketServer:8080]

View file

@ -1,8 +1,8 @@
apiVersion: "vmoperator.jdrupes.org/v1"
kind: VirtualMachine
metadata:
namespace: vmop-dev
name: unittest-vm
namespace: vmop-test
name: test-vm
spec:
image:
repository: docker-registry.lan.mnl.de

View file

@ -0,0 +1,111 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../deploy
namespace: vmop-test
patches:
- patch: |-
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: vmop-image-repository
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
storageClassName: local-path
- patch: |-
kind: ConfigMap
apiVersion: v1
metadata:
name: vm-operator
data:
# Keep in sync with config.yaml
config.yaml: |
"/Manager":
# clusterName: "test"
"/Controller":
"/Reconciler":
runnerData:
storageClassName: null
loadBalancerService:
labels:
label1: label1
label2: toBeReplaced
annotations:
metallb.universe.tf/loadBalancerIPs: 192.168.168.1
metallb.universe.tf/ip-allocated-from-pool: single-common
metallb.universe.tf/allow-shared-ip: single-common
"/GuiSocketServer":
port: 8888
"/GuiHttpServer":
# This configures the GUI
"/ConsoleWeblet":
"/WebConsole":
"/LoginConlet":
users:
- name: admin
fullName: Administrator
password: "$2b$05$NiBd74ZGdplLC63ePZf1f.UtjMKkbQ23cQoO2OKOFalDBHWAOy21."
- name: test1
fullName: Test Account
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
- name: test2
fullName: Test Account
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
- name: test3
fullName: Test Account
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
"/RoleConfigurator":
rolesByUser:
# User admin has role admin
admin:
- admin
test1:
- user
test2:
- user
test3:
- user
# All users have role other
"*":
- other
replace: false
"/RoleConletFilter":
conletTypesByRole:
# Admins can use all conlets
admin:
- "*"
user:
- org.jdrupes.vmoperator.vmviewer.VmViewer
# Others cannot use any conlet (except login conlet to log out)
other:
- org.jgrapes.webconlet.locallogin.LoginConlet
"/ComponentCollector":
"/VmAccess":
displayResource:
preferredIpVersion: ipv4
syncPreviewsFor:
- role: user
- target:
group: apps
version: v1
kind: Deployment
name: vm-operator
patch: |-
- op: replace
path: /spec/template/spec/containers/0/image
value: docker-registry.lan.mnl.de/vmoperator/org.jdrupes.vmoperator.manager:test
- op: replace
path: /spec/template/spec/containers/0/imagePullPolicy
value: Always
- op: replace
path: /spec/replicas
value: 0

View file

@ -41,7 +41,7 @@ class BasicTests {
private static APIResource vmsContext;
private static K8sV1DeploymentStub mgrDeployment;
private static K8sDynamicStub vmStub;
private static final String VM_NAME = "unittest-vm";
private static final String VM_NAME = "test-vm";
private static final Object EXISTS = new Object();
@BeforeAll
@ -54,7 +54,7 @@ class BasicTests {
// Update manager pod by scaling deployment
mgrDeployment
= K8sV1DeploymentStub.get(client, "vmop-dev", "vm-operator");
= K8sV1DeploymentStub.get(client, "vmop-test", "vm-operator");
mgrDeployment.scale(0);
mgrDeployment.scale(1);
waitForManager();
@ -65,13 +65,13 @@ class BasicTests {
vmsContext = apiRes.get();
// Cleanup existing VM
K8sDynamicStub.get(client, vmsContext, "vmop-dev", VM_NAME)
K8sDynamicStub.get(client, vmsContext, "vmop-test", VM_NAME)
.delete();
ListOptions listOpts = new ListOptions();
listOpts.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
+ "app.kubernetes.io/instance=" + VM_NAME + ","
+ "app.kubernetes.io/component=" + DisplaySecret.NAME);
var secrets = K8sV1SecretStub.list(client, "vmop-dev", listOpts);
var secrets = K8sV1SecretStub.list(client, "vmop-test", listOpts);
for (var secret : secrets) {
secret.delete();
}
@ -103,7 +103,7 @@ class BasicTests {
"app.kubernetes.io/managed-by=" + VM_OP_NAME + ","
+ "app.kubernetes.io/name=" + APP_NAME + ","
+ "app.kubernetes.io/instance=" + VM_NAME);
var knownPvcs = K8sV1PvcStub.list(client, "vmop-dev", listOpts);
var knownPvcs = K8sV1PvcStub.list(client, "vmop-test", listOpts);
for (var pvc : knownPvcs) {
pvc.delete();
}
@ -112,7 +112,7 @@ class BasicTests {
@AfterAll
static void tearDownAfterClass() throws Exception {
// Cleanup
K8sDynamicStub.get(client, vmsContext, "vmop-dev", VM_NAME)
K8sDynamicStub.get(client, vmsContext, "vmop-test", VM_NAME)
.delete();
deletePvcs();
@ -124,7 +124,7 @@ class BasicTests {
void testConfigMap()
throws IOException, InterruptedException, ApiException {
K8sV1ConfigMapStub stub
= K8sV1ConfigMapStub.get(client, "vmop-dev", VM_NAME);
= K8sV1ConfigMapStub.get(client, "vmop-test", VM_NAME);
for (int i = 0; i < 10; i++) {
if (stub.model().isPresent()) {
break;
@ -134,7 +134,7 @@ class BasicTests {
// Check config map
var config = stub.model().get();
Map<List<? extends Object>, Object> toCheck = Map.of(
List.of("namespace"), "vmop-dev",
List.of("namespace"), "vmop-test",
List.of("name"), VM_NAME,
List.of("labels", "app.kubernetes.io/name"), Constants.APP_NAME,
List.of("labels", "app.kubernetes.io/instance"), VM_NAME,
@ -191,7 +191,7 @@ class BasicTests {
+ "app.kubernetes.io/component=" + DisplaySecret.NAME);
Collection<K8sV1SecretStub> secrets = null;
for (int i = 0; i < 10; i++) {
secrets = K8sV1SecretStub.list(client, "vmop-dev", listOpts);
secrets = K8sV1SecretStub.list(client, "vmop-test", listOpts);
if (secrets.size() > 0) {
break;
}
@ -207,7 +207,7 @@ class BasicTests {
@Test
void testRunnerPvc() throws ApiException, InterruptedException {
var stub
= K8sV1PvcStub.get(client, "vmop-dev", VM_NAME + "-runner-data");
= K8sV1PvcStub.get(client, "vmop-test", VM_NAME + "-runner-data");
for (int i = 0; i < 10; i++) {
if (stub.model().isPresent()) {
break;
@ -227,7 +227,7 @@ class BasicTests {
@Test
void testSystemDiskPvc() throws ApiException, InterruptedException {
var stub
= K8sV1PvcStub.get(client, "vmop-dev", VM_NAME + "-system-disk");
= K8sV1PvcStub.get(client, "vmop-test", VM_NAME + "-system-disk");
for (int i = 0; i < 10; i++) {
if (stub.model().isPresent()) {
break;
@ -248,7 +248,7 @@ class BasicTests {
@Test
void testDisk1Pvc() throws ApiException, InterruptedException {
var stub
= K8sV1PvcStub.get(client, "vmop-dev", VM_NAME + "-disk-1");
= K8sV1PvcStub.get(client, "vmop-test", VM_NAME + "-disk-1");
for (int i = 0; i < 10; i++) {
if (stub.model().isPresent()) {
break;
@ -274,7 +274,7 @@ class BasicTests {
new V1Patch("[{\"op\": \"replace\", \"path\": \"/spec/vm/state"
+ "\", \"value\": \"Running\"}]"),
client.defaultPatchOptions()).isPresent());
var stub = K8sV1PodStub.get(client, "vmop-dev", VM_NAME);
var stub = K8sV1PodStub.get(client, "vmop-test", VM_NAME);
for (int i = 0; i < 20; i++) {
if (stub.model().isPresent()) {
break;
@ -303,7 +303,7 @@ class BasicTests {
@Test
public void testLoadBalancer() throws ApiException, InterruptedException {
var stub = K8sV1ServiceStub.get(client, "vmop-dev", VM_NAME);
var stub = K8sV1ServiceStub.get(client, "vmop-test", VM_NAME);
for (int i = 0; i < 10; i++) {
if (stub.model().isPresent()) {
break;

View file

@ -36,7 +36,6 @@ import org.jgrapes.core.annotation.Handler;
/**
* The Class CdMediaController.
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class CdMediaController extends Component {
/**
@ -55,7 +54,6 @@ public class CdMediaController extends Component {
*
* @param componentChannel the component channel
*/
@SuppressWarnings("PMD.AssignmentToNonFinalStatic")
public CdMediaController(Channel componentChannel) {
super(componentChannel);
}
@ -66,8 +64,7 @@ public class CdMediaController extends Component {
* @param event the event
*/
@Handler
@SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition",
"PMD.AvoidInstantiatingObjectsInLoops" })
@SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops" })
public void onConfigureQemu(ConfigureQemu event) {
if (event.runState() == RunState.TERMINATING) {
return;

View file

@ -39,11 +39,9 @@ import org.jdrupes.vmoperator.util.FsdUtils;
/**
* The configuration information from the configuration file.
*/
@SuppressWarnings({ "PMD.ExcessivePublicCount", "PMD.TooManyFields" })
public class Configuration implements Dto {
private static final String CI_INSTANCE_ID = "instance-id";
@SuppressWarnings("PMD.FieldNamingConventions")
protected final Logger logger = Logger.getLogger(getClass().getName());
/** Configuration timestamp. */
@ -95,15 +93,12 @@ public class Configuration implements Dto {
public static class CloudInit implements Dto {
/** The meta data. */
@SuppressWarnings("PMD.UseConcurrentHashMap")
public Map<String, Object> metaData;
/** The user data. */
@SuppressWarnings("PMD.UseConcurrentHashMap")
public Map<String, Object> userData;
/** The network config. */
@SuppressWarnings("PMD.UseConcurrentHashMap")
public Map<String, Object> networkConfig;
}
@ -299,7 +294,6 @@ public class Configuration implements Dto {
return true;
}
@SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
private void checkDrives() {
for (Drive drive : vm.drives) {
if (drive.file != null || drive.device != null
@ -319,7 +313,6 @@ public class Configuration implements Dto {
}
}
@SuppressWarnings("PMD.AvoidDeeplyNestedIfStmts")
private boolean checkRuntimeDir() {
// Runtime directory (sockets etc.)
if (runtimeDir == null) {
@ -355,7 +348,6 @@ public class Configuration implements Dto {
return true;
}
@SuppressWarnings("PMD.AvoidDeeplyNestedIfStmts")
private boolean checkDataDir() {
// Data directory
if (dataDir == null) {

View file

@ -41,7 +41,6 @@ import org.jgrapes.core.events.Start;
* A (sub)component that updates the console status in the CR status.
* Created as child of {@link StatusUpdater}.
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class ConsoleTracker extends VmDefUpdater {
private VmDefinitionStub vmStub;
@ -53,7 +52,6 @@ public class ConsoleTracker extends VmDefUpdater {
*
* @param componentChannel the component channel
*/
@SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
public ConsoleTracker(Channel componentChannel) {
super(componentChannel);
apiClient = (K8sClient) io.kubernetes.client.openapi.Configuration
@ -91,8 +89,7 @@ public class ConsoleTracker extends VmDefUpdater {
* @throws ApiException the api exception
*/
@Handler
@SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition",
"PMD.AvoidDuplicateLiterals" })
@SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition" })
public void onSpiceInitialized(SpiceInitializedEvent event)
throws ApiException {
if (vmStub == null) {
@ -127,7 +124,6 @@ public class ConsoleTracker extends VmDefUpdater {
* @throws ApiException the api exception
*/
@Handler
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
public void onSpiceDisconnected(SpiceDisconnectedEvent event)
throws ApiException {
if (vmStub == null) {

View file

@ -21,7 +21,6 @@ package org.jdrupes.vmoperator.runner.qemu;
/**
* Some constants.
*/
@SuppressWarnings("PMD.DataClass")
public class Constants extends org.jdrupes.vmoperator.common.Constants {
/**

View file

@ -41,7 +41,6 @@ import org.jgrapes.core.annotation.Handler;
/**
* The Class CpuController.
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class CpuController extends Component {
private Integer currentCpus;

View file

@ -43,7 +43,6 @@ import org.jgrapes.util.events.WatchFile;
/**
* The Class DisplayController.
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class DisplayController extends Component {
private String currentPassword;
@ -59,8 +58,7 @@ public class DisplayController extends Component {
* @param componentChannel the component channel
* @param configDir
*/
@SuppressWarnings({ "PMD.AssignmentToNonFinalStatic",
"PMD.ConstructorCallsOverridableMethod" })
@SuppressWarnings({ "PMD.ConstructorCallsOverridableMethod" })
public DisplayController(Channel componentChannel, Path configDir) {
super(componentChannel);
this.configDir = configDir;
@ -114,7 +112,6 @@ public class DisplayController extends Component {
* @param event the event
*/
@Handler
@SuppressWarnings("PMD.EmptyCatchBlock")
public void onFileChanged(FileChanged event) {
if (event.path().equals(configDir.resolve(DisplaySecret.PASSWORD))) {
logger.fine(() -> "Display password updated");

View file

@ -69,14 +69,14 @@ public class GuestAgentClient extends AgentConnector {
*/
@Override
protected void agentConnected() {
logger.fine(() -> "guest agent connected");
logger.fine(() -> "Guest agent connected");
connected = true;
rep().fire(new GuestAgentCommand(new QmpGuestGetOsinfo()));
}
@Override
protected void agentDisconnected() {
logger.fine(() -> "guest agent disconnected");
logger.fine(() -> "Guest agent disconnected");
connected = false;
}
@ -88,15 +88,16 @@ public class GuestAgentClient extends AgentConnector {
*/
@Override
protected void processInput(String line) throws IOException {
logger.fine(() -> "guest agent(in): " + line);
logger.finer(() -> "guest agent(in): " + line);
try {
var response = mapper.readValue(line, ObjectNode.class);
if (response.has("return") || response.has("error")) {
QmpCommand executed = executing.poll();
logger.fine(() -> String.format("(Previous \"guest agent(in)\""
logger.finer(() -> String.format("(Previous \"guest agent(in)\""
+ " is result from executing %s)", executed));
if (executed instanceof QmpGuestGetOsinfo) {
var osInfo = new OsinfoEvent(response.get("return"));
logger.fine(() -> "Guest agent triggers: " + osInfo);
rep().fire(osInfo);
}
}
@ -120,10 +121,11 @@ public class GuestAgentClient extends AgentConnector {
return;
}
var command = event.command();
logger.fine(() -> "guest agent(out): " + command.toString());
logger.fine(() -> "Guest handles: " + event);
String asText;
try {
asText = command.asText();
logger.finer(() -> "guest agent(out): " + asText);
} catch (JsonProcessingException e) {
logger.log(Level.SEVERE, e,
() -> "Cannot serialize Json: " + e.getMessage());
@ -163,8 +165,8 @@ public class GuestAgentClient extends AgentConnector {
}
event.suspendHandling();
suspendedStop = event;
logger.fine(() -> "Sending powerdown command, waiting for"
+ " termination until " + waitUntil);
logger.fine(() -> "Attempting shutdown through guest agent,"
+ " waiting for termination until " + waitUntil);
powerdownTimer = Components.schedule(t -> {
logger.fine(() -> "Powerdown timeout reached.");
synchronized (this) {

View file

@ -54,7 +54,6 @@ import org.jgrapes.util.events.ConfigurationUpdate;
* If the log level for this class is set to fine, the messages
* exchanged on the monitor socket are logged.
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class QemuMonitor extends QemuConnector {
private int powerdownTimeout;
@ -72,8 +71,6 @@ public class QemuMonitor extends QemuConnector {
* @param configDir the config dir
* @throws IOException Signals that an I/O exception has occurred.
*/
@SuppressWarnings({ "PMD.AssignmentToNonFinalStatic",
"PMD.ConstructorCallsOverridableMethod" })
public QemuMonitor(Channel componentChannel, Path configDir)
throws IOException {
super(componentChannel);
@ -108,24 +105,30 @@ public class QemuMonitor extends QemuConnector {
@Override
protected void processInput(String line)
throws IOException {
logger.fine(() -> "monitor(in): " + line);
logger.finer(() -> "monitor(in): " + line);
try {
var response = mapper.readValue(line, ObjectNode.class);
if (response.has("QMP")) {
monitorReady = true;
logger.fine(() -> "QMP connection ready");
rep().fire(new MonitorReady());
return;
}
if (response.has("return") || response.has("error")) {
QmpCommand executed = executing.poll();
logger.fine(
logger.finer(
() -> String.format("(Previous \"monitor(in)\" is result "
+ "from executing %s)", executed));
rep().fire(MonitorResult.from(executed, response));
var monRes = MonitorResult.from(executed, response);
logger.fine(() -> "QMP triggers: " + monRes);
rep().fire(monRes);
return;
}
if (response.has("event")) {
MonitorEvent.from(response).ifPresent(rep()::fire);
MonitorEvent.from(response).ifPresent(me -> {
logger.fine(() -> "QMP triggers: " + me);
rep().fire(me);
});
}
} catch (JsonProcessingException e) {
throw new IOException(e);
@ -141,7 +144,7 @@ public class QemuMonitor extends QemuConnector {
public void onClosed(Closed<?> event, SocketIOChannel channel) {
channel.associated(this, getClass()).ifPresent(qm -> {
super.onClosed(event, channel);
logger.finer(() -> "QMP socket closed.");
logger.fine(() -> "QMP connection closed.");
monitorReady = false;
});
}
@ -158,7 +161,7 @@ public class QemuMonitor extends QemuConnector {
public void onMonitorCommand(MonitorCommand event) throws IOException {
// Check prerequisites
if (!monitorReady && !(event.command() instanceof QmpCapabilities)) {
logger.severe(() -> "Premature monitor command (not ready): "
logger.severe(() -> "Premature QMP command (not ready): "
+ event.command());
rep().fire(new Stop());
return;
@ -166,10 +169,11 @@ public class QemuMonitor extends QemuConnector {
// Send the command
var command = event.command();
logger.fine(() -> "monitor(out): " + command.toString());
logger.fine(() -> "QMP handles: " + event.toString());
String asText;
try {
asText = command.asText();
logger.finer(() -> "monitor(out): " + asText);
} catch (JsonProcessingException e) {
logger.log(Level.SEVERE, e,
() -> "Cannot serialize Json: " + e.getMessage());
@ -192,8 +196,8 @@ public class QemuMonitor extends QemuConnector {
@SuppressWarnings("PMD.AvoidSynchronizedStatement")
public void onStop(Stop event) {
if (!monitorReady) {
logger.fine(() -> "No QMP connection,"
+ " cannot send powerdown command");
logger.fine(() -> "Not sending QMP powerdown command"
+ " because QMP connection is closed");
return;
}

View file

@ -39,7 +39,6 @@ public class RamController extends Component {
*
* @param componentChannel the component channel
*/
@SuppressWarnings("PMD.AssignmentToNonFinalStatic")
public RamController(Channel componentChannel) {
super(componentChannel);
}

View file

@ -157,6 +157,15 @@ import org.jgrapes.util.events.WatchFile;
*
* success --> Running
*
* state Running {
* state Booting
* state Booted
*
* [*] -right-> Booting
* Booting -down-> Booting: VserportChanged[guest agent connected]/fire GetOsinfo
* Booting --> Booted: Osinfo
* }
*
* state Terminating {
* state terminate <<entryPoint>>
* state qemuRunning <<choice>>
@ -192,8 +201,7 @@ import org.jgrapes.util.events.WatchFile;
*
*/
@SuppressWarnings({ "PMD.ExcessiveImports", "PMD.AvoidPrintStackTrace",
"PMD.DataflowAnomalyAnalysis", "PMD.TooManyMethods",
"PMD.CouplingBetweenObjects", "PMD.TooManyFields" })
"PMD.TooManyMethods", "PMD.CouplingBetweenObjects" })
public class Runner extends Component {
private static final String TEMPLATE_DIR
@ -209,7 +217,6 @@ public class Runner extends Component {
.builder().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER)
.build());
private final JsonNode defaults;
@SuppressWarnings("PMD.UseConcurrentHashMap")
private final File configFile;
private final Path configDir;
private Configuration initialConfig;
@ -241,8 +248,7 @@ public class Runner extends Component {
* @param cmdLine the cmd line
* @throws IOException Signals that an I/O exception has occurred.
*/
@SuppressWarnings({ "PMD.SystemPrintln",
"PMD.ConstructorCallsOverridableMethod" })
@SuppressWarnings({ "PMD.ConstructorCallsOverridableMethod" })
public Runner(CommandLine cmdLine) throws IOException {
yamlMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
false);
@ -378,8 +384,6 @@ public class Runner extends Component {
}
}
@SuppressWarnings({ "PMD.CognitiveComplexity",
"PMD.DataflowAnomalyAnalysis" })
private void setFirmwarePaths(Configuration config) throws IOException {
JsonNode firmware = defaults.path("firmware").path(config.vm.firmware);
// Get file for firmware ROM
@ -611,8 +615,6 @@ public class Runner extends Component {
* @throws InterruptedException the interrupted exception
*/
@Handler
@SuppressWarnings({ "PMD.SwitchStmtsShouldHaveDefault",
"PMD.TooFewBranchesForASwitchStatement" })
public void onProcessStarted(ProcessStarted event, ProcessChannel channel)
throws InterruptedException {
event.startEvent().associated(CommandDefinition.class)
@ -769,7 +771,6 @@ public class Runner extends Component {
"The VM has been shut down"));
}
@SuppressWarnings("PMD.ConfusingArgumentToVarargsMethod")
private void shutdown() {
if (!Set.of(RunState.TERMINATING, RunState.STOPPED).contains(state)) {
fire(new Stop());

View file

@ -31,6 +31,8 @@ import io.kubernetes.client.openapi.JSON;
import io.kubernetes.client.openapi.models.EventsV1Event;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.Instant;
import java.util.Optional;
import java.util.logging.Level;
import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
@ -55,6 +57,8 @@ import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLoggedIn;
import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLoggedOut;
import org.jdrupes.vmoperator.util.GsonPtr;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Components;
import org.jgrapes.core.Components.Timer;
import org.jgrapes.core.annotation.Handler;
import org.jgrapes.core.events.HandlingError;
import org.jgrapes.core.events.Start;
@ -62,7 +66,7 @@ import org.jgrapes.core.events.Start;
/**
* Updates the CR status.
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
@SuppressWarnings({ "PMD.CouplingBetweenObjects" })
public class StatusUpdater extends VmDefUpdater {
@SuppressWarnings("PMD.FieldNamingConventions")
@ -71,18 +75,20 @@ public class StatusUpdater extends VmDefUpdater {
private static final ObjectMapper objectMapper
= new ObjectMapper().registerModule(new JavaTimeModule());
private long observedGeneration;
private boolean guestShutdownStops;
private boolean shutdownByGuest;
private VmDefinitionStub vmStub;
private String loggedInUser;
private BigInteger lastRamValue;
private Instant lastRamChange;
private Timer balloonTimer;
private BigInteger targetRamValue;
/**
* Instantiates a new status updater.
*
* @param componentChannel the component channel
*/
@SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
public StatusUpdater(Channel componentChannel) {
super(componentChannel);
attach(new ConsoleTracker(componentChannel));
@ -122,7 +128,6 @@ public class StatusUpdater extends VmDefUpdater {
if (vmDef == null) {
return;
}
observedGeneration = vmDef.getMetadata().getGeneration();
vmStub.updateStatus(from -> {
JsonObject status = from.statusJson();
status.addProperty(Status.RUNNER_VERSION, Optional.ofNullable(
@ -146,31 +151,16 @@ public class StatusUpdater extends VmDefUpdater {
* @throws ApiException
*/
@Handler
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
public void onConfigureQemu(ConfigureQemu event)
throws ApiException {
guestShutdownStops = event.configuration().guestShutdownStops;
loggedInUser = event.configuration().vm.display.loggedInUser;
targetRamValue = event.configuration().vm.currentRam;
// Remainder applies only if we have a connection to k8s.
if (vmStub == null) {
return;
}
// A change of the runner configuration is typically caused
// by a new version of the CR. So we update only if we have
// a new version of the CR. There's one exception: the display
// password is configured by a file, not by the CR.
var vmDef = vmStub.model().orElse(null);
if (vmDef == null) {
return;
}
if (vmDef.metadata().getGeneration() == observedGeneration
&& (event.configuration().hasDisplayPassword
|| vmDef.statusJson().getAsJsonPrimitive(
Status.DISPLAY_PASSWORD_SERIAL).getAsInt() == -1)) {
return;
}
vmStub.updateStatus(from -> {
JsonObject status = from.statusJson();
if (!event.configuration().hasDisplayPassword) {
@ -184,7 +174,7 @@ public class StatusUpdater extends VmDefUpdater {
from.getMetadata().getGeneration()));
updateUserLoggedIn(from);
return status;
}, vmDef);
});
}
/**
@ -194,8 +184,7 @@ public class StatusUpdater extends VmDefUpdater {
* @throws ApiException
*/
@Handler
@SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition",
"PMD.AssignmentInOperand", "PMD.AvoidDuplicateLiterals" })
@SuppressWarnings({ "PMD.AssignmentInOperand" })
public void onRunnerStateChanged(RunnerStateChange event)
throws ApiException {
VmDefinition vmDef;
@ -279,7 +268,11 @@ public class StatusUpdater extends VmDefUpdater {
}
/**
* On ballon change.
* Update the current RAM size in the status. Balloon changes happen
* more than once every second during changes. While this is nice
* to watch, this puts a heavy load on the system. Therefore we
* only update the status once every 15 seconds or when the target
* value is reached.
*
* @param event the event
* @throws ApiException
@ -289,10 +282,45 @@ public class StatusUpdater extends VmDefUpdater {
if (vmStub == null) {
return;
}
Instant now = Instant.now();
if (lastRamChange == null
|| lastRamChange.isBefore(now.minusSeconds(15))
|| event.size().equals(targetRamValue)) {
if (balloonTimer != null) {
balloonTimer.cancel();
balloonTimer = null;
}
lastRamChange = now;
lastRamValue = event.size();
updateRam();
return;
}
// Save for later processing and maybe start timer
lastRamChange = now;
lastRamValue = event.size();
if (balloonTimer != null) {
return;
}
final var pipeline = activeEventPipeline();
balloonTimer = Components.schedule(t -> {
pipeline.submit("Update RAM size", () -> {
try {
updateRam();
} catch (ApiException e) {
logger.log(Level.WARNING, e,
() -> "Failed to update ram size: " + e.getMessage());
}
balloonTimer = null;
});
}, now.plusSeconds(15));
}
private void updateRam() throws ApiException {
vmStub.updateStatus(from -> {
JsonObject status = from.statusJson();
status.addProperty(Status.RAM,
new Quantity(new BigDecimal(event.size()), Format.BINARY_SI)
new Quantity(new BigDecimal(lastRamValue), Format.BINARY_SI)
.toSuffixedString());
return status;
});
@ -393,7 +421,6 @@ public class StatusUpdater extends VmDefUpdater {
* @throws ApiException
*/
@Handler
@SuppressWarnings("PMD.AssignmentInOperand")
public void onVmopAgentLoggedIn(VmopAgentLoggedIn event)
throws ApiException {
vmStub.updateStatus(from -> {
@ -410,7 +437,6 @@ public class StatusUpdater extends VmDefUpdater {
* @throws ApiException
*/
@Handler
@SuppressWarnings("PMD.AssignmentInOperand")
public void onVmopAgentLoggedOut(VmopAgentLoggedOut event)
throws ApiException {
vmStub.updateStatus(from -> {

View file

@ -43,7 +43,6 @@ import org.jgrapes.util.events.InitialConfiguration;
/**
* Updates the CR status.
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class VmDefUpdater extends Component {
protected String namespace;
@ -125,16 +124,19 @@ public class VmDefUpdater extends Component {
protected JsonObject updateCondition(VmDefinition from, String type,
boolean state, String reason, String message) {
JsonObject status = from.statusJson();
// Optimize, as we can get this several times
// Avoid redundant updates, as this may be called several times
var current = status.getAsJsonArray("conditions").asList().stream()
.map(cond -> (JsonObject) cond)
.filter(cond -> type.equals(cond.get("type").getAsString()))
.findFirst();
if (current.isPresent()
&& current.map(c -> c.get("status").getAsString())
.map("True"::equals).map(s -> s == state).orElse(false)
var stateUnchanged = current.map(c -> c.get("status").getAsString())
.map("True"::equals).map(s -> s == state).orElse(false);
if (stateUnchanged
&& current.map(c -> c.get("reason").getAsString())
.map(reason::equals).orElse(false)) {
.map(reason::equals).orElse(false)
&& current.map(c -> c.get("observedGeneration").getAsLong())
.map(from.getMetadata().getGeneration()::equals)
.orElse(false)) {
return status;
}
@ -143,7 +145,9 @@ public class VmDefUpdater extends Component {
"status", state ? "True" : "False",
"observedGeneration", from.getMetadata().getGeneration(),
"reason", reason,
"lastTransitionTime", Instant.now().toString()));
"lastTransitionTime", stateUnchanged
? current.get().get("lastTransitionTime").getAsString()
: Instant.now().toString()));
if (message != null) {
condition.put("message", message);
}

View file

@ -59,10 +59,14 @@ public class VmopAgentClient extends AgentConnector {
*/
@Handler
public void onVmopAgentLogIn(VmopAgentLogIn event) throws IOException {
logger.fine(() -> "vmop agent(out): login " + event.user());
if (writer().isPresent()) {
logger.fine(() -> "Vmop agent handles:" + event);
executing.add(event);
logger.finer(() -> "vmop agent(out): login " + event.user());
sendCommand("login " + event.user());
} else {
logger
.warning(() -> "No vmop agent connection for sending " + event);
}
}
@ -74,34 +78,38 @@ public class VmopAgentClient extends AgentConnector {
*/
@Handler
public void onVmopAgentLogout(VmopAgentLogOut event) throws IOException {
logger.fine(() -> "vmop agent(out): logout");
if (writer().isPresent()) {
logger.fine(() -> "Vmop agent handles:" + event);
executing.add(event);
logger.finer(() -> "vmop agent(out): logout");
sendCommand("logout");
}
}
@Override
@SuppressWarnings({ "PMD.UnnecessaryReturn",
"PMD.AvoidLiteralsInIfCondition" })
@SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition" })
protected void processInput(String line) throws IOException {
logger.fine(() -> "vmop agent(in): " + line);
logger.finer(() -> "vmop agent(in): " + line);
// Check validity
if (line.isEmpty() || !Character.isDigit(line.charAt(0))) {
logger.warning(() -> "Illegal response: " + line);
logger.warning(() -> "Illegal vmop agent response: " + line);
return;
}
// Check positive responses
if (line.startsWith("220 ")) {
rep().fire(new VmopAgentConnected());
var evt = new VmopAgentConnected();
logger.fine(() -> "Vmop agent triggers " + evt);
rep().fire(evt);
return;
}
if (line.startsWith("201 ")) {
Event<?> cmd = executing.pop();
if (cmd instanceof VmopAgentLogIn login) {
rep().fire(new VmopAgentLoggedIn(login));
var evt = new VmopAgentLoggedIn(login);
logger.fine(() -> "Vmop agent triggers " + evt);
rep().fire(evt);
} else {
logger.severe(() -> "Response " + line
+ " does not match executing command " + cmd);
@ -111,7 +119,9 @@ public class VmopAgentClient extends AgentConnector {
if (line.startsWith("202 ")) {
Event<?> cmd = executing.pop();
if (cmd instanceof VmopAgentLogOut logout) {
rep().fire(new VmopAgentLoggedOut(logout));
var evt = new VmopAgentLoggedOut(logout);
logger.fine(() -> "Vmop agent triggers " + evt);
rep().fire(evt);
} else {
logger.severe(() -> "Response " + line
+ "does not match executing command " + cmd);
@ -125,7 +135,7 @@ public class VmopAgentClient extends AgentConnector {
}
// Error
logger.warning(() -> "Error response: " + line);
logger.warning(() -> "Error response from vmop agent: " + line);
executing.pop();
}

View file

@ -25,8 +25,7 @@ import com.fasterxml.jackson.databind.JsonNode;
*/
public class QmpCapabilities extends QmpCommand {
@SuppressWarnings({ "PMD.FieldNamingConventions",
"PMD.VariableNamingConventions" })
@SuppressWarnings({ "PMD.FieldNamingConventions" })
private static final JsonNode jsonTemplate
= parseJson("{ \"execute\": \"qmp_capabilities\" }");

View file

@ -27,8 +27,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
*/
public class QmpChangeMedium extends QmpCommand {
@SuppressWarnings({ "PMD.FieldNamingConventions",
"PMD.VariableNamingConventions" })
@SuppressWarnings({ "PMD.FieldNamingConventions" })
private static final JsonNode jsonTemplate
= parseJson("{ \"execute\": \"blockdev-change-medium\",\"arguments\": {"
+ "\"id\": \"\",\"filename\": \"\",\"format\": \"raw\","

View file

@ -30,8 +30,7 @@ import java.util.logging.Logger;
*/
public abstract class QmpCommand {
@SuppressWarnings({ "PMD.FieldNamingConventions",
"PMD.VariableNamingConventions" })
@SuppressWarnings({ "PMD.FieldNamingConventions" })
protected static final ObjectMapper mapper = new ObjectMapper();
/**

View file

@ -25,8 +25,7 @@ import com.fasterxml.jackson.databind.JsonNode;
*/
public class QmpCont extends QmpCommand {
@SuppressWarnings({ "PMD.FieldNamingConventions",
"PMD.VariableNamingConventions" })
@SuppressWarnings({ "PMD.FieldNamingConventions" })
private static final JsonNode jsonTemplate
= parseJson("{ \"execute\": \"cont\" }");

View file

@ -27,8 +27,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
*/
public class QmpDelCpu extends QmpCommand {
@SuppressWarnings({ "PMD.FieldNamingConventions",
"PMD.VariableNamingConventions" })
@SuppressWarnings({ "PMD.FieldNamingConventions" })
private static final JsonNode jsonTemplate
= parseJson("{ \"execute\": \"device_del\", "
+ "\"arguments\": " + "{ \"id\": 0 } }");

View file

@ -27,8 +27,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
*/
public class QmpOpenTray extends QmpCommand {
@SuppressWarnings({ "PMD.FieldNamingConventions",
"PMD.VariableNamingConventions" })
@SuppressWarnings({ "PMD.FieldNamingConventions" })
private static final JsonNode jsonTemplate
= parseJson("{ \"execute\": \"blockdev-open-tray\",\"arguments\": {"
+ "\"id\": \"\" } }");

View file

@ -25,8 +25,7 @@ import com.fasterxml.jackson.databind.JsonNode;
*/
public class QmpPowerdown extends QmpCommand {
@SuppressWarnings({ "PMD.FieldNamingConventions",
"PMD.VariableNamingConventions" })
@SuppressWarnings({ "PMD.FieldNamingConventions" })
private static final JsonNode jsonTemplate
= parseJson("{ \"execute\": \"system_powerdown\" }");

View file

@ -25,8 +25,7 @@ import com.fasterxml.jackson.databind.JsonNode;
*/
public class QmpQueryHotpluggableCpus extends QmpCommand {
@SuppressWarnings({ "PMD.FieldNamingConventions",
"PMD.VariableNamingConventions" })
@SuppressWarnings({ "PMD.FieldNamingConventions" })
private static final JsonNode jsonTemplate = parseJson(
"{\"execute\":\"query-hotpluggable-cpus\",\"arguments\":{}}");

View file

@ -27,8 +27,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
*/
public class QmpRemoveMedium extends QmpCommand {
@SuppressWarnings({ "PMD.FieldNamingConventions",
"PMD.VariableNamingConventions" })
@SuppressWarnings({ "PMD.FieldNamingConventions" })
private static final JsonNode jsonTemplate
= parseJson("{ \"execute\": \"blockdev-remove-medium\",\"arguments\": {"
+ "\"id\": \"\" } }");

View file

@ -25,8 +25,7 @@ import com.fasterxml.jackson.databind.JsonNode;
*/
public class QmpReset extends QmpCommand {
@SuppressWarnings({ "PMD.FieldNamingConventions",
"PMD.VariableNamingConventions" })
@SuppressWarnings({ "PMD.FieldNamingConventions" })
private static final JsonNode jsonTemplate
= parseJson("{ \"execute\": \"system_reset\" }");

View file

@ -28,8 +28,7 @@ import java.math.BigInteger;
*/
public class QmpSetBalloon extends QmpCommand {
@SuppressWarnings({ "PMD.FieldNamingConventions",
"PMD.VariableNamingConventions" })
@SuppressWarnings({ "PMD.FieldNamingConventions" })
private static final JsonNode jsonTemplate
= parseJson("{ \"execute\": \"balloon\", "
+ "\"arguments\": " + "{ \"value\": 0 } }");

View file

@ -20,6 +20,8 @@ package org.jdrupes.vmoperator.runner.qemu.events;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.Optional;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Components;
import org.jgrapes.core.Event;
/**
@ -47,7 +49,6 @@ public class MonitorEvent extends Event<Void> {
* @param response the response
* @return the optional
*/
@SuppressWarnings("PMD.TooFewBranchesForASwitchStatement")
public static Optional<MonitorEvent> from(JsonNode response) {
try {
var kind = Kind.valueOf(response.get("event").asText());
@ -112,4 +113,20 @@ public class MonitorEvent extends Event<Void> {
public JsonNode data() {
return data;
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append(Components.objectName(this)).append(" [").append(data);
if (channels() != null) {
builder.append(", channels=").append(Channel.toString(channels()));
}
builder.append(']');
return builder.toString();
}
}

View file

@ -19,6 +19,8 @@
package org.jdrupes.vmoperator.runner.qemu.events;
import com.fasterxml.jackson.databind.JsonNode;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Components;
import org.jgrapes.core.Event;
/**
@ -40,4 +42,21 @@ public class OsinfoEvent extends Event<Void> {
public JsonNode osinfo() {
return osinfo;
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append(Components.objectName(this)).append(" [")
.append(osinfo);
if (channels() != null) {
builder.append(", channels=").append(Channel.toString(channels()));
}
builder.append(']');
return builder.toString();
}
}

View file

@ -26,7 +26,6 @@ import org.jgrapes.core.Event;
/**
* The Class RunnerStateChange.
*/
@SuppressWarnings("PMD.DataClass")
public class RunnerStateChange extends Event<Void> {
/**

View file

@ -32,7 +32,6 @@ import java.util.logging.Logger;
*/
public final class DataPath {
@SuppressWarnings("PMD.FieldNamingConventions")
private static final Logger logger
= Logger.getLogger(DataPath.class.getName());
@ -56,7 +55,6 @@ public final class DataPath {
* @param selectors the selectors
* @return the result
*/
@SuppressWarnings("PMD.UseLocaleWithCaseConversions")
public static <T> Optional<T> get(Object from, Object... selectors) {
Object cur = from;
for (var selector : selectors) {
@ -132,7 +130,6 @@ public final class DataPath {
@SuppressWarnings({ "PMD.CognitiveComplexity", "unchecked" })
public static <T> T deepCopy(T object) {
if (object instanceof Map map) {
@SuppressWarnings("PMD.UseConcurrentHashMap")
Map<Object, Object> copy;
try {
copy = (Map<Object, Object>) object.getClass().getConstructor()

View file

@ -32,8 +32,7 @@ import java.util.function.Supplier;
/**
* Utility class for pointing to elements on a Gson (Json) tree.
*/
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis",
"PMD.ClassWithOnlyPrivateConstructorsShouldBeFinal", "PMD.GodClass" })
@SuppressWarnings({ "PMD.ClassWithOnlyPrivateConstructorsShouldBeFinal" })
public class GsonPtr {
private final JsonElement position;
@ -102,7 +101,7 @@ public class GsonPtr {
* @param selectors the selectors
* @return the Gson pointer
*/
@SuppressWarnings({ "PMD.ShortMethodName", "PMD.PreserveStackTrace" })
@SuppressWarnings({ "PMD.PreserveStackTrace" })
public Optional<GsonPtr> get(Object... selectors) {
JsonElement element = position;
for (Object sel : selectors) {
@ -146,7 +145,6 @@ public class GsonPtr {
* @param cls the cls
* @return the result
*/
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop" })
public <T extends JsonElement> T getAs(Class<T> cls) {
if (cls.isAssignableFrom(position.getClass())) {
return cls.cast(position);

View file

@ -57,8 +57,8 @@ import org.jdrupes.vmoperator.manager.events.GetVms.VmData;
import org.jdrupes.vmoperator.manager.events.ModifyVm;
import org.jdrupes.vmoperator.manager.events.ResetVm;
import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jdrupes.vmoperator.manager.events.VmPoolChanged;
import org.jdrupes.vmoperator.manager.events.VmResourceChanged;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Components;
import org.jgrapes.core.Event;
@ -111,9 +111,8 @@ import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet;
* users and roles.
*
*/
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports",
"PMD.CouplingBetweenObjects", "PMD.GodClass", "PMD.TooManyMethods",
"PMD.CyclomaticComplexity" })
@SuppressWarnings({ "PMD.ExcessiveImports", "PMD.CouplingBetweenObjects",
"PMD.GodClass", "PMD.TooManyMethods", "PMD.CyclomaticComplexity" })
public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
private static final String VM_NAME_PROPERTY = "vmName";
@ -129,6 +128,7 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
private EventPipeline appPipeline;
private static ObjectMapper objectMapper
= new ObjectMapper().registerModule(new JavaTimeModule());
private Class<?> preferredIpVersion = Inet4Address.class;
private Set<String> syncUsers = Collections.emptySet();
private Set<String> syncRoles = Collections.emptySet();
@ -166,7 +166,7 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
*
* @param event the event
*/
@SuppressWarnings({ "unchecked", "PMD.AvoidDuplicateLiterals" })
@SuppressWarnings({ "unchecked" })
@Handler
public void onConfigurationUpdate(ConfigurationUpdate event) {
event.structured(componentPath())
@ -266,7 +266,7 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
public void onConsoleConfigured(ConsoleConfigured event,
ConsoleConnection connection) throws InterruptedException,
IOException {
@SuppressWarnings({ "unchecked", "PMD.PrematureDeclaration" })
@SuppressWarnings({ "unchecked" })
final var rendered
= (Set<ResourceModel>) connection.session().get(RENDERED);
connection.session().remove(RENDERED);
@ -276,8 +276,7 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
addMissingConlets(event, connection, rendered);
}
@SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops",
"PMD.AvoidDuplicateLiterals" })
@SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops" })
private void addMissingConlets(ConsoleConfigured event,
ConsoleConnection connection, final Set<ResourceModel> rendered)
throws InterruptedException {
@ -405,7 +404,6 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
}
@Override
@SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops" })
protected Set<RenderMode> doRenderConlet(RenderConletRequestBase<?> event,
ConsoleConnection channel, String conletId, ResourceModel model)
throws Exception {
@ -654,10 +652,9 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
* @throws InterruptedException
*/
@Handler(namedChannels = "manager")
@SuppressWarnings({ "PMD.ConfusingTernary", "PMD.CognitiveComplexity",
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.AvoidDuplicateLiterals",
"PMD.ConfusingArgumentToVarargsMethod" })
public void onVmDefChanged(VmDefChanged event, VmChannel channel)
@SuppressWarnings({ "PMD.CognitiveComplexity",
"PMD.AvoidInstantiatingObjectsInLoops" })
public void onVmResourceChanged(VmResourceChanged event, VmChannel channel)
throws IOException, InterruptedException {
var vmDef = event.vmDefinition();
@ -785,12 +782,12 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
switch (event.method()) {
case "start":
if (perms.contains(VmDefinition.Permission.START)) {
fire(new ModifyVm(vmName, "state", "Running", vmChannel));
vmChannel.fire(new ModifyVm(vmName, "state", "Running"));
}
break;
case "stop":
if (perms.contains(VmDefinition.Permission.STOP)) {
fire(new ModifyVm(vmName, "state", "Stopped", vmChannel));
vmChannel.fire(new ModifyVm(vmName, "state", "Stopped"));
}
break;
case "reset":
@ -800,7 +797,7 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
break;
case "resetConfirmed":
if (perms.contains(VmDefinition.Permission.RESET)) {
fire(new ResetVm(vmName), vmChannel);
vmChannel.fire(new ResetVm(vmName));
}
break;
case "openConsole":
@ -838,7 +835,7 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
}
var pwQuery = Event.onCompletion(new GetDisplaySecret(vmDef, user),
e -> gotPassword(channel, model, vmDef, e));
fire(pwQuery, vmChannel);
vmChannel.fire(pwQuery);
}
private void gotPassword(ConsoleConnection channel, ResourceModel model,
@ -846,14 +843,13 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
if (!event.secretAvailable()) {
return;
}
vmDef.extra().map(xtra -> xtra.connectionFile(event.secret(),
preferredIpVersion, deleteConnectionFile))
vmDef.extra().connectionFile(event.secret(),
preferredIpVersion, deleteConnectionFile)
.ifPresent(cf -> channel.respond(new NotifyConletView(type(),
model.getConletId(), "openConsole", cf)));
}
@SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition",
"PMD.UseLocaleWithCaseConversions" })
@SuppressWarnings({ "PMD.UseLocaleWithCaseConversions" })
private void selectResource(NotifyConletModel event,
ConsoleConnection channel, ResourceModel model)
throws JsonProcessingException, InterruptedException {
@ -880,7 +876,6 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
/**
* The Class AccessModel.
*/
@SuppressWarnings("PMD.DataClass")
public static class ResourceModel extends ConletBaseModel {
/**

View file

@ -30,7 +30,7 @@
</tr>
</thead>
<tbody>
<template v-for="(entry, rowIndex) in filteredData">
<template v-for="(entry, rowIndex) in filteredData" :key="entry.name">
<tr :class="[(rowIndex % 2) ? 'odd' : 'even']"
:aria-expanded="(entry.name in detailsByName) ? 'true' : 'false'">
<td v-for="key in controller.keys"

Some files were not shown because too many files have changed in this diff Show more