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 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) ![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 ![Overview picture](webpages/index-pic.svg)
for running Qemu based VMs in Kubernetes pods.
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/) See the [project's home page](https://vm-operator.jdrupes.org/)
for details. for details.

View file

@ -21,22 +21,31 @@ spec:
- name: vm-operator - name: vm-operator
image: >- image: >-
ghcr.io/mnlipp/org.jdrupes.vmoperator.manager:latest 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: volumeMounts:
- name: config - name: config
mountPath: /etc/opt/vmoperator mountPath: /etc/opt/vmoperator
- name: vmop-image-repository - name: vmop-image-repository
mountPath: /var/local/vmop-image-repository mountPath: /var/local/vmop-image-repository
imagePullPolicy: Always
securityContext: securityContext:
capabilities: capabilities:
drop: drop:
- ALL - ALL
readOnlyRootFilesystem: true readOnlyRootFilesystem: true
allowPrivilegeEscalation: false allowPrivilegeEscalation: false
resources:
requests:
cpu: 100m
memory: 128Mi
volumes: volumes:
- name: config - name: config
configMap: configMap:

View file

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

View file

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

View file

@ -32,4 +32,5 @@
<noscript><p><img referrerpolicy="no-referrer-when-downgrade" <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> 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 --> <!-- End Matomo Code -->
<script defer src="https://gotit.mnl.de/script.js" data-website-id="14b277ad-d330-4a54-82f1-a77d111240ac"></script>
</div> </div>

View file

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

View file

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

View file

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

View file

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

View file

@ -45,8 +45,7 @@ import java.util.function.Function;
* @param <O> the generic type * @param <O> the generic type
* @param <L> the generic type * @param <L> the generic type
*/ */
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", @SuppressWarnings({ "PMD.CouplingBetweenObjects" })
"PMD.CouplingBetweenObjects" })
public class K8sClusterGenericStub<O extends KubernetesObject, public class K8sClusterGenericStub<O extends KubernetesObject,
L extends KubernetesListObject> { L extends KubernetesListObject> {
protected final K8sClient client; protected final K8sClient client;
@ -240,6 +239,7 @@ public class K8sClusterGenericStub<O extends KubernetesObject,
* @param <L> the object list type * @param <L> the object list type
* @param <R> the result type * @param <R> the result type
*/ */
@FunctionalInterface
public interface GenericSupplier<O extends KubernetesObject, public interface GenericSupplier<O extends KubernetesObject,
L extends KubernetesListObject, L extends KubernetesListObject,
R extends K8sClusterGenericStub<O, L>> { R extends K8sClusterGenericStub<O, L>> {
@ -254,7 +254,6 @@ public class K8sClusterGenericStub<O extends KubernetesObject,
* @param name the name * @param name the name
* @return the result * @return the result
*/ */
@SuppressWarnings("PMD.UseObjectForClearerAPI")
R get(Class<O> objectClass, Class<L> objectListClass, K8sClient client, R get(Class<O> objectClass, Class<L> objectListClass, K8sClient client,
APIResource context, String name); APIResource context, String name);
} }
@ -283,7 +282,6 @@ public class K8sClusterGenericStub<O extends KubernetesObject,
* @return the stub if the object exists * @return the stub if the object exists
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop" })
public static <O extends KubernetesObject, L extends KubernetesListObject, public static <O extends KubernetesObject, L extends KubernetesListObject,
R extends K8sClusterGenericStub<O, L>> R extends K8sClusterGenericStub<O, L>>
R get(Class<O> objectClass, Class<L> objectListClass, 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 * @return the stub if the object exists
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
"PMD.UseObjectForClearerAPI" })
public static <O extends KubernetesObject, L extends KubernetesListObject, public static <O extends KubernetesObject, L extends KubernetesListObject,
R extends K8sClusterGenericStub<O, L>> R extends K8sClusterGenericStub<O, L>>
R get(Class<O> objectClass, Class<L> objectListClass, 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 * @return the stub if the object exists
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
public static <O extends KubernetesObject, L extends KubernetesListObject, public static <O extends KubernetesObject, L extends KubernetesListObject,
R extends K8sClusterGenericStub<O, L>> R extends K8sClusterGenericStub<O, L>>
R create(Class<O> objectClass, Class<L> objectListClass, 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 * notably the metadata, is made available through the methods
* defined by {@link KubernetesObject}. * defined by {@link KubernetesObject}.
*/ */
@SuppressWarnings("PMD.DataClass")
public class K8sDynamicModel implements KubernetesObject { public class K8sDynamicModel implements KubernetesObject {
private final V1ObjectMeta metadata; private final V1ObjectMeta metadata;

View file

@ -62,7 +62,7 @@ public class K8sDynamicModelsBase<T extends K8sDynamicModel>
} catch (InstantiationException | IllegalAccessException } catch (InstantiationException | IllegalAccessException
| IllegalArgumentException | InvocationTargetException | IllegalArgumentException | InvocationTargetException
| NoSuchMethodException | SecurityException exc) { | 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 * state and can therefore be used for any kind of object, especially
* custom objects. * custom objects.
*/ */
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class K8sDynamicStub public class K8sDynamicStub
extends K8sDynamicStubBase<K8sDynamicModel, K8sDynamicModels> { extends K8sDynamicStubBase<K8sDynamicModel, K8sDynamicModels> {
@ -64,8 +63,6 @@ public class K8sDynamicStub
* @return the stub if the object exists * @return the stub if the object exists
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
public static K8sDynamicStub get(K8sClient client, public static K8sDynamicStub get(K8sClient client,
GroupVersionKind gvk, String namespace, String name) GroupVersionKind gvk, String namespace, String name)
throws ApiException { throws ApiException {
@ -83,8 +80,6 @@ public class K8sDynamicStub
* @return the stub if the object exists * @return the stub if the object exists
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
public static K8sDynamicStub get(K8sClient client, public static K8sDynamicStub get(K8sClient client,
APIResource context, String namespace, String name) { APIResource context, String namespace, String name) {
return new K8sDynamicStub(client, context, namespace, 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 * state and can therefore be used for any kind of object, especially
* custom objects. * custom objects.
*/ */
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public abstract class K8sDynamicStubBase<O extends K8sDynamicModel, public abstract class K8sDynamicStubBase<O extends K8sDynamicModel,
L extends K8sDynamicModelsBase<O>> extends K8sGenericStub<O, L> { L extends K8sDynamicModelsBase<O>> extends K8sGenericStub<O, L> {
@ -40,7 +39,6 @@ public abstract class K8sDynamicStubBase<O extends K8sDynamicModel,
* @param namespace the namespace * @param namespace the namespace
* @param name the name * @param name the name
*/ */
@SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
public K8sDynamicStubBase(Class<O> objectClass, public K8sDynamicStubBase(Class<O> objectClass,
Class<L> objectListClass, DynamicTypeAdapterFactory<O, L> taf, Class<L> objectListClass, DynamicTypeAdapterFactory<O, L> taf,
K8sClient client, APIResource context, String namespace, K8sClient client, APIResource context, String namespace,

View file

@ -48,7 +48,7 @@ import java.util.function.Function;
* @param <O> the generic type * @param <O> the generic type
* @param <L> the generic type * @param <L> the generic type
*/ */
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.TooManyMethods" }) @SuppressWarnings({ "PMD.TooManyMethods" })
public class K8sGenericStub<O extends KubernetesObject, public class K8sGenericStub<O extends KubernetesObject,
L extends KubernetesListObject> { L extends KubernetesListObject> {
protected final K8sClient client; 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 * @return the updated model or empty if the object was not found
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
@SuppressWarnings("PMD.AssignmentInOperand")
public Optional<O> updateStatus(O object, Function<O, Object> updater) public Optional<O> updateStatus(O object, Function<O, Object> updater)
throws ApiException { throws ApiException {
return K8s.optional(api.updateStatus(object, updater)); 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 * @return the updated model or empty if the object was not found
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
@SuppressWarnings({ "PMD.AssignmentInOperand", "PMD.UnusedAssignment" }) @SuppressWarnings({ "PMD.AssignmentInOperand" })
public Optional<O> updateStatus(Function<O, Object> updater, O current, public Optional<O> updateStatus(Function<O, Object> updater, O current,
int retries) throws ApiException { int retries) throws ApiException {
while (true) { while (true) {
@ -248,7 +247,6 @@ public class K8sGenericStub<O extends KubernetesObject,
* @return the updated model or empty if the object was not found * @return the updated model or empty if the object was not found
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
@SuppressWarnings({ "PMD.AssignmentInOperand", "PMD.UnusedAssignment" })
public Optional<O> updateStatus(Function<O, Object> updater, int retries) public Optional<O> updateStatus(Function<O, Object> updater, int retries)
throws ApiException { throws ApiException {
return updateStatus(updater, null, retries); return updateStatus(updater, null, retries);
@ -359,6 +357,7 @@ public class K8sGenericStub<O extends KubernetesObject,
* @param <L> the object list type * @param <L> the object list type
* @param <R> the result type * @param <R> the result type
*/ */
@FunctionalInterface
public interface GenericSupplier<O extends KubernetesObject, public interface GenericSupplier<O extends KubernetesObject,
L extends KubernetesListObject, R extends K8sGenericStub<O, L>> { L extends KubernetesListObject, R extends K8sGenericStub<O, L>> {
@ -370,7 +369,6 @@ public class K8sGenericStub<O extends KubernetesObject,
* @param name the name * @param name the name
* @return the result * @return the result
*/ */
@SuppressWarnings("PMD.UseObjectForClearerAPI")
R get(K8sClient client, String namespace, String name); 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 * @return the stub if the object exists
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
public static <O extends KubernetesObject, L extends KubernetesListObject, public static <O extends KubernetesObject, L extends KubernetesListObject,
R extends K8sGenericStub<O, L>> R extends K8sGenericStub<O, L>>
R create(Class<O> objectClass, Class<L> objectListClass, 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 io.kubernetes.client.util.generic.options.ListOptions;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.Optional;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -49,7 +50,6 @@ public class K8sObserver<O extends KubernetesObject,
ADDED, MODIFIED, DELETED ADDED, MODIFIED, DELETED
} }
@SuppressWarnings("PMD.FieldNamingConventions")
protected final Logger logger = Logger.getLogger(getClass().getName()); protected final Logger logger = Logger.getLogger(getClass().getName());
protected final K8sClient client; protected final K8sClient client;
@ -72,8 +72,7 @@ public class K8sObserver<O extends KubernetesObject,
* @param namespace the namespace * @param namespace the namespace
* @param options the options * @param options the options
*/ */
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop", @SuppressWarnings({ "PMD.AvoidCatchingThrowable",
"PMD.UseObjectForClearerAPI", "PMD.AvoidCatchingThrowable",
"PMD.CognitiveComplexity", "PMD.AvoidCatchingGenericException" }) "PMD.CognitiveComplexity", "PMD.AvoidCatchingGenericException" })
public K8sObserver(Class<O> objectClass, Class<L> objectListClass, public K8sObserver(Class<O> objectClass, Class<L> objectListClass,
K8sClient client, APIResource context, String namespace, K8sClient client, APIResource context, String namespace,
@ -89,23 +88,29 @@ public class K8sObserver<O extends KubernetesObject,
thread = (Components.useVirtualThreads() ? Thread.ofVirtual() thread = (Components.useVirtualThreads() ? Thread.ofVirtual()
: Thread.ofPlatform()).unstarted(() -> { : Thread.ofPlatform()).unstarted(() -> {
try { try {
logger logger.fine(() -> "Observing " + context.getResourcePlural()
.config(() -> "Watching " + context.getResourcePlural() + " (" + context.getPreferredVersion() + ")"
+ " (" + context.getPreferredVersion() + ")" + Optional.ofNullable(options.getLabelSelector())
+ " in " + namespace); .map(ls -> " with labels " + ls).orElse("")
+ " in " + namespace);
// Watch sometimes terminates without apparent reason. // Watch sometimes terminates without apparent reason.
while (!Thread.currentThread().isInterrupted()) { while (!Thread.currentThread().isInterrupted()) {
Instant startedAt = Instant.now(); Instant startedAt = Instant.now();
try { try {
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
var changed var changed
= api.watch(namespace, options).iterator(); = api.watch(namespace, options).iterator();
while (changed.hasNext()) { 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) { } catch (ApiException | RuntimeException e) {
logger.log(Level.FINE, e, () -> "Problem watching" logger.log(Level.FINE, e, () -> "Problem watching"
+ " resource " + context.getKind()
+ " (will retry): " + e.getMessage()); + " (will retry): " + e.getMessage());
delayRestart(startedAt); delayRestart(startedAt);
} }
@ -225,7 +230,6 @@ public class K8sObserver<O extends KubernetesObject,
} }
@Override @Override
@SuppressWarnings("PMD.UseLocaleWithCaseConversions")
public String toString() { public String toString() {
return "Observer for " + K8s.toString(context) + " " + namespace; return "Observer for " + K8s.toString(context) + " " + namespace;
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -29,7 +29,6 @@ import java.util.List;
/** /**
* A stub for secrets (v1). * A stub for secrets (v1).
*/ */
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class K8sV1SecretStub extends K8sGenericStub<V1Secret, V1SecretList> { public class K8sV1SecretStub extends K8sGenericStub<V1Secret, V1SecretList> {
public static final APIResource CONTEXT = new APIResource("", List.of("v1"), 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). * A stub for secrets (v1).
*/ */
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class K8sV1ServiceStub extends K8sGenericStub<V1Service, V1ServiceList> { public class K8sV1ServiceStub extends K8sGenericStub<V1Service, V1ServiceList> {
public static final APIResource CONTEXT = new APIResource("", List.of("v1"), 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). * A stub for stateful sets (v1).
*/ */
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class K8sV1StatefulSetStub public class K8sV1StatefulSetStub
extends K8sGenericStub<V1StatefulSet, V1StatefulSetList> { extends K8sGenericStub<V1StatefulSet, V1StatefulSetList> {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -43,7 +43,6 @@ public interface ChannelDictionary<K, C extends Channel, A> {
* @param channel the channel * @param channel the channel
* @param associated the associated * @param associated the associated
*/ */
@SuppressWarnings("PMD.ShortClassName")
public record Value<C extends Channel, A>(C channel, A associated) { 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 * @param supplier the supplier
* @return the channel * @return the channel
*/ */
@SuppressWarnings({ "PMD.AssignmentInOperand",
"PMD.DataflowAnomalyAnalysis" })
public C computeIfAbsent(K key, Function<K, C> supplier) { public C computeIfAbsent(K key, Function<K, C> supplier) {
return entries.computeIfAbsent(key, return entries.computeIfAbsent(key,
k -> new Value<>(supplier.apply(k), null)).channel(); 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. * Gets the current display secret and optionally updates it.
*/ */
@SuppressWarnings("PMD.DataClass")
public class GetDisplaySecret extends Event<String> { public class GetDisplaySecret extends Event<String> {
private final VmDefinition vmDef; private final VmDefinition vmDef;

View file

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

View file

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

View file

@ -24,7 +24,6 @@ import org.jgrapes.core.Event;
/** /**
* Modifies a VM. * Modifies a VM.
*/ */
@SuppressWarnings("PMD.DataClass")
public class ModifyVm extends Event<Void> { public class ModifyVm extends Event<Void> {
private final String name; 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. * Triggers a reset of the VM.
*/ */
@SuppressWarnings("PMD.DataClass")
public class ResetVm extends Event<String> { public class ResetVm extends Event<String> {
private final String vmName; 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. * Note the assignment to a user in the VM status.
*/ */
@SuppressWarnings("PMD.DataClass")
public class UpdateAssignment extends Event<Boolean> { public class UpdateAssignment extends Event<Boolean> {
private final VmPool fromPool; 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.K8sClient;
import org.jdrupes.vmoperator.common.VmDefinition; import org.jdrupes.vmoperator.common.VmDefinition;
import org.jgrapes.core.Channel; import org.jgrapes.core.Channel;
import org.jgrapes.core.Event;
import org.jgrapes.core.EventPipeline; import org.jgrapes.core.EventPipeline;
import org.jgrapes.core.Subchannel.DefaultSubchannel; import org.jgrapes.core.Subchannel.DefaultSubchannel;
/** /**
* A subchannel used to send the events related to a specific VM. * A subchannel used to send the events related to a specific VM.
*/ */
@SuppressWarnings("PMD.DataClass")
public class VmChannel extends DefaultSubchannel { public class VmChannel extends DefaultSubchannel {
private final EventPipeline pipeline; private final EventPipeline pipeline;
@ -55,7 +55,6 @@ public class VmChannel extends DefaultSubchannel {
* @param definition the definition * @param definition the definition
* @return the watch channel * @return the watch channel
*/ */
@SuppressWarnings("PMD.LinguisticNaming")
public VmChannel setVmDefinition(VmDefinition definition) { public VmChannel setVmDefinition(VmDefinition definition) {
this.definition = definition; this.definition = definition;
return this; return this;
@ -86,7 +85,6 @@ public class VmChannel extends DefaultSubchannel {
* @param generation the generation to set * @param generation the generation to set
* @return true if value has changed * @return true if value has changed
*/ */
@SuppressWarnings("PMD.LinguisticNaming")
public boolean setGeneration(long generation) { public boolean setGeneration(long generation) {
if (this.generation == generation) { if (this.generation == generation) {
return false; return false;
@ -104,6 +102,19 @@ public class VmChannel extends DefaultSubchannel {
return pipeline; 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. * Returns the API client.
* *

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
# #
# VM-Operator # 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 # 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 # under the terms of the GNU General Public License as published by
@ -19,10 +19,7 @@
handlers=java.util.logging.ConsoleHandler, \ handlers=java.util.logging.ConsoleHandler, \
org.jgrapes.webconlet.logviewer.LogViewerHandler org.jgrapes.webconlet.logviewer.LogViewerHandler
org.jgrapes.level=FINE org.jdrupes.vmoperator.level=FINE
org.jgrapes.core.handlerTracking.level=FINER
org.jdrupes.vmoperator.manager.level=FINE
java.util.logging.ConsoleHandler.level=ALL java.util.logging.ConsoleHandler.level=ALL
java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter 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. # The template to use. Resolved relative to /usr/share/vmrunner/templates.
# template: "Standard-VM-latest.ftl.yaml" # template: "Standard-VM-latest.ftl.yaml"
<#if spec.runnerTemplate?? && spec.runnerTemplate.source?? > <#if spec.runnerTemplate?? && spec.runnerTemplate.source?? >
template: ${ cm.spec().runnerTemplate.source } template: ${ spec.runnerTemplate.source }
</#if> </#if>
# The template is copied to the data diretory when the VM starts for # 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 # 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 # decide to trigger a reset later, you have to first set the value
# and then inrement it. # and then inrement it.
resetCounter: ${ cr.extra().get().resetCount()?c } resetCounter: ${ cr.extra().resetCount()?c }
# Forward the cloud-init data if provided # Forward the cloud-init data if provided
<#if spec.cloudInit??> <#if spec.cloudInit??>

View file

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

View file

@ -56,7 +56,6 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
/** /**
* Delegee for reconciling the config map * Delegee for reconciling the config map
*/ */
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
/* default */ class ConfigMapReconciler { /* default */ class ConfigMapReconciler {
protected final Logger logger = Logger.getLogger(getClass().getName()); protected final Logger logger = Logger.getLogger(getClass().getName());
@ -76,15 +75,31 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
* *
* @param model the model * @param model the model
* @param channel the channel * @param channel the channel
* @param modelChanged the model has changed
* @throws IOException Signals that an I/O exception has occurred. * @throws IOException Signals that an I/O exception has occurred.
* @throws TemplateException the template exception * @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 { 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 // Combine template and data and parse result
logger.fine(() -> "Create/update configmap "
+ DataPath.<String> get(model, "cr", "name").orElse("unknown"));
model.put("adjustCloudInitMeta", adjustCloudInitMetaModel); model.put("adjustCloudInitMeta", adjustCloudInitMetaModel);
prevData.added.put("adjustCloudInitMeta", adjustCloudInitMetaModel);
var fmTemplate = fmConfig.getTemplate("runnerConfig.ftl.yaml"); var fmTemplate = fmConfig.getTemplate("runnerConfig.ftl.yaml");
StringWriter out = new StringWriter(); StringWriter out = new StringWriter();
fmTemplate.process(model, out); fmTemplate.process(model, out);
@ -107,19 +122,6 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
.get().addProperty("logging.properties", props); .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 // Get API and update
DynamicKubernetesApi cmApi = new DynamicKubernetesApi("", "v1", DynamicKubernetesApi cmApi = new DynamicKubernetesApi("", "v1",
"configmaps", channel.client()); "configmaps", channel.client());
@ -129,6 +131,14 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
maybeForceUpdate(channel.client(), updatedCm); maybeForceUpdate(channel.client(), updatedCm);
model.put("configMapResourceVersion", model.put("configMapResourceVersion",
updatedCm.getMetadata().getResourceVersion()); 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 private final TemplateMethodModelEx adjustCloudInitMetaModel
= new TemplateMethodModelEx() { = new TemplateMethodModelEx() {
@Override @Override
@SuppressWarnings("PMD.PreserveStackTrace")
public Object exec(@SuppressWarnings("rawtypes") List arguments) public Object exec(@SuppressWarnings("rawtypes") List arguments)
throws TemplateModelException { throws TemplateModelException {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")

View file

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

View file

@ -1,6 +1,6 @@
/* /*
* VM-Operator * 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 * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * 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 com.google.gson.JsonObject;
import io.kubernetes.client.apimachinery.GroupVersionKind; import io.kubernetes.client.apimachinery.GroupVersionKind;
import io.kubernetes.client.custom.V1Patch;
import io.kubernetes.client.openapi.ApiException; import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.Configuration; import io.kubernetes.client.openapi.Configuration;
import java.io.IOException; import java.io.IOException;
import java.net.HttpURLConnection;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.Instant; import java.time.Instant;
import java.util.Comparator;
import java.util.Optional;
import java.util.logging.Level; import java.util.logging.Level;
import org.jdrupes.vmoperator.common.Constants.Crd; import org.jdrupes.vmoperator.common.Constants.Crd;
import org.jdrupes.vmoperator.common.Constants.Status; import org.jdrupes.vmoperator.common.Constants.Status;
import org.jdrupes.vmoperator.common.K8sClient; 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.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.ChannelManager;
import org.jdrupes.vmoperator.manager.events.Exit; 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.ModifyVm;
import org.jdrupes.vmoperator.manager.events.PodChanged;
import org.jdrupes.vmoperator.manager.events.UpdateAssignment; import org.jdrupes.vmoperator.manager.events.UpdateAssignment;
import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged; import org.jdrupes.vmoperator.manager.events.VmPoolChanged;
import org.jdrupes.vmoperator.util.GsonPtr; import org.jdrupes.vmoperator.manager.events.VmResourceChanged;
import org.jgrapes.core.Channel; import org.jgrapes.core.Channel;
import org.jgrapes.core.Component; import org.jgrapes.core.Component;
import org.jgrapes.core.EventPipeline;
import org.jgrapes.core.annotation.Handler; import org.jgrapes.core.annotation.Handler;
import org.jgrapes.core.events.HandlingError; import org.jgrapes.core.events.HandlingError;
import org.jgrapes.core.events.Start; 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 * The implementation splits the controller in two components. The
* {@link VmMonitor} and the {@link Reconciler}. The former watches * {@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 * when they change. The latter handles the changes and reconciles the
* resources in the cluster. * resources in the cluster.
* *
@ -87,6 +95,7 @@ import org.jgrapes.util.events.ConfigurationUpdate;
public class Controller extends Component { public class Controller extends Component {
private String namespace; private String namespace;
private final ChannelManager<String, VmChannel, EventPipeline> chanMgr;
/** /**
* Creates a new instance. * Creates a new instance.
@ -95,17 +104,16 @@ public class Controller extends Component {
public Controller(Channel componentChannel) { public Controller(Channel componentChannel) {
super(componentChannel); super(componentChannel);
// Prepare component tree // Prepare component tree
ChannelManager<String, VmChannel, ?> chanMgr chanMgr = new ChannelManager<>(name -> {
= new ChannelManager<>(name -> { try {
try { return new VmChannel(channel(), newEventPipeline(),
return new VmChannel(channel(), newEventPipeline(), new K8sClient());
new K8sClient()); } catch (IOException e) {
} catch (IOException e) { logger.log(Level.SEVERE, e, () -> "Failed to create client"
logger.log(Level.SEVERE, e, () -> "Failed to create client" + " for handling changes: " + e.getMessage());
+ " for handling changes: " + e.getMessage()); return null;
return null; }
} });
});
attach(new VmMonitor(channel(), chanMgr)); attach(new VmMonitor(channel(), chanMgr));
attach(new DisplaySecretMonitor(channel(), chanMgr)); attach(new DisplaySecretMonitor(channel(), chanMgr));
// Currently, we don't use the IP assigned by the load balancer // Currently, we don't use the IP assigned by the load balancer
@ -113,6 +121,7 @@ public class Controller extends Component {
// attach(new ServiceMonitor(channel()).channelManager(chanMgr)); // attach(new ServiceMonitor(channel()).channelManager(chanMgr));
attach(new Reconciler(channel())); attach(new Reconciler(channel()));
attach(new PoolMonitor(channel())); attach(new PoolMonitor(channel()));
attach(new PodMonitor(channel(), chanMgr));
} }
/** /**
@ -174,77 +183,146 @@ public class Controller extends Component {
fire(new Exit(2)); fire(new Exit(2));
return; 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 * @param event the event
* @throws ApiException the api exception * @throws ApiException the api exception
* @throws IOException Signals that an I/O exception has occurred. * @throws InterruptedException
*/ */
@Handler @Handler
public void onModifyVm(ModifyVm event, VmChannel channel) @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
throws ApiException, IOException { public void onAssignVm(AssignVm event)
patchVmDef(channel.client(), event.name(), "spec/vm/" + event.path(), throws ApiException, InterruptedException {
event.value()); 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;
}
// 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;
}
// 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 void patchVmDef(K8sClient client, String name, String path, private static Comparator<VmChannel> preferRunning
Object value) throws ApiException, IOException { = new Comparator<>() {
var vmStub = K8sDynamicStub.get(client, @Override
new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM), namespace, public int compare(VmChannel ch1, VmChannel ch2) {
name); if (ch1.vmDefinition().conditionStatus("Running").orElse(false)
&& !ch2.vmDefinition().conditionStatus("Running")
.orElse(false)) {
return -1;
}
return 0;
}
};
// Patch running /**
String valueAsText = value instanceof String * When s pool is deleted, remove all related assignments.
? "\"" + value + "\"" *
: value.toString(); * @param event the event
var res = vmStub.patch(V1Patch.PATCH_FORMAT_JSON_PATCH, * @throws InterruptedException
new V1Patch("[{\"op\": \"replace\", \"path\": \"/" */
+ path + "\", \"value\": " + valueAsText + "}]"), @Handler
client.defaultPatchOptions()); @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
if (!res.isPresent()) { public void onPoolChanged(VmPoolChanged event) throws InterruptedException {
logger.warning( if (!event.deleted()) {
() -> "Cannot patch definition for Vm " + vmStub.name()); 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 * Remove runner version from status when pod is deleted
* 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 event the event
* @param channel the channel * @param channel the channel
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
@Handler @Handler
public void onUpdatedAssignment(UpdateAssignment event, VmChannel channel) public void onPodChange(PodChanged event, VmChannel channel)
throws ApiException { throws ApiException {
try { if (event.type() == ResponseType.DELETED) {
// Remove runner info from status
var vmDef = channel.vmDefinition(); var vmDef = channel.vmDefinition();
var vmStub = VmDefinitionStub.get(channel.client(), var vmStub = VmDefinitionStub.get(channel.client(),
new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM), new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM),
vmDef.namespace(), vmDef.name()); vmDef.namespace(), vmDef.name());
if (vmStub.updateStatus(vmDef, from -> { vmStub.updateStatus(from -> {
JsonObject status = from.statusJson(); JsonObject status = from.statusJson();
var assignment = GsonPtr.to(status).to(Status.ASSIGNMENT); status.remove(Status.RUNNER_VERSION);
assignment.set("pool", event.fromPool().name());
assignment.set("user", event.toUser());
assignment.set("lastUsed", Instant.now().toString());
return status; 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 * 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. * in the pod that reflect the information from the secret.
*/ */
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.TooManyStaticImports" })
public class DisplaySecretMonitor public class DisplaySecretMonitor
extends AbstractMonitor<V1Secret, V1SecretList, VmChannel> { 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.common.VmDefinitionStub;
import org.jdrupes.vmoperator.manager.events.GetDisplaySecret; import org.jdrupes.vmoperator.manager.events.GetDisplaySecret;
import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged; import org.jdrupes.vmoperator.manager.events.VmResourceChanged;
import org.jdrupes.vmoperator.util.DataPath; import org.jdrupes.vmoperator.util.DataPath;
import org.jgrapes.core.Channel; import org.jgrapes.core.Channel;
import org.jgrapes.core.CompletionLock; import org.jgrapes.core.CompletionLock;
@ -66,7 +66,6 @@ import org.jose4j.base64url.Base64;
* * `passwordValidity`: the validity of the random password in seconds. * * `passwordValidity`: the validity of the random password in seconds.
* Used to calculate the password expiry time in the generated secret. * Used to calculate the password expiry time in the generated secret.
*/ */
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.TooManyStaticImports" })
public class DisplaySecretReconciler extends Component { public class DisplaySecretReconciler extends Component {
protected final Logger logger = Logger.getLogger(getClass().getName()); protected final Logger logger = Logger.getLogger(getClass().getName());
@ -104,12 +103,15 @@ public class DisplaySecretReconciler extends Component {
return oldConfig; return oldConfig;
}).ifPresent(c -> { }).ifPresent(c -> {
try { try {
if (c.containsKey("passwordValidity")) { Optional.ofNullable(c.get("passwordValidity"))
passwordValidity = Integer .map(p -> p instanceof Integer ? (Integer) p
.parseInt((String) c.get("passwordValidity")); : Integer.valueOf((String) p))
} .ifPresent(p -> {
} catch (ClassCastException e) { passwordValidity = p;
logger.config("Malformed configuration: " + e.getMessage()); });
} 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 * secret with a random password and immediate expiration, thus
* preventing access to the display. * preventing access to the display.
* *
* @param event the event * @param vmDef the VM definition
* @param model the model * @param model the model
* @param channel the channel * @param channel the channel
* @param specChanged the spec changed
* @throws IOException Signals that an I/O exception has occurred. * @throws IOException Signals that an I/O exception has occurred.
* @throws TemplateException the template exception * @throws TemplateException the template exception
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
public void reconcile(VmDefChanged event, public void reconcile(VmDefinition vmDef, Map<String, Object> model,
Map<String, Object> model, VmChannel channel) VmChannel channel, boolean specChanged)
throws IOException, TemplateException, ApiException { throws IOException, TemplateException, ApiException {
// Nothing to do unless spec changed
if (!specChanged) {
return;
}
// Secret needed at all? // Secret needed at all?
var display = event.vmDefinition().fromVm("display").get(); var display = vmDef.fromVm("display").get();
if (!DataPath.<Boolean> get(display, "spice", "generateSecret") if (!DataPath.<Boolean> get(display, "spice", "generateSecret")
.orElse(true)) { .orElse(true)) {
return; return;
} }
// Check if exists // Check if exists
var vmDef = event.vmDefinition();
ListOptions options = new ListOptions(); ListOptions options = new ListOptions();
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + "," options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
+ "app.kubernetes.io/component=" + DisplaySecret.NAME + "," + "app.kubernetes.io/component=" + DisplaySecret.NAME + ","
@ -150,9 +157,11 @@ public class DisplaySecretReconciler extends Component {
} }
// Create secret // Create secret
var secretName = vmDef.name() + "-" + DisplaySecret.NAME;
logger.fine(() -> "Create/update secret " + secretName);
var secret = new V1Secret(); var secret = new V1Secret();
secret.setMetadata(new V1ObjectMeta().namespace(vmDef.namespace()) 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/name", APP_NAME)
.putLabelsItem("app.kubernetes.io/component", DisplaySecret.NAME) .putLabelsItem("app.kubernetes.io/component", DisplaySecret.NAME)
.putLabelsItem("app.kubernetes.io/instance", vmDef.name())); .putLabelsItem("app.kubernetes.io/instance", vmDef.name()));
@ -183,7 +192,6 @@ public class DisplaySecretReconciler extends Component {
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
@Handler @Handler
@SuppressWarnings("PMD.StringInstantiation")
public void onGetDisplaySecret(GetDisplaySecret event, VmChannel channel) public void onGetDisplaySecret(GetDisplaySecret event, VmChannel channel)
throws ApiException { throws ApiException {
// Get VM definition and check if running // Get VM definition and check if running
@ -293,7 +301,7 @@ public class DisplaySecretReconciler extends Component {
*/ */
@Handler @Handler
@SuppressWarnings("PMD.AvoidSynchronizedStatement") @SuppressWarnings("PMD.AvoidSynchronizedStatement")
public void onVmDefChanged(VmDefChanged event, Channel channel) { public void onVmResourceChanged(VmResourceChanged event, Channel channel) {
synchronized (pendingPrepares) { synchronized (pendingPrepares) {
String vmName = event.vmDefinition().name(); String vmName = event.vmDefinition().name();
for (var pending : pendingPrepares) { for (var pending : pendingPrepares) {
@ -312,7 +320,6 @@ public class DisplaySecretReconciler extends Component {
/** /**
* The Class PendingGet. * The Class PendingGet.
*/ */
@SuppressWarnings("PMD.DataClass")
private static class PendingRequest { private static class PendingRequest {
public final GetDisplaySecret event; public final GetDisplaySecret event;
public final long expectedSerial; 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.K8sV1ServiceStub;
import org.jdrupes.vmoperator.common.VmDefinition; import org.jdrupes.vmoperator.common.VmDefinition;
import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged; import org.jdrupes.vmoperator.util.DataPath;
import org.jdrupes.vmoperator.util.GsonPtr; import org.jdrupes.vmoperator.util.GsonPtr;
import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.Yaml;
@ -45,7 +45,6 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
/** /**
* Delegee for reconciling the service * Delegee for reconciling the service
*/ */
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
/* default */ class LoadBalancerReconciler { /* default */ class LoadBalancerReconciler {
private static final String LOAD_BALANCER_SERVICE = "loadBalancerService"; private static final String LOAD_BALANCER_SERVICE = "loadBalancerService";
@ -69,18 +68,24 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
/** /**
* Reconcile. * Reconcile.
* *
* @param event the event * @param vmDef the VM definition
* @param model the model * @param model the model
* @param channel the channel * @param channel the channel
* @param specChanged the spec changed
* @throws IOException Signals that an I/O exception has occurred. * @throws IOException Signals that an I/O exception has occurred.
* @throws TemplateException the template exception * @throws TemplateException the template exception
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
public void reconcile(VmDefChanged event, public void reconcile(VmDefinition vmDef, Map<String, Object> model,
Map<String, Object> model, VmChannel channel) VmChannel channel, boolean specChanged)
throws IOException, TemplateException, ApiException { throws IOException, TemplateException, ApiException {
// Nothing to do unless spec changed
if (!specChanged) {
return;
}
// Check if to be generated // Check if to be generated
@SuppressWarnings({ "PMD.AvoidDuplicateLiterals", "unchecked" }) @SuppressWarnings({ "unchecked" })
var lbsDef = Optional.of(model) var lbsDef = Optional.of(model)
.map(m -> (Map<String, Object>) m.get("reconciler")) .map(m -> (Map<String, Object>) m.get("reconciler"))
.map(c -> c.get(LOAD_BALANCER_SERVICE)).orElse(Boolean.FALSE); .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 // Load balancer can also be turned off for VM
var vmDef = event.vmDefinition();
if (vmDef if (vmDef
.<Map<String, Map<String, String>>> fromSpec(LOAD_BALANCER_SERVICE) .<Map<String, Map<String, String>>> fromSpec(LOAD_BALANCER_SERVICE)
.map(m -> m.isEmpty()).orElse(false)) { .map(m -> m.isEmpty()).orElse(false)) {
@ -103,6 +107,8 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
} }
// Combine template and data and parse result // 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"); var fmTemplate = fmConfig.getTemplate("runnerLoadBalancer.ftl.yaml");
StringWriter out = new StringWriter(); StringWriter out = new StringWriter();
fmTemplate.process(model, out); fmTemplate.process(model, out);

View file

@ -81,7 +81,7 @@ import org.jgrapes.webconsole.vuejs.VueJsConsoleWeblet;
/** /**
* The application class. * The application class.
*/ */
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports" }) @SuppressWarnings({ "PMD.ExcessiveImports" })
public class Manager extends Component { public class Manager extends Component {
private static String version; private static String version;
@ -97,8 +97,8 @@ public class Manager extends Component {
* @throws IOException Signals that an I/O exception has occurred. * @throws IOException Signals that an I/O exception has occurred.
* @throws URISyntaxException * @throws URISyntaxException
*/ */
@SuppressWarnings({ "PMD.TooFewBranchesForASwitchStatement", @SuppressWarnings({ "PMD.NcssCount",
"PMD.NcssCount", "PMD.ConstructorCallsOverridableMethod" }) "PMD.ConstructorCallsOverridableMethod" })
public Manager(CommandLine cmdLine) throws IOException, URISyntaxException { public Manager(CommandLine cmdLine) throws IOException, URISyntaxException {
super(new NamedChannel("manager")); super(new NamedChannel("manager"));
// Prepare component tree // Prepare component tree
@ -217,7 +217,6 @@ public class Manager extends Component {
* @param event the event * @param event the event
*/ */
@Handler @Handler
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public void onConfigurationUpdate(ConfigurationUpdate event) { public void onConfigurationUpdate(ConfigurationUpdate event) {
event.structured(componentPath()).ifPresent(c -> { event.structured(componentPath()).ifPresent(c -> {
if (c.containsKey("clusterName")) { if (c.containsKey("clusterName")) {
@ -264,7 +263,7 @@ public class Manager extends Component {
*/ */
@Handler(priority = -1000) @Handler(priority = -1000)
public void onStop(Stop event) { public void onStop(Stop event) {
logger.fine(() -> "Application stopped."); logger.info(() -> "Application stopped.");
} }
static { static {
@ -291,7 +290,6 @@ public class Manager extends Component {
* @param args the arguments * @param args the arguments
* @throws Exception the exception * @throws Exception the exception
*/ */
@SuppressWarnings("PMD.SignatureDeclareThrowsException")
public static void main(String[] args) { public static void main(String[] args) {
try { try {
// Instance logger is not available yet. // 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 freemarker.template.TemplateException;
import io.kubernetes.client.openapi.ApiException; import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.util.generic.dynamic.Dynamics; 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 io.kubernetes.client.util.generic.options.PatchOptions;
import java.io.IOException; import java.io.IOException;
import java.io.StringWriter; import java.io.StringWriter;
import java.util.Map; import java.util.Map;
import java.util.logging.Logger; import java.util.logging.Logger;
import static org.jdrupes.vmoperator.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.K8sV1PodStub;
import org.jdrupes.vmoperator.common.K8sV1SecretStub;
import org.jdrupes.vmoperator.common.VmDefinition;
import org.jdrupes.vmoperator.common.VmDefinition.RequestedVmState; import org.jdrupes.vmoperator.common.VmDefinition.RequestedVmState;
import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor; import org.yaml.snakeyaml.constructor.SafeConstructor;
@ -38,7 +43,6 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
/** /**
* Delegee for reconciling the pod. * Delegee for reconciling the pod.
*/ */
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
/* default */ class PodReconciler { /* default */ class PodReconciler {
protected final Logger logger = Logger.getLogger(getClass().getName()); protected final Logger logger = Logger.getLogger(getClass().getName());
@ -56,23 +60,18 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
/** /**
* Reconcile the pod. * Reconcile the pod.
* *
* @param event the event * @param vmDef the vm def
* @param model the model * @param model the model
* @param channel the channel * @param channel the channel
* @param specChanged the spec changed
* @throws IOException Signals that an I/O exception has occurred. * @throws IOException Signals that an I/O exception has occurred.
* @throws TemplateException the template exception * @throws TemplateException the template exception
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
public void reconcile(VmDefChanged event, Map<String, Object> model, public void reconcile(VmDefinition vmDef, Map<String, Object> model,
VmChannel channel) VmChannel channel, boolean specChanged)
throws IOException, TemplateException, ApiException { 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. // Get pod stub.
var vmDef = event.vmDefinition();
var podStub = K8sV1PodStub.get(channel.client(), vmDef.namespace(), var podStub = K8sV1PodStub.get(channel.client(), vmDef.namespace(),
vmDef.name()); vmDef.name());
@ -92,6 +91,7 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
// Create pod. First combine template and data and parse result // Create pod. First combine template and data and parse result
logger.fine(() -> "Create/update pod " + podStub.name()); logger.fine(() -> "Create/update pod " + podStub.name());
addDisplaySecret(channel.client(), model, vmDef);
var fmTemplate = fmConfig.getTemplate("runnerPod.ftl.yaml"); var fmTemplate = fmConfig.getTemplate("runnerPod.ftl.yaml");
StringWriter out = new StringWriter(); StringWriter out = new StringWriter();
fmTemplate.process(model, out); 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.VmDefinitionStub;
import org.jdrupes.vmoperator.common.VmPool; import org.jdrupes.vmoperator.common.VmPool;
import org.jdrupes.vmoperator.manager.events.GetPools; 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.VmPoolChanged;
import org.jdrupes.vmoperator.manager.events.VmResourceChanged;
import org.jdrupes.vmoperator.util.GsonPtr; import org.jdrupes.vmoperator.util.GsonPtr;
import org.jgrapes.core.Channel; import org.jgrapes.core.Channel;
import org.jgrapes.core.EventPipeline; import org.jgrapes.core.EventPipeline;
@ -53,7 +53,6 @@ import org.jgrapes.core.events.Attached;
* {@link VmPoolChanged} events fired on a special pipeline to * {@link VmPoolChanged} events fired on a special pipeline to
* avoid concurrent change informations. * avoid concurrent change informations.
*/ */
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports" })
public class PoolMonitor extends public class PoolMonitor extends
AbstractMonitor<K8sDynamicModel, K8sDynamicModels, Channel> { AbstractMonitor<K8sDynamicModel, K8sDynamicModels, Channel> {
@ -142,7 +141,8 @@ public class PoolMonitor extends
* @throws ApiException * @throws ApiException
*/ */
@Handler @Handler
public void onVmDefChanged(VmDefChanged event) throws ApiException { public void onVmResourceChanged(VmResourceChanged event)
throws ApiException {
final var vmDef = event.vmDefinition(); final var vmDef = event.vmDefinition();
final String vmName = vmDef.name(); final String vmName = vmDef.name();
switch (event.type()) { 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.APP_NAME;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_NAME; import static org.jdrupes.vmoperator.common.Constants.VM_OP_NAME;
import org.jdrupes.vmoperator.common.K8sV1PvcStub; 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.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jdrupes.vmoperator.util.DataPath; import org.jdrupes.vmoperator.util.DataPath;
import org.jdrupes.vmoperator.util.GsonPtr; import org.jdrupes.vmoperator.util.GsonPtr;
import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.LoaderOptions;
@ -49,7 +49,6 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
/** /**
* Delegee for reconciling the stateful set (effectively the pod). * Delegee for reconciling the stateful set (effectively the pod).
*/ */
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
/* default */ class PvcReconciler { /* default */ class PvcReconciler {
protected final Logger logger = Logger.getLogger(getClass().getName()); protected final Logger logger = Logger.getLogger(getClass().getName());
@ -67,32 +66,35 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
/** /**
* Reconcile the PVCs. * Reconcile the PVCs.
* *
* @param event the event * @param vmDef the VM definition
* @param model the model * @param model the model
* @param channel the channel * @param channel the channel
* @param specChanged the spec changed
* @throws IOException Signals that an I/O exception has occurred. * @throws IOException Signals that an I/O exception has occurred.
* @throws TemplateException the template exception * @throws TemplateException the template exception
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
@SuppressWarnings("PMD.AvoidDuplicateLiterals") @SuppressWarnings({ "unchecked" })
public void reconcile(VmDefChanged event, Map<String, Object> model, public void reconcile(VmDefinition vmDef, Map<String, Object> model,
VmChannel channel) VmChannel channel, boolean specChanged)
throws IOException, TemplateException, ApiException { throws IOException, TemplateException, ApiException {
var vmDef = event.vmDefinition(); Set<String> knownPvcs;
if (!specChanged && channel.associated(this, Set.class).isPresent()) {
// Existing disks knownPvcs = (Set<String>) channel.associated(this, Set.class).get();
ListOptions listOpts = new ListOptions(); } else {
listOpts.setLabelSelector( ListOptions listOpts = new ListOptions();
"app.kubernetes.io/managed-by=" + VM_OP_NAME + "," listOpts.setLabelSelector(
+ "app.kubernetes.io/name=" + APP_NAME + "," "app.kubernetes.io/managed-by=" + VM_OP_NAME + ","
+ "app.kubernetes.io/instance=" + vmDef.name()); + "app.kubernetes.io/name=" + APP_NAME + ","
var knownDisks = K8sV1PvcStub.list(channel.client(), + "app.kubernetes.io/instance=" + vmDef.name());
vmDef.namespace(), listOpts); knownPvcs = K8sV1PvcStub.list(channel.client(),
var knownPvcs = knownDisks.stream().map(K8sV1PvcStub::name) vmDef.namespace(), listOpts).stream().map(K8sV1PvcStub::name)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
channel.setAssociated(this, knownPvcs);
}
// Reconcile runner data pvc // Reconcile runner data pvc
reconcileRunnerDataPvc(event, model, channel, knownPvcs); reconcileRunnerDataPvc(vmDef, model, channel, knownPvcs, specChanged);
// Reconcile pvcs for defined disks // Reconcile pvcs for defined disks
var diskDefs = vmDef.<List<Map<String, Object>>> fromVm("disks") var diskDefs = vmDef.<List<Map<String, Object>>> fromVm("disks")
@ -116,18 +118,15 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
} }
// Update PVC // Update PVC
model.put("disk", diskDef); reconcileRunnerDiskPvc(vmDef, model, channel, specChanged, diskDef);
reconcileRunnerDiskPvc(event, model, channel);
} }
model.remove("disk");
} }
private void reconcileRunnerDataPvc(VmDefChanged event, private void reconcileRunnerDataPvc(VmDefinition vmDef,
Map<String, Object> model, VmChannel channel, Map<String, Object> model, VmChannel channel,
Set<String> knownPvcs) Set<String> knownPvcs, boolean specChanged)
throws TemplateNotFoundException, MalformedTemplateNameException, throws TemplateNotFoundException, MalformedTemplateNameException,
ParseException, IOException, TemplateException, ApiException { ParseException, IOException, TemplateException, ApiException {
var vmDef = event.vmDefinition();
// Look for old (sts generated) name. // Look for old (sts generated) name.
var stsRunnerDataPvcName var stsRunnerDataPvcName
@ -138,7 +137,13 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
} }
// Generate PVC // 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"); var fmTemplate = fmConfig.getTemplate("runnerDataPvc.ftl.yaml");
StringWriter out = new StringWriter(); StringWriter out = new StringWriter();
fmTemplate.process(model, out); fmTemplate.process(model, out);
@ -161,20 +166,26 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
} }
} }
private void reconcileRunnerDiskPvc(VmDefChanged event, private void reconcileRunnerDiskPvc(VmDefinition vmDef,
Map<String, Object> model, VmChannel channel) Map<String, Object> model, VmChannel channel, boolean specChanged,
Map<String, Object> diskDef)
throws TemplateNotFoundException, MalformedTemplateNameException, throws TemplateNotFoundException, MalformedTemplateNameException,
ParseException, IOException, TemplateException, ApiException { ParseException, IOException, TemplateException, ApiException {
var vmDef = event.vmDefinition();
// Generate PVC // Generate PVC
@SuppressWarnings("unchecked")
var diskDef = (Map<String, Object>) model.get("disk");
var pvcName = vmDef.name() + "-" + diskDef.get("generatedDiskName"); var pvcName = vmDef.name() + "-" + diskDef.get("generatedDiskName");
diskDef.put("generatedPvcName", pvcName); 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"); var fmTemplate = fmConfig.getTemplate("runnerDiskPvc.ftl.yaml");
StringWriter out = new StringWriter(); StringWriter out = new StringWriter();
fmTemplate.process(model, out); fmTemplate.process(model, out);
model.remove("disk");
// Avoid Yaml.load due to // Avoid Yaml.load due to
// https://github.com/kubernetes-client/java/issues/2741 // https://github.com/kubernetes-client/java/issues/2741
var pvcDef = Dynamics.newFromYaml( var pvcDef = Dynamics.newFromYaml(

View file

@ -30,7 +30,6 @@ import freemarker.template.TemplateMethodModelEx;
import freemarker.template.TemplateModelException; import freemarker.template.TemplateModelException;
import io.kubernetes.client.custom.Quantity; import io.kubernetes.client.custom.Quantity;
import io.kubernetes.client.openapi.ApiException; import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.util.generic.options.ListOptions;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.math.BigDecimal; import java.math.BigDecimal;
@ -43,19 +42,15 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.logging.Level; 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.Convertions;
import org.jdrupes.vmoperator.common.K8sClient;
import org.jdrupes.vmoperator.common.K8sObserver; import org.jdrupes.vmoperator.common.K8sObserver;
import org.jdrupes.vmoperator.common.K8sV1SecretStub;
import org.jdrupes.vmoperator.common.VmDefinition; import org.jdrupes.vmoperator.common.VmDefinition;
import org.jdrupes.vmoperator.common.VmDefinition.Assignment; import org.jdrupes.vmoperator.common.VmDefinition.Assignment;
import org.jdrupes.vmoperator.common.VmPool; import org.jdrupes.vmoperator.common.VmPool;
import org.jdrupes.vmoperator.manager.events.GetPools; import org.jdrupes.vmoperator.manager.events.GetPools;
import org.jdrupes.vmoperator.manager.events.ResetVm; import org.jdrupes.vmoperator.manager.events.ResetVm;
import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged; import org.jdrupes.vmoperator.manager.events.VmResourceChanged;
import org.jdrupes.vmoperator.util.ExtendedObjectWrapper; import org.jdrupes.vmoperator.util.ExtendedObjectWrapper;
import org.jgrapes.core.Channel; import org.jgrapes.core.Channel;
import org.jgrapes.core.Component; import org.jgrapes.core.Component;
@ -142,19 +137,16 @@ import org.jgrapes.util.events.ConfigurationUpdate;
* *
* @see org.jdrupes.vmoperator.manager.DisplaySecretReconciler * @see org.jdrupes.vmoperator.manager.DisplaySecretReconciler
*/ */
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", @SuppressWarnings({ "PMD.AvoidDuplicateLiterals" })
"PMD.AvoidDuplicateLiterals" })
public class Reconciler extends Component { public class Reconciler extends Component {
/** The Constant mapper. */ /** The Constant mapper. */
@SuppressWarnings("PMD.FieldNamingConventions") @SuppressWarnings("PMD.FieldNamingConventions")
protected static final ObjectMapper mapper = new ObjectMapper(); protected static final ObjectMapper mapper = new ObjectMapper();
@SuppressWarnings("PMD.SingularField")
private final Configuration fmConfig; private final Configuration fmConfig;
private final ConfigMapReconciler cmReconciler; private final ConfigMapReconciler cmReconciler;
private final DisplaySecretReconciler dsReconciler; private final DisplaySecretReconciler dsReconciler;
private final StatefulSetReconciler stsReconciler;
private final PvcReconciler pvcReconciler; private final PvcReconciler pvcReconciler;
private final PodReconciler podReconciler; private final PodReconciler podReconciler;
private final LoadBalancerReconciler lbReconciler; private final LoadBalancerReconciler lbReconciler;
@ -182,7 +174,6 @@ public class Reconciler extends Component {
cmReconciler = new ConfigMapReconciler(fmConfig); cmReconciler = new ConfigMapReconciler(fmConfig);
dsReconciler = attach(new DisplaySecretReconciler(componentChannel)); dsReconciler = attach(new DisplaySecretReconciler(componentChannel));
stsReconciler = new StatefulSetReconciler(fmConfig);
pvcReconciler = new PvcReconciler(fmConfig); pvcReconciler = new PvcReconciler(fmConfig);
podReconciler = new PodReconciler(fmConfig); podReconciler = new PodReconciler(fmConfig);
lbReconciler = new LoadBalancerReconciler(fmConfig); lbReconciler = new LoadBalancerReconciler(fmConfig);
@ -210,8 +201,7 @@ public class Reconciler extends Component {
* @throws IOException Signals that an I/O exception has occurred. * @throws IOException Signals that an I/O exception has occurred.
*/ */
@Handler @Handler
@SuppressWarnings("PMD.ConfusingTernary") public void onVmResourceChanged(VmResourceChanged event, VmChannel channel)
public void onVmDefChanged(VmDefChanged event, VmChannel channel)
throws ApiException, TemplateException, IOException { throws ApiException, TemplateException, IOException {
// Ownership relationships takes care of deletions // Ownership relationships takes care of deletions
if (event.type() == K8sObserver.ResponseType.DELETED) { if (event.type() == K8sObserver.ResponseType.DELETED) {
@ -219,20 +209,19 @@ public class Reconciler extends Component {
} }
// Create model for processing templates // Create model for processing templates
Map<String, Object> model var vmDef = event.vmDefinition();
= prepareModel(channel.client(), event.vmDefinition()); Map<String, Object> model = prepareModel(vmDef);
cmReconciler.reconcile(model, channel); cmReconciler.reconcile(model, channel, event.specChanged());
// The remaining reconcilers depend only on changes of the spec part. // The remaining reconcilers depend only on changes of the spec part
if (!event.specChanged()) { // or the pod state.
if (!event.specChanged() && !event.podChanged()) {
return; return;
} }
dsReconciler.reconcile(event, model, channel); dsReconciler.reconcile(vmDef, model, channel, event.specChanged());
// Manage (eventual) removal of stateful set. pvcReconciler.reconcile(vmDef, model, channel, event.specChanged());
stsReconciler.reconcile(event, model, channel); podReconciler.reconcile(vmDef, model, channel, event.specChanged());
pvcReconciler.reconcile(event, model, channel); lbReconciler.reconcile(vmDef, model, channel, event.specChanged());
podReconciler.reconcile(event, model, channel);
lbReconciler.reconcile(event, model, channel);
} }
/** /**
@ -249,15 +238,15 @@ public class Reconciler extends Component {
public void onResetVm(ResetVm event, VmChannel channel) public void onResetVm(ResetVm event, VmChannel channel)
throws ApiException, IOException, TemplateException { throws ApiException, IOException, TemplateException {
var vmDef = channel.vmDefinition(); 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 Map<String, Object> model
= prepareModel(channel.client(), channel.vmDefinition()); = prepareModel(channel.vmDefinition());
cmReconciler.reconcile(model, channel); cmReconciler.reconcile(model, channel, true);
} }
@SuppressWarnings({ "PMD.CognitiveComplexity", "PMD.NPathComplexity" }) private Map<String, Object> prepareModel(VmDefinition vmDef)
private Map<String, Object> prepareModel(K8sClient client, throws TemplateModelException, ApiException {
VmDefinition vmDef) throws TemplateModelException, ApiException {
@SuppressWarnings("PMD.UseConcurrentHashMap") @SuppressWarnings("PMD.UseConcurrentHashMap")
Map<String, Object> model = new HashMap<>(); Map<String, Object> model = new HashMap<>();
model.put("managerVersion", model.put("managerVersion",
@ -267,7 +256,6 @@ public class Reconciler extends Component {
model.put("reconciler", config); model.put("reconciler", config);
model.put("constants", constantsMap(Constants.class)); model.put("constants", constantsMap(Constants.class));
addLoginRequestedFor(model, vmDef); addLoginRequestedFor(model, vmDef);
addDisplaySecret(client, model, vmDef);
// Methods // Methods
model.put("parseQuantity", parseQuantityModel); model.put("parseQuantity", parseQuantityModel);
@ -325,21 +313,6 @@ public class Reconciler extends Component {
.ifPresent(u -> model.put("loginRequestedFor", u)); .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 private final TemplateMethodModelEx parseQuantityModel
= new TemplateMethodModelEx() { = new TemplateMethodModelEx() {
@Override @Override
@ -362,7 +335,6 @@ public class Reconciler extends Component {
private final TemplateMethodModelEx formatMemoryModel private final TemplateMethodModelEx formatMemoryModel
= new TemplateMethodModelEx() { = new TemplateMethodModelEx() {
@Override @Override
@SuppressWarnings("PMD.PreserveStackTrace")
public Object exec(@SuppressWarnings("rawtypes") List arguments) public Object exec(@SuppressWarnings("rawtypes") List arguments)
throws TemplateModelException { throws TemplateModelException {
var arg = arguments.get(0); var arg = arguments.get(0);
@ -392,8 +364,7 @@ public class Reconciler extends Component {
private final TemplateMethodModelEx imgageLocationModel private final TemplateMethodModelEx imgageLocationModel
= new TemplateMethodModelEx() { = new TemplateMethodModelEx() {
@Override @Override
@SuppressWarnings({ "PMD.PreserveStackTrace", @SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition" })
"PMD.AvoidLiteralsInIfCondition" })
public Object exec(@SuppressWarnings("rawtypes") List arguments) public Object exec(@SuppressWarnings("rawtypes") List arguments)
throws TemplateModelException { throws TemplateModelException {
var image = ((SimpleScalar) arguments.get(0)).getAsString(); var image = ((SimpleScalar) arguments.get(0)).getAsString();
@ -418,7 +389,6 @@ public class Reconciler extends Component {
private final TemplateMethodModelEx toJsonModel private final TemplateMethodModelEx toJsonModel
= new TemplateMethodModelEx() { = new TemplateMethodModelEx() {
@Override @Override
@SuppressWarnings("PMD.PreserveStackTrace")
public Object exec(@SuppressWarnings("rawtypes") List arguments) public Object exec(@SuppressWarnings("rawtypes") List arguments)
throws TemplateModelException { throws TemplateModelException {
try { 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; 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.ApiException;
import io.kubernetes.client.openapi.models.V1ObjectMeta;
import io.kubernetes.client.util.Watch; import io.kubernetes.client.util.Watch;
import io.kubernetes.client.util.generic.options.ListOptions; import io.kubernetes.client.util.generic.options.ListOptions;
import java.io.IOException; import java.io.IOException;
import java.net.HttpURLConnection;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Collections;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.jdrupes.vmoperator.common.Constants.Crd; 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.K8s;
import org.jdrupes.vmoperator.common.K8sClient; import org.jdrupes.vmoperator.common.K8sClient;
import org.jdrupes.vmoperator.common.K8sDynamicStub; import org.jdrupes.vmoperator.common.K8sDynamicStub;
import org.jdrupes.vmoperator.common.K8sObserver.ResponseType; import org.jdrupes.vmoperator.common.K8sObserver.ResponseType;
import org.jdrupes.vmoperator.common.K8sV1ConfigMapStub; import org.jdrupes.vmoperator.common.K8sV1ConfigMapStub;
import org.jdrupes.vmoperator.common.K8sV1PodStub;
import org.jdrupes.vmoperator.common.K8sV1StatefulSetStub; import org.jdrupes.vmoperator.common.K8sV1StatefulSetStub;
import org.jdrupes.vmoperator.common.VmDefinition; import org.jdrupes.vmoperator.common.VmDefinition;
import org.jdrupes.vmoperator.common.VmDefinition.Assignment;
import org.jdrupes.vmoperator.common.VmDefinitionStub; import org.jdrupes.vmoperator.common.VmDefinitionStub;
import org.jdrupes.vmoperator.common.VmDefinitions; import org.jdrupes.vmoperator.common.VmDefinitions;
import org.jdrupes.vmoperator.common.VmExtraData; 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.APP_NAME;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME; import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME;
import org.jdrupes.vmoperator.manager.events.AssignVm;
import org.jdrupes.vmoperator.manager.events.ChannelManager; 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.ModifyVm;
import org.jdrupes.vmoperator.manager.events.PodChanged;
import org.jdrupes.vmoperator.manager.events.UpdateAssignment; import org.jdrupes.vmoperator.manager.events.UpdateAssignment;
import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged; import org.jdrupes.vmoperator.manager.events.VmResourceChanged;
import org.jdrupes.vmoperator.util.GsonPtr;
import org.jgrapes.core.Channel; import org.jgrapes.core.Channel;
import org.jgrapes.core.Event; import org.jgrapes.core.Event;
import org.jgrapes.core.EventPipeline;
import org.jgrapes.core.annotation.Handler; 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 public class VmMonitor extends
AbstractMonitor<VmDefinition, VmDefinitions, VmChannel> { AbstractMonitor<VmDefinition, VmDefinitions, VmChannel> {
private final ChannelManager<String, VmChannel, ?> channelManager; private final ChannelManager<String, VmChannel,
EventPipeline> channelManager;
/** /**
* Instantiates a new VM definition watcher. * Instantiates a new VM definition watcher.
@ -76,7 +84,7 @@ public class VmMonitor extends
* @param channelManager the channel manager * @param channelManager the channel manager
*/ */
public VmMonitor(Channel componentChannel, public VmMonitor(Channel componentChannel,
ChannelManager<String, VmChannel, ?> channelManager) { ChannelManager<String, VmChannel, EventPipeline> channelManager) {
super(componentChannel, VmDefinition.class, super(componentChannel, VmDefinition.class,
VmDefinitions.class); VmDefinitions.class);
this.channelManager = channelManager; this.channelManager = channelManager;
@ -98,7 +106,6 @@ public class VmMonitor extends
purge(); purge();
} }
@SuppressWarnings("PMD.CognitiveComplexity")
private void purge() throws ApiException { private void purge() throws ApiException {
// Get existing CRs (VMs) // Get existing CRs (VMs)
var known = K8sDynamicStub.list(client(), context(), namespace()) var known = K8sDynamicStub.list(client(), context(), namespace())
@ -123,14 +130,18 @@ public class VmMonitor extends
@Override @Override
protected void handleChange(K8sClient client, protected void handleChange(K8sClient client,
Watch.Response<VmDefinition> response) { Watch.Response<VmDefinition> response) {
V1ObjectMeta metadata = response.object.getMetadata(); var name = response.object.getMetadata().getName();
AtomicBoolean toBeAdded = new AtomicBoolean(false);
VmChannel channel = channelManager.channel(metadata.getName())
.orElseGet(() -> {
toBeAdded.set(true);
return channelManager.createChannel(metadata.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 // Get full definition and associate with channel as backup
var vmDef = response.object; var vmDef = response.object;
if (vmDef.data() == null) { if (vmDef.data() == null) {
@ -138,9 +149,12 @@ public class VmMonitor extends
// https://github.com/kubernetes-client/java/issues/3215 // https://github.com/kubernetes-client/java/issues/3215
vmDef = getModel(client, vmDef); vmDef = getModel(client, vmDef);
} }
var name = response.object.getMetadata().getName();
var channel = channelManager.channel(name)
.orElseGet(() -> channelManager.createChannel(name));
if (vmDef.data() != null) { if (vmDef.data() != null) {
// New data, augment and save // New data, augment and save
addExtraData(channel.client(), vmDef, channel.vmDefinition()); addExtraData(vmDef, channel.vmDefinition());
channel.setVmDefinition(vmDef); channel.setVmDefinition(vmDef);
} else { } else {
// Reuse cached (e.g. if deleted) // Reuse cached (e.g. if deleted)
@ -151,22 +165,20 @@ public class VmMonitor extends
+ response.object.getMetadata()); + response.object.getMetadata());
return; return;
} }
if (toBeAdded.get()) { channelManager.put(name, channel, preparing);
channelManager.put(vmDef.name(), channel);
}
// Create and fire changed event. Remove channel from channel // Create and fire changed event. Remove channel from channel
// manager on completion. // manager on completion.
VmDefChanged chgEvt VmResourceChanged chgEvt
= new VmDefChanged(ResponseType.valueOf(response.type), = new VmResourceChanged(ResponseType.valueOf(response.type), vmDef,
channel.setGeneration(response.object.getMetadata() channel.setGeneration(response.object.getMetadata()
.getGeneration()), .getGeneration()),
vmDef); false);
if (ResponseType.valueOf(response.type) == ResponseType.DELETED) { if (ResponseType.valueOf(response.type) == ResponseType.DELETED) {
chgEvt = Event.onCompletion(chgEvt, chgEvt = Event.onCompletion(chgEvt,
e -> channelManager.remove(e.vmDefinition().name())); e -> channelManager.remove(e.vmDefinition().name()));
} }
channel.pipeline().fire(chgEvt, channel); channel.fire(chgEvt);
} }
private VmDefinition getModel(K8sClient client, VmDefinition vmDef) { private VmDefinition getModel(K8sClient client, VmDefinition vmDef) {
@ -178,147 +190,137 @@ public class VmMonitor extends
} }
} }
@SuppressWarnings("PMD.AvoidDuplicateLiterals") private void addExtraData(VmDefinition vmDef, VmDefinition prevState) {
private void addExtraData(K8sClient client, VmDefinition vmDef,
VmDefinition prevState) {
var extra = new VmExtraData(vmDef); var extra = new VmExtraData(vmDef);
var prevExtra = Optional.ofNullable(prevState).map(VmDefinition::extra);
// Maintain (or initialize) the resetCount // Maintain (or initialize) the resetCount
extra.resetCount( extra.resetCount(prevExtra.map(VmExtraData::resetCount).orElse(0L));
Optional.ofNullable(prevState).flatMap(VmDefinition::extra)
.map(VmExtraData::resetCount).orElse(0L));
// VM definition status changes before the pod terminates. // Maintain node info
// This results in pod information being shown for a stopped prevExtra
// VM which is irritating. So check condition first. .ifPresent(e -> extra.nodeInfo(e.nodeName(), e.nodeAddresses()));
if (!vmDef.conditionStatus("Running").orElse(false)) { }
/**
* 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; return;
} }
// Get pod and extract node information. // Get current node info from pod
var podSearch = new ListOptions(); var pod = event.pod();
podSearch.setLabelSelector("app.kubernetes.io/name=" + APP_NAME var nodeName = Optional
+ ",app.kubernetes.io/component=" + APP_NAME .ofNullable(pod.getSpec().getNodeName()).orElse("");
+ ",app.kubernetes.io/instance=" + vmDef.name()); logger.finer(() -> "Adding node name " + nodeName
try { + " to VM info for " + vmDef.name());
var podList var addrs = new ArrayList<String>();
= K8sV1PodStub.list(client, namespace(), podSearch); Optional.ofNullable(pod.getStatus().getPodIPs())
for (var podStub : podList) { .orElse(Collections.emptyList()).stream()
var nodeName = podStub.model().get().getSpec().getNodeName(); .map(ip -> ip.getIp()).forEach(addrs::add);
logger.finer(() -> "Adding node name " + nodeName logger.finer(() -> "Adding node addresses " + addrs
+ " to VM info for " + vmDef.name()); + " to VM info for " + vmDef.name());
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") extra.nodeInfo(nodeName, addrs);
var addrs = new ArrayList<String>();
podStub.model().get().getStatus().getPodIPs().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. * On modify vm.
*
* @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.
* *
* @param event the event * @param event the event
* @throws ApiException the api exception * @throws ApiException the api exception
* @throws InterruptedException * @throws IOException Signals that an I/O exception has occurred.
*/ */
@Handler @Handler
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") public void onModifyVm(ModifyVm event, VmChannel channel)
public void onAssignVm(AssignVm event) throws ApiException, IOException {
throws ApiException, InterruptedException { patchVmDef(channel.client(), event.name(), "spec/vm/" + event.path(),
while (true) { event.value());
// 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;
}
// Get the pool definition for checking possible assignment private void patchVmDef(K8sClient client, String name, String path,
VmPool vmPool = newEventPipeline().fire(new GetPools() Object value) throws ApiException, IOException {
.withName(event.fromPool())).get().stream().findFirst() var vmStub = K8sDynamicStub.get(client,
.orElse(null); new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM), namespace(),
if (vmPool == null) { name);
return;
}
// Find available VM. // Patch running
vmQuery = channelManager.channels().stream() String valueAsText = value instanceof String
.filter(c -> vmPool.isAssignable(c.vmDefinition())) ? "\"" + value + "\""
.sorted(Comparator.comparing((VmChannel c) -> c.vmDefinition() : value.toString();
.assignment().map(Assignment::lastUsed) var res = vmStub.patch(V1Patch.PATCH_FORMAT_JSON_PATCH,
.orElse(Instant.ofEpochSecond(0))) new V1Patch("[{\"op\": \"replace\", \"path\": \"/"
.thenComparing(preferRunning)) + path + "\", \"value\": " + valueAsText + "}]"),
.findFirst(); client.defaultPatchOptions());
if (!res.isPresent()) {
// None found logger.warning(
if (vmQuery.isEmpty()) { () -> "Cannot patch definition for Vm " + vmStub.name());
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;
}
} }
} }
private static Comparator<VmChannel> preferRunning /**
= new Comparator<>() { * Attempt to Update the assignment information in the status of the
@Override * VM CR. Returns true if successful. The handler does not attempt
public int compare(VmChannel ch1, VmChannel ch2) { * retries, because in case of failure it will be necessary to
if (ch1.vmDefinition().conditionStatus("Running").orElse(false) * re-evaluate the chosen VM.
&& !ch2.vmDefinition().conditionStatus("Running") *
.orElse(false)) { * @param event the event
return -1; * @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 * 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 * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@ -83,8 +83,18 @@
* [YamlConfigurationStore] *-right[hidden]- [Controller] * [YamlConfigurationStore] *-right[hidden]- [Controller]
* *
* [Manager] *-- [Controller] * [Manager] *-- [Controller]
* [Controller] *-- [VmWatcher] * Component VmMonitor as VmMonitor <<internal>>
* [Controller] *-- [Reconciler] * [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] * [Controller] -right[hidden]- [GuiHttpServer]
* *
* [Manager] *-down- [GuiSocketServer:8080] * [Manager] *-down- [GuiSocketServer:8080]

View file

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

View file

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

View file

@ -39,11 +39,9 @@ import org.jdrupes.vmoperator.util.FsdUtils;
/** /**
* The configuration information from the configuration file. * The configuration information from the configuration file.
*/ */
@SuppressWarnings({ "PMD.ExcessivePublicCount", "PMD.TooManyFields" })
public class Configuration implements Dto { public class Configuration implements Dto {
private static final String CI_INSTANCE_ID = "instance-id"; private static final String CI_INSTANCE_ID = "instance-id";
@SuppressWarnings("PMD.FieldNamingConventions")
protected final Logger logger = Logger.getLogger(getClass().getName()); protected final Logger logger = Logger.getLogger(getClass().getName());
/** Configuration timestamp. */ /** Configuration timestamp. */
@ -95,15 +93,12 @@ public class Configuration implements Dto {
public static class CloudInit implements Dto { public static class CloudInit implements Dto {
/** The meta data. */ /** The meta data. */
@SuppressWarnings("PMD.UseConcurrentHashMap")
public Map<String, Object> metaData; public Map<String, Object> metaData;
/** The user data. */ /** The user data. */
@SuppressWarnings("PMD.UseConcurrentHashMap")
public Map<String, Object> userData; public Map<String, Object> userData;
/** The network config. */ /** The network config. */
@SuppressWarnings("PMD.UseConcurrentHashMap")
public Map<String, Object> networkConfig; public Map<String, Object> networkConfig;
} }
@ -299,7 +294,6 @@ public class Configuration implements Dto {
return true; return true;
} }
@SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
private void checkDrives() { private void checkDrives() {
for (Drive drive : vm.drives) { for (Drive drive : vm.drives) {
if (drive.file != null || drive.device != null if (drive.file != null || drive.device != null
@ -319,7 +313,6 @@ public class Configuration implements Dto {
} }
} }
@SuppressWarnings("PMD.AvoidDeeplyNestedIfStmts")
private boolean checkRuntimeDir() { private boolean checkRuntimeDir() {
// Runtime directory (sockets etc.) // Runtime directory (sockets etc.)
if (runtimeDir == null) { if (runtimeDir == null) {
@ -355,7 +348,6 @@ public class Configuration implements Dto {
return true; return true;
} }
@SuppressWarnings("PMD.AvoidDeeplyNestedIfStmts")
private boolean checkDataDir() { private boolean checkDataDir() {
// Data directory // Data directory
if (dataDir == null) { 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. * A (sub)component that updates the console status in the CR status.
* Created as child of {@link StatusUpdater}. * Created as child of {@link StatusUpdater}.
*/ */
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class ConsoleTracker extends VmDefUpdater { public class ConsoleTracker extends VmDefUpdater {
private VmDefinitionStub vmStub; private VmDefinitionStub vmStub;
@ -53,7 +52,6 @@ public class ConsoleTracker extends VmDefUpdater {
* *
* @param componentChannel the component channel * @param componentChannel the component channel
*/ */
@SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
public ConsoleTracker(Channel componentChannel) { public ConsoleTracker(Channel componentChannel) {
super(componentChannel); super(componentChannel);
apiClient = (K8sClient) io.kubernetes.client.openapi.Configuration apiClient = (K8sClient) io.kubernetes.client.openapi.Configuration
@ -91,8 +89,7 @@ public class ConsoleTracker extends VmDefUpdater {
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
@Handler @Handler
@SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition", @SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition" })
"PMD.AvoidDuplicateLiterals" })
public void onSpiceInitialized(SpiceInitializedEvent event) public void onSpiceInitialized(SpiceInitializedEvent event)
throws ApiException { throws ApiException {
if (vmStub == null) { if (vmStub == null) {
@ -127,7 +124,6 @@ public class ConsoleTracker extends VmDefUpdater {
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
@Handler @Handler
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
public void onSpiceDisconnected(SpiceDisconnectedEvent event) public void onSpiceDisconnected(SpiceDisconnectedEvent event)
throws ApiException { throws ApiException {
if (vmStub == null) { if (vmStub == null) {

View file

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

View file

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

View file

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

View file

@ -69,14 +69,14 @@ public class GuestAgentClient extends AgentConnector {
*/ */
@Override @Override
protected void agentConnected() { protected void agentConnected() {
logger.fine(() -> "guest agent connected"); logger.fine(() -> "Guest agent connected");
connected = true; connected = true;
rep().fire(new GuestAgentCommand(new QmpGuestGetOsinfo())); rep().fire(new GuestAgentCommand(new QmpGuestGetOsinfo()));
} }
@Override @Override
protected void agentDisconnected() { protected void agentDisconnected() {
logger.fine(() -> "guest agent disconnected"); logger.fine(() -> "Guest agent disconnected");
connected = false; connected = false;
} }
@ -88,15 +88,16 @@ public class GuestAgentClient extends AgentConnector {
*/ */
@Override @Override
protected void processInput(String line) throws IOException { protected void processInput(String line) throws IOException {
logger.fine(() -> "guest agent(in): " + line); logger.finer(() -> "guest agent(in): " + line);
try { try {
var response = mapper.readValue(line, ObjectNode.class); var response = mapper.readValue(line, ObjectNode.class);
if (response.has("return") || response.has("error")) { if (response.has("return") || response.has("error")) {
QmpCommand executed = executing.poll(); 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)); + " is result from executing %s)", executed));
if (executed instanceof QmpGuestGetOsinfo) { if (executed instanceof QmpGuestGetOsinfo) {
var osInfo = new OsinfoEvent(response.get("return")); var osInfo = new OsinfoEvent(response.get("return"));
logger.fine(() -> "Guest agent triggers: " + osInfo);
rep().fire(osInfo); rep().fire(osInfo);
} }
} }
@ -120,10 +121,11 @@ public class GuestAgentClient extends AgentConnector {
return; return;
} }
var command = event.command(); var command = event.command();
logger.fine(() -> "guest agent(out): " + command.toString()); logger.fine(() -> "Guest handles: " + event);
String asText; String asText;
try { try {
asText = command.asText(); asText = command.asText();
logger.finer(() -> "guest agent(out): " + asText);
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
logger.log(Level.SEVERE, e, logger.log(Level.SEVERE, e,
() -> "Cannot serialize Json: " + e.getMessage()); () -> "Cannot serialize Json: " + e.getMessage());
@ -163,8 +165,8 @@ public class GuestAgentClient extends AgentConnector {
} }
event.suspendHandling(); event.suspendHandling();
suspendedStop = event; suspendedStop = event;
logger.fine(() -> "Sending powerdown command, waiting for" logger.fine(() -> "Attempting shutdown through guest agent,"
+ " termination until " + waitUntil); + " waiting for termination until " + waitUntil);
powerdownTimer = Components.schedule(t -> { powerdownTimer = Components.schedule(t -> {
logger.fine(() -> "Powerdown timeout reached."); logger.fine(() -> "Powerdown timeout reached.");
synchronized (this) { 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 * If the log level for this class is set to fine, the messages
* exchanged on the monitor socket are logged. * exchanged on the monitor socket are logged.
*/ */
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class QemuMonitor extends QemuConnector { public class QemuMonitor extends QemuConnector {
private int powerdownTimeout; private int powerdownTimeout;
@ -72,8 +71,6 @@ public class QemuMonitor extends QemuConnector {
* @param configDir the config dir * @param configDir the config dir
* @throws IOException Signals that an I/O exception has occurred. * @throws IOException Signals that an I/O exception has occurred.
*/ */
@SuppressWarnings({ "PMD.AssignmentToNonFinalStatic",
"PMD.ConstructorCallsOverridableMethod" })
public QemuMonitor(Channel componentChannel, Path configDir) public QemuMonitor(Channel componentChannel, Path configDir)
throws IOException { throws IOException {
super(componentChannel); super(componentChannel);
@ -108,24 +105,30 @@ public class QemuMonitor extends QemuConnector {
@Override @Override
protected void processInput(String line) protected void processInput(String line)
throws IOException { throws IOException {
logger.fine(() -> "monitor(in): " + line); logger.finer(() -> "monitor(in): " + line);
try { try {
var response = mapper.readValue(line, ObjectNode.class); var response = mapper.readValue(line, ObjectNode.class);
if (response.has("QMP")) { if (response.has("QMP")) {
monitorReady = true; monitorReady = true;
logger.fine(() -> "QMP connection ready");
rep().fire(new MonitorReady()); rep().fire(new MonitorReady());
return; return;
} }
if (response.has("return") || response.has("error")) { if (response.has("return") || response.has("error")) {
QmpCommand executed = executing.poll(); QmpCommand executed = executing.poll();
logger.fine( logger.finer(
() -> String.format("(Previous \"monitor(in)\" is result " () -> String.format("(Previous \"monitor(in)\" is result "
+ "from executing %s)", executed)); + "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; return;
} }
if (response.has("event")) { 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) { } catch (JsonProcessingException e) {
throw new IOException(e); throw new IOException(e);
@ -141,7 +144,7 @@ public class QemuMonitor extends QemuConnector {
public void onClosed(Closed<?> event, SocketIOChannel channel) { public void onClosed(Closed<?> event, SocketIOChannel channel) {
channel.associated(this, getClass()).ifPresent(qm -> { channel.associated(this, getClass()).ifPresent(qm -> {
super.onClosed(event, channel); super.onClosed(event, channel);
logger.finer(() -> "QMP socket closed."); logger.fine(() -> "QMP connection closed.");
monitorReady = false; monitorReady = false;
}); });
} }
@ -158,7 +161,7 @@ public class QemuMonitor extends QemuConnector {
public void onMonitorCommand(MonitorCommand event) throws IOException { public void onMonitorCommand(MonitorCommand event) throws IOException {
// Check prerequisites // Check prerequisites
if (!monitorReady && !(event.command() instanceof QmpCapabilities)) { if (!monitorReady && !(event.command() instanceof QmpCapabilities)) {
logger.severe(() -> "Premature monitor command (not ready): " logger.severe(() -> "Premature QMP command (not ready): "
+ event.command()); + event.command());
rep().fire(new Stop()); rep().fire(new Stop());
return; return;
@ -166,10 +169,11 @@ public class QemuMonitor extends QemuConnector {
// Send the command // Send the command
var command = event.command(); var command = event.command();
logger.fine(() -> "monitor(out): " + command.toString()); logger.fine(() -> "QMP handles: " + event.toString());
String asText; String asText;
try { try {
asText = command.asText(); asText = command.asText();
logger.finer(() -> "monitor(out): " + asText);
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
logger.log(Level.SEVERE, e, logger.log(Level.SEVERE, e,
() -> "Cannot serialize Json: " + e.getMessage()); () -> "Cannot serialize Json: " + e.getMessage());
@ -192,8 +196,8 @@ public class QemuMonitor extends QemuConnector {
@SuppressWarnings("PMD.AvoidSynchronizedStatement") @SuppressWarnings("PMD.AvoidSynchronizedStatement")
public void onStop(Stop event) { public void onStop(Stop event) {
if (!monitorReady) { if (!monitorReady) {
logger.fine(() -> "No QMP connection," logger.fine(() -> "Not sending QMP powerdown command"
+ " cannot send powerdown command"); + " because QMP connection is closed");
return; return;
} }

View file

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

View file

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

View file

@ -31,6 +31,8 @@ import io.kubernetes.client.openapi.JSON;
import io.kubernetes.client.openapi.models.EventsV1Event; import io.kubernetes.client.openapi.models.EventsV1Event;
import java.io.IOException; import java.io.IOException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.Instant;
import java.util.Optional; import java.util.Optional;
import java.util.logging.Level; import java.util.logging.Level;
import static org.jdrupes.vmoperator.common.Constants.APP_NAME; 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.runner.qemu.events.VmopAgentLoggedOut;
import org.jdrupes.vmoperator.util.GsonPtr; import org.jdrupes.vmoperator.util.GsonPtr;
import org.jgrapes.core.Channel; import org.jgrapes.core.Channel;
import org.jgrapes.core.Components;
import org.jgrapes.core.Components.Timer;
import org.jgrapes.core.annotation.Handler; import org.jgrapes.core.annotation.Handler;
import org.jgrapes.core.events.HandlingError; import org.jgrapes.core.events.HandlingError;
import org.jgrapes.core.events.Start; import org.jgrapes.core.events.Start;
@ -62,7 +66,7 @@ import org.jgrapes.core.events.Start;
/** /**
* Updates the CR status. * Updates the CR status.
*/ */
@SuppressWarnings("PMD.DataflowAnomalyAnalysis") @SuppressWarnings({ "PMD.CouplingBetweenObjects" })
public class StatusUpdater extends VmDefUpdater { public class StatusUpdater extends VmDefUpdater {
@SuppressWarnings("PMD.FieldNamingConventions") @SuppressWarnings("PMD.FieldNamingConventions")
@ -71,18 +75,20 @@ public class StatusUpdater extends VmDefUpdater {
private static final ObjectMapper objectMapper private static final ObjectMapper objectMapper
= new ObjectMapper().registerModule(new JavaTimeModule()); = new ObjectMapper().registerModule(new JavaTimeModule());
private long observedGeneration;
private boolean guestShutdownStops; private boolean guestShutdownStops;
private boolean shutdownByGuest; private boolean shutdownByGuest;
private VmDefinitionStub vmStub; private VmDefinitionStub vmStub;
private String loggedInUser; private String loggedInUser;
private BigInteger lastRamValue;
private Instant lastRamChange;
private Timer balloonTimer;
private BigInteger targetRamValue;
/** /**
* Instantiates a new status updater. * Instantiates a new status updater.
* *
* @param componentChannel the component channel * @param componentChannel the component channel
*/ */
@SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
public StatusUpdater(Channel componentChannel) { public StatusUpdater(Channel componentChannel) {
super(componentChannel); super(componentChannel);
attach(new ConsoleTracker(componentChannel)); attach(new ConsoleTracker(componentChannel));
@ -122,7 +128,6 @@ public class StatusUpdater extends VmDefUpdater {
if (vmDef == null) { if (vmDef == null) {
return; return;
} }
observedGeneration = vmDef.getMetadata().getGeneration();
vmStub.updateStatus(from -> { vmStub.updateStatus(from -> {
JsonObject status = from.statusJson(); JsonObject status = from.statusJson();
status.addProperty(Status.RUNNER_VERSION, Optional.ofNullable( status.addProperty(Status.RUNNER_VERSION, Optional.ofNullable(
@ -146,31 +151,16 @@ public class StatusUpdater extends VmDefUpdater {
* @throws ApiException * @throws ApiException
*/ */
@Handler @Handler
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
public void onConfigureQemu(ConfigureQemu event) public void onConfigureQemu(ConfigureQemu event)
throws ApiException { throws ApiException {
guestShutdownStops = event.configuration().guestShutdownStops; guestShutdownStops = event.configuration().guestShutdownStops;
loggedInUser = event.configuration().vm.display.loggedInUser; loggedInUser = event.configuration().vm.display.loggedInUser;
targetRamValue = event.configuration().vm.currentRam;
// Remainder applies only if we have a connection to k8s. // Remainder applies only if we have a connection to k8s.
if (vmStub == null) { if (vmStub == null) {
return; 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 -> { vmStub.updateStatus(from -> {
JsonObject status = from.statusJson(); JsonObject status = from.statusJson();
if (!event.configuration().hasDisplayPassword) { if (!event.configuration().hasDisplayPassword) {
@ -184,7 +174,7 @@ public class StatusUpdater extends VmDefUpdater {
from.getMetadata().getGeneration())); from.getMetadata().getGeneration()));
updateUserLoggedIn(from); updateUserLoggedIn(from);
return status; return status;
}, vmDef); });
} }
/** /**
@ -194,8 +184,7 @@ public class StatusUpdater extends VmDefUpdater {
* @throws ApiException * @throws ApiException
*/ */
@Handler @Handler
@SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition", @SuppressWarnings({ "PMD.AssignmentInOperand" })
"PMD.AssignmentInOperand", "PMD.AvoidDuplicateLiterals" })
public void onRunnerStateChanged(RunnerStateChange event) public void onRunnerStateChanged(RunnerStateChange event)
throws ApiException { throws ApiException {
VmDefinition vmDef; 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 * @param event the event
* @throws ApiException * @throws ApiException
@ -289,10 +282,45 @@ public class StatusUpdater extends VmDefUpdater {
if (vmStub == null) { if (vmStub == null) {
return; 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 -> { vmStub.updateStatus(from -> {
JsonObject status = from.statusJson(); JsonObject status = from.statusJson();
status.addProperty(Status.RAM, status.addProperty(Status.RAM,
new Quantity(new BigDecimal(event.size()), Format.BINARY_SI) new Quantity(new BigDecimal(lastRamValue), Format.BINARY_SI)
.toSuffixedString()); .toSuffixedString());
return status; return status;
}); });
@ -393,7 +421,6 @@ public class StatusUpdater extends VmDefUpdater {
* @throws ApiException * @throws ApiException
*/ */
@Handler @Handler
@SuppressWarnings("PMD.AssignmentInOperand")
public void onVmopAgentLoggedIn(VmopAgentLoggedIn event) public void onVmopAgentLoggedIn(VmopAgentLoggedIn event)
throws ApiException { throws ApiException {
vmStub.updateStatus(from -> { vmStub.updateStatus(from -> {
@ -410,7 +437,6 @@ public class StatusUpdater extends VmDefUpdater {
* @throws ApiException * @throws ApiException
*/ */
@Handler @Handler
@SuppressWarnings("PMD.AssignmentInOperand")
public void onVmopAgentLoggedOut(VmopAgentLoggedOut event) public void onVmopAgentLoggedOut(VmopAgentLoggedOut event)
throws ApiException { throws ApiException {
vmStub.updateStatus(from -> { vmStub.updateStatus(from -> {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -20,6 +20,8 @@ package org.jdrupes.vmoperator.runner.qemu.events;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import java.util.Optional; import java.util.Optional;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Components;
import org.jgrapes.core.Event; import org.jgrapes.core.Event;
/** /**
@ -47,7 +49,6 @@ public class MonitorEvent extends Event<Void> {
* @param response the response * @param response the response
* @return the optional * @return the optional
*/ */
@SuppressWarnings("PMD.TooFewBranchesForASwitchStatement")
public static Optional<MonitorEvent> from(JsonNode response) { public static Optional<MonitorEvent> from(JsonNode response) {
try { try {
var kind = Kind.valueOf(response.get("event").asText()); var kind = Kind.valueOf(response.get("event").asText());
@ -112,4 +113,20 @@ public class MonitorEvent extends Event<Void> {
public JsonNode data() { public JsonNode data() {
return 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; package org.jdrupes.vmoperator.runner.qemu.events;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Components;
import org.jgrapes.core.Event; import org.jgrapes.core.Event;
/** /**
@ -40,4 +42,21 @@ public class OsinfoEvent extends Event<Void> {
public JsonNode osinfo() { public JsonNode osinfo() {
return 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. * The Class RunnerStateChange.
*/ */
@SuppressWarnings("PMD.DataClass")
public class RunnerStateChange extends Event<Void> { public class RunnerStateChange extends Event<Void> {
/** /**

View file

@ -32,7 +32,6 @@ import java.util.logging.Logger;
*/ */
public final class DataPath { public final class DataPath {
@SuppressWarnings("PMD.FieldNamingConventions")
private static final Logger logger private static final Logger logger
= Logger.getLogger(DataPath.class.getName()); = Logger.getLogger(DataPath.class.getName());
@ -56,7 +55,6 @@ public final class DataPath {
* @param selectors the selectors * @param selectors the selectors
* @return the result * @return the result
*/ */
@SuppressWarnings("PMD.UseLocaleWithCaseConversions")
public static <T> Optional<T> get(Object from, Object... selectors) { public static <T> Optional<T> get(Object from, Object... selectors) {
Object cur = from; Object cur = from;
for (var selector : selectors) { for (var selector : selectors) {
@ -132,7 +130,6 @@ public final class DataPath {
@SuppressWarnings({ "PMD.CognitiveComplexity", "unchecked" }) @SuppressWarnings({ "PMD.CognitiveComplexity", "unchecked" })
public static <T> T deepCopy(T object) { public static <T> T deepCopy(T object) {
if (object instanceof Map map) { if (object instanceof Map map) {
@SuppressWarnings("PMD.UseConcurrentHashMap")
Map<Object, Object> copy; Map<Object, Object> copy;
try { try {
copy = (Map<Object, Object>) object.getClass().getConstructor() 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. * Utility class for pointing to elements on a Gson (Json) tree.
*/ */
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", @SuppressWarnings({ "PMD.ClassWithOnlyPrivateConstructorsShouldBeFinal" })
"PMD.ClassWithOnlyPrivateConstructorsShouldBeFinal", "PMD.GodClass" })
public class GsonPtr { public class GsonPtr {
private final JsonElement position; private final JsonElement position;
@ -102,7 +101,7 @@ public class GsonPtr {
* @param selectors the selectors * @param selectors the selectors
* @return the Gson pointer * @return the Gson pointer
*/ */
@SuppressWarnings({ "PMD.ShortMethodName", "PMD.PreserveStackTrace" }) @SuppressWarnings({ "PMD.PreserveStackTrace" })
public Optional<GsonPtr> get(Object... selectors) { public Optional<GsonPtr> get(Object... selectors) {
JsonElement element = position; JsonElement element = position;
for (Object sel : selectors) { for (Object sel : selectors) {
@ -146,7 +145,6 @@ public class GsonPtr {
* @param cls the cls * @param cls the cls
* @return the result * @return the result
*/ */
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop" })
public <T extends JsonElement> T getAs(Class<T> cls) { public <T extends JsonElement> T getAs(Class<T> cls) {
if (cls.isAssignableFrom(position.getClass())) { if (cls.isAssignableFrom(position.getClass())) {
return cls.cast(position); 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.ModifyVm;
import org.jdrupes.vmoperator.manager.events.ResetVm; import org.jdrupes.vmoperator.manager.events.ResetVm;
import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jdrupes.vmoperator.manager.events.VmPoolChanged; import org.jdrupes.vmoperator.manager.events.VmPoolChanged;
import org.jdrupes.vmoperator.manager.events.VmResourceChanged;
import org.jgrapes.core.Channel; import org.jgrapes.core.Channel;
import org.jgrapes.core.Components; import org.jgrapes.core.Components;
import org.jgrapes.core.Event; import org.jgrapes.core.Event;
@ -111,9 +111,8 @@ import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet;
* users and roles. * users and roles.
* *
*/ */
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports", @SuppressWarnings({ "PMD.ExcessiveImports", "PMD.CouplingBetweenObjects",
"PMD.CouplingBetweenObjects", "PMD.GodClass", "PMD.TooManyMethods", "PMD.GodClass", "PMD.TooManyMethods", "PMD.CyclomaticComplexity" })
"PMD.CyclomaticComplexity" })
public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> { public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
private static final String VM_NAME_PROPERTY = "vmName"; private static final String VM_NAME_PROPERTY = "vmName";
@ -129,6 +128,7 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
private EventPipeline appPipeline; private EventPipeline appPipeline;
private static ObjectMapper objectMapper private static ObjectMapper objectMapper
= new ObjectMapper().registerModule(new JavaTimeModule()); = new ObjectMapper().registerModule(new JavaTimeModule());
private Class<?> preferredIpVersion = Inet4Address.class; private Class<?> preferredIpVersion = Inet4Address.class;
private Set<String> syncUsers = Collections.emptySet(); private Set<String> syncUsers = Collections.emptySet();
private Set<String> syncRoles = Collections.emptySet(); private Set<String> syncRoles = Collections.emptySet();
@ -166,7 +166,7 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
* *
* @param event the event * @param event the event
*/ */
@SuppressWarnings({ "unchecked", "PMD.AvoidDuplicateLiterals" }) @SuppressWarnings({ "unchecked" })
@Handler @Handler
public void onConfigurationUpdate(ConfigurationUpdate event) { public void onConfigurationUpdate(ConfigurationUpdate event) {
event.structured(componentPath()) event.structured(componentPath())
@ -266,7 +266,7 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
public void onConsoleConfigured(ConsoleConfigured event, public void onConsoleConfigured(ConsoleConfigured event,
ConsoleConnection connection) throws InterruptedException, ConsoleConnection connection) throws InterruptedException,
IOException { IOException {
@SuppressWarnings({ "unchecked", "PMD.PrematureDeclaration" }) @SuppressWarnings({ "unchecked" })
final var rendered final var rendered
= (Set<ResourceModel>) connection.session().get(RENDERED); = (Set<ResourceModel>) connection.session().get(RENDERED);
connection.session().remove(RENDERED); connection.session().remove(RENDERED);
@ -276,8 +276,7 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
addMissingConlets(event, connection, rendered); addMissingConlets(event, connection, rendered);
} }
@SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops", @SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops" })
"PMD.AvoidDuplicateLiterals" })
private void addMissingConlets(ConsoleConfigured event, private void addMissingConlets(ConsoleConfigured event,
ConsoleConnection connection, final Set<ResourceModel> rendered) ConsoleConnection connection, final Set<ResourceModel> rendered)
throws InterruptedException { throws InterruptedException {
@ -405,7 +404,6 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
} }
@Override @Override
@SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops" })
protected Set<RenderMode> doRenderConlet(RenderConletRequestBase<?> event, protected Set<RenderMode> doRenderConlet(RenderConletRequestBase<?> event,
ConsoleConnection channel, String conletId, ResourceModel model) ConsoleConnection channel, String conletId, ResourceModel model)
throws Exception { throws Exception {
@ -654,10 +652,9 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
* @throws InterruptedException * @throws InterruptedException
*/ */
@Handler(namedChannels = "manager") @Handler(namedChannels = "manager")
@SuppressWarnings({ "PMD.ConfusingTernary", "PMD.CognitiveComplexity", @SuppressWarnings({ "PMD.CognitiveComplexity",
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.AvoidDuplicateLiterals", "PMD.AvoidInstantiatingObjectsInLoops" })
"PMD.ConfusingArgumentToVarargsMethod" }) public void onVmResourceChanged(VmResourceChanged event, VmChannel channel)
public void onVmDefChanged(VmDefChanged event, VmChannel channel)
throws IOException, InterruptedException { throws IOException, InterruptedException {
var vmDef = event.vmDefinition(); var vmDef = event.vmDefinition();
@ -785,12 +782,12 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
switch (event.method()) { switch (event.method()) {
case "start": case "start":
if (perms.contains(VmDefinition.Permission.START)) { if (perms.contains(VmDefinition.Permission.START)) {
fire(new ModifyVm(vmName, "state", "Running", vmChannel)); vmChannel.fire(new ModifyVm(vmName, "state", "Running"));
} }
break; break;
case "stop": case "stop":
if (perms.contains(VmDefinition.Permission.STOP)) { if (perms.contains(VmDefinition.Permission.STOP)) {
fire(new ModifyVm(vmName, "state", "Stopped", vmChannel)); vmChannel.fire(new ModifyVm(vmName, "state", "Stopped"));
} }
break; break;
case "reset": case "reset":
@ -800,7 +797,7 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
break; break;
case "resetConfirmed": case "resetConfirmed":
if (perms.contains(VmDefinition.Permission.RESET)) { if (perms.contains(VmDefinition.Permission.RESET)) {
fire(new ResetVm(vmName), vmChannel); vmChannel.fire(new ResetVm(vmName));
} }
break; break;
case "openConsole": case "openConsole":
@ -838,7 +835,7 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
} }
var pwQuery = Event.onCompletion(new GetDisplaySecret(vmDef, user), var pwQuery = Event.onCompletion(new GetDisplaySecret(vmDef, user),
e -> gotPassword(channel, model, vmDef, e)); e -> gotPassword(channel, model, vmDef, e));
fire(pwQuery, vmChannel); vmChannel.fire(pwQuery);
} }
private void gotPassword(ConsoleConnection channel, ResourceModel model, private void gotPassword(ConsoleConnection channel, ResourceModel model,
@ -846,14 +843,13 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
if (!event.secretAvailable()) { if (!event.secretAvailable()) {
return; return;
} }
vmDef.extra().map(xtra -> xtra.connectionFile(event.secret(), vmDef.extra().connectionFile(event.secret(),
preferredIpVersion, deleteConnectionFile)) preferredIpVersion, deleteConnectionFile)
.ifPresent(cf -> channel.respond(new NotifyConletView(type(), .ifPresent(cf -> channel.respond(new NotifyConletView(type(),
model.getConletId(), "openConsole", cf))); model.getConletId(), "openConsole", cf)));
} }
@SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition", @SuppressWarnings({ "PMD.UseLocaleWithCaseConversions" })
"PMD.UseLocaleWithCaseConversions" })
private void selectResource(NotifyConletModel event, private void selectResource(NotifyConletModel event,
ConsoleConnection channel, ResourceModel model) ConsoleConnection channel, ResourceModel model)
throws JsonProcessingException, InterruptedException { throws JsonProcessingException, InterruptedException {
@ -880,7 +876,6 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
/** /**
* The Class AccessModel. * The Class AccessModel.
*/ */
@SuppressWarnings("PMD.DataClass")
public static class ResourceModel extends ConletBaseModel { public static class ResourceModel extends ConletBaseModel {
/** /**

View file

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

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