diff --git a/.editorconfig b/.editorconfig
index d8c711a..ad8e2c3 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -5,10 +5,10 @@ charset = utf-8
insert_final_newline = true
trim_trailing_whitespace = true
-[*.{md,yml,yaml}]
+[*.{html,md,yml,yaml}]
indent_size = 2
indent_style = space
-[*.gradle]
+[*.{gradle,js,ts}]
indent_size = 4
indent_style = space
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..943329e
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,16 @@
+{
+ "root": true,
+ "rules": {
+ "no-unused-vars": "off",
+ "@typescript-eslint/no-unused-vars": [
+ "warn",
+ {
+ "argsIgnorePattern": "^_",
+ "varsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_"
+ }
+ ]
+ },
+ "ignorePatterns": ["src/**/*.test.ts", "build/**/*"]
+}
+
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 32e0219..380981e 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -2,7 +2,7 @@ name: "CodeQL"
on:
push:
- branches: [ "main" ]
+ branches: [ "*" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "main" ]
diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml
index 547c1a4..f47366a 100644
--- a/.github/workflows/gradle.yml
+++ b/.github/workflows/gradle.yml
@@ -22,10 +22,10 @@ jobs:
fetch-depth: 0
- name: Install graphviz
run: sudo apt-get install graphviz
- - name: Set up JDK 17
+ - name: Set up JDK 21
uses: actions/setup-java@v3
with:
- java-version: '17'
+ java-version: '21'
distribution: 'temurin'
- name: Build with Gradle
- run: ./gradlew -Prepo.access.token=${{ secrets.REPO_ACCESS_TOKEN }} stage
+ run: ./gradlew -Pwebsite.push.token=${{ secrets.WEBSITE_PUSH_TOKEN }} stage
diff --git a/.github/workflows/jekyll.yml b/.github/workflows/jekyll.yml
new file mode 100644
index 0000000..d0e4ec9
--- /dev/null
+++ b/.github/workflows/jekyll.yml
@@ -0,0 +1,89 @@
+# This workflow uses actions that are not certified by GitHub.
+# They are provided by a third-party and are governed by
+# separate terms of service, privacy policy, and support
+# documentation.
+
+# Sample workflow for building and deploying a Jekyll site to GitHub Pages
+name: Deploy Jekyll site to Pages
+
+on:
+ # Runs on pushes targeting the default branch
+ push:
+ branches: ["main"]
+
+ # Allows you to run this workflow manually from the Actions tab
+ workflow_dispatch:
+
+# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+# Allow only one concurrent deployment, skipping runs queued between
+# the run in-progress and latest queued. However, do NOT cancel
+# in-progress runs as we want to allow these production deployments
+# to complete.
+concurrency:
+ group: "pages"
+ cancel-in-progress: false
+
+jobs:
+ # Build job
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Setup Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: '3.3' # Not needed with a .ruby-version file
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
+ cache-version: 0 # Increment this number if you need to re-download cached gems
+ working-directory: webpages
+ - name: Setup Pages
+ id: pages
+ uses: actions/configure-pages@v5
+ - name: Build with Jekyll
+ # Outputs to the './_site' directory by default
+ run: cd webpages && bundle exec jekyll build
+ env:
+ JEKYLL_ENV: production
+ - name: Install graphviz
+ run: sudo apt-get install graphviz
+ - name: Set up JDK 21
+ uses: actions/setup-java@v3
+ with:
+ java-version: '21'
+ distribution: 'temurin'
+ - name: Build apidocs
+ run: ./gradlew apidocs
+ - name: Copy javadoc
+ run: cp -a build/javadoc webpages/_site/
+ - name: Generate the sitemap
+ uses: cicirello/generate-sitemap@v1
+ with:
+ path-to-root: webpages/_site
+ base-url-path: https://vm-operator.jdrupes.org
+ - name: Index pagefind
+ run: cd webpages && npx pagefind --source "_site"
+ - name: Upload artifact
+ # Automatically uploads an artifact from the './_site' directory by default
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: './webpages/_site'
+
+ # Deployment job
+ deploy:
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ runs-on: ubuntu-latest
+ needs: build
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 3e6b3c9..beab0c4 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -18,10 +18,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
with:
fetch-depth: 0
- ref: main
- name: Install graphviz
run: sudo apt-get install graphviz
- name: Install podman
@@ -32,10 +31,10 @@ jobs:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- - name: Set up JDK 17
+ - name: Set up JDK 21
uses: actions/setup-java@v3
with:
- java-version: '17'
+ java-version: '21'
distribution: 'temurin'
- name: Push with Gradle
- run: ./gradlew -Prepo.access.token=${{ secrets.REPO_ACCESS_TOKEN }} -Pdocker.registry=ghcr.io/${{ github.actor }} stage pushImages
+ run: ./gradlew -Pwebsite.push.token=${{ secrets.WEBSITE_PUSH_TOKEN }} -Pdocker.registry=ghcr.io/${{ github.actor }} stage publishImage
diff --git a/.gitignore b/.gitignore
index 2595367..ca012b5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,7 @@ bin
.classpath
.project
org.eclipse.jdt.core.prefs
+**/.externalToolBuilders/
+
+# Node
+**/node_modules/
diff --git a/.markdownlint.yaml b/.markdownlint.yaml
new file mode 100644
index 0000000..6ed5002
--- /dev/null
+++ b/.markdownlint.yaml
@@ -0,0 +1,30 @@
+# See [rules](https://github.com/DavidAnson/markdownlint/blob/main/schema/.markdownlint.yaml)
+
+# Default state for all rules
+default: true
+
+# MD007/ul-indent : Unordered list indentation :
+# https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md007.md
+MD007:
+ # Spaces for indent
+ indent: 2
+ # Whether to indent the first level of the list
+ start_indented: true
+ # Spaces for first level indent (when start_indented is set)
+ start_indent: 2
+
+# MD025/single-title/single-h1 : Multiple top-level headings in the same document :
+# https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md025.md
+MD025:
+ # Heading level
+ level: 1
+ # RegExp for matching title in front matter (disable)
+ front_matter_title: ""
+
+# MD036/no-emphasis-as-heading : Emphasis used instead of a heading :
+# https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md036.md
+MD036: false
+
+# MD043/required-headings : Required heading structure :
+# https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md043.md
+MD043: false
diff --git a/.settings/org.eclipse.buildship.core.prefs b/.settings/org.eclipse.buildship.core.prefs
index d0fed22..72733d9 100644
--- a/.settings/org.eclipse.buildship.core.prefs
+++ b/.settings/org.eclipse.buildship.core.prefs
@@ -1,4 +1,4 @@
-arguments=--init-script /home/mnl/.config/Code/User/globalStorage/redhat.java/1.18.0/config_linux/org.eclipse.osgi/51/0/.cp/gradle/init/init.gradle --init-script /home/mnl/.config/Code/User/globalStorage/redhat.java/1.18.0/config_linux/org.eclipse.osgi/51/0/.cp/gradle/protobuf/init.gradle
+arguments=--init-script /home/mnl/.config/Code/User/globalStorage/redhat.java/1.24.0/config_linux/org.eclipse.osgi/55/0/.cp/gradle/init/init.gradle --init-script /home/mnl/.config/Code/User/globalStorage/redhat.java/1.24.0/config_linux/org.eclipse.osgi/55/0/.cp/gradle/protobuf/init.gradle
auto.sync=false
build.scans.enabled=false
connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
diff --git a/.woodpecker/build.yaml b/.woodpecker/build.yaml
new file mode 100644
index 0000000..56a575c
--- /dev/null
+++ b/.woodpecker/build.yaml
@@ -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
diff --git a/README.md b/README.md
index 1895bbb..09fcd25 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,25 @@
-[](https://github.com/mnlipp/VM-Operator/actions/workflows/gradle.yml)
+[](https://github.com/mnlipp/VM-Operator/actions/workflows/gradle.yml)
[](https://app.codacy.com/gh/mnlipp/VM-Operator/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)


-# Run Qemu in Kubernetes Pods
+# Run QEMU/KVM in Kubernetes Pods
-The goal of this project is to provide the means for running Qemu
-based VMs in Kubernetes pods.
+
-See the [project's home page](https://mnlipp.github.io/VM-Operator/)
+This project provides an easy to use and flexible solution for running
+QEMU/KVM based VMs in Kubernetes pods.
+
+The central component of this solution is the kubernetes operator that
+manages "runners". These run in pods and are used to start and manage
+the QEMU/KVM process for the VMs (optionally together with a SW-TPM).
+
+A web GUI for administrators provides an overview of the VMs together
+with some basic control over the VMs. A web GUI for users provides an
+interface to access and optionally start, stop and reset the VMs.
+
+Advanced features of the operator include pooling of VMs and automatic
+login.
+
+See the [project's home page](https://vm-operator.jdrupes.org/)
for details.
-
diff --git a/VM-Operator.png b/VM-Operator.png
new file mode 100644
index 0000000..9ecb022
Binary files /dev/null and b/VM-Operator.png differ
diff --git a/build.gradle b/build.gradle
index 4fd0f66..eb8e59a 100644
--- a/build.gradle
+++ b/build.gradle
@@ -5,11 +5,13 @@ buildscript {
}
plugins {
- id 'org.ajoberstar.grgit' version '5.2.0' apply false
+ id 'org.ajoberstar.grgit' version '5.2.0'
id 'org.ajoberstar.git-publish' version '4.2.0' apply false
- id 'pl.allegro.tech.build.axion-release' version '1.15.0' apply false
+ id 'pl.allegro.tech.build.axion-release' version '1.17.2' apply false
+ id 'org.jdrupes.vmoperator.versioning-conventions'
id 'org.jdrupes.vmoperator.java-doc-conventions'
id 'eclipse'
+ id "com.github.node-gradle.node" version "7.0.1"
}
allprojects {
@@ -17,7 +19,7 @@ allprojects {
}
task stage {
- description = 'To be executed by CI, build and update JavaDoc.'
+ description = 'To be executed by CI.'
group = 'build'
// Build everything first
@@ -25,11 +27,6 @@ task stage {
dependsOn subprojects.tasks.collect {
tc -> tc.findByName("build") }.flatten()
}
-
- if (JavaVersion.current() == JavaVersion.VERSION_17) {
- // Publish JavaDoc
- dependsOn gitPublishPush
- }
}
eclipse {
diff --git a/buildSrc/.project b/buildSrc/.project
deleted file mode 100644
index effb550..0000000
--- a/buildSrc/.project
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
- buildSrc
-
-
-
-
-
- org.eclipse.jdt.core.javabuilder
-
-
-
-
- org.eclipse.buildship.core.gradleprojectbuilder
-
-
-
-
-
- org.eclipse.jdt.groovy.core.groovyNature
- org.eclipse.jdt.core.javanature
- org.eclipse.buildship.core.gradleprojectnature
-
-
diff --git a/buildSrc/.settings/org.eclipse.jdt.core.prefs b/buildSrc/.settings/org.eclipse.jdt.core.prefs
index 68fda12..b25073a 100644
--- a/buildSrc/.settings/org.eclipse.jdt.core.prefs
+++ b/buildSrc/.settings/org.eclipse.jdt.core.prefs
@@ -1,9 +1,7 @@
+#
+#Wed Oct 02 14:48:43 CEST 2024
eclipse.preferences.version=1
-org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull
-org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable
-org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
-org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
org.eclipse.jdt.core.compiler.codegen.targetPlatform=21
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=21
@@ -11,12 +9,5 @@ org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
-org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
-org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
-org.eclipse.jdt.core.compiler.problem.nullReference=warning
-org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
-org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore
-org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
-org.eclipse.jdt.core.compiler.release=disabled
org.eclipse.jdt.core.compiler.source=21
diff --git a/buildSrc/.settings/org.eclipse.jdt.groovy.core.prefs b/buildSrc/.settings/org.eclipse.jdt.groovy.core.prefs
index bf0ca13..71b5e37 100644
--- a/buildSrc/.settings/org.eclipse.jdt.groovy.core.prefs
+++ b/buildSrc/.settings/org.eclipse.jdt.groovy.core.prefs
@@ -1,3 +1,3 @@
eclipse.preferences.version=1
-groovy.compiler.level=40
+groovy.compiler.level=-1
groovy.script.filters=**/*.dsld,y,**/*.gradle,n
diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle
index 8a1e6e1..4a5db6d 100644
--- a/buildSrc/build.gradle
+++ b/buildSrc/build.gradle
@@ -1,9 +1,3 @@
-/*
- * This file was generated by the Gradle 'init' task.
- *
- * This project uses @Incubating APIs which are subject to change.
- */
-
plugins {
// Support convention plugins written in Groovy. Convention plugins
// are build scripts in 'src/main' that automatically become available
@@ -14,51 +8,24 @@ plugins {
id 'eclipse'
}
-repositories {
- // Use the plugin portal to apply community plugins in convention plugins.
- gradlePluginPortal()
-}
-
sourceSets {
- main {
- groovy {
- srcDirs = ['src']
- }
- }
-
- test {
- groovy {
- srcDirs = ['test']
- }
- }
+ main {
+ groovy {
+ srcDirs = ['src']
+ }
+ resources {
+ srcDirs = ['resources']
+ }
+ }
}
eclipse {
- project {
- file {
- // closure executed after .project content is loaded from existing file
- // and before gradle build information is merged
- beforeMerged { project ->
- project.natures.clear()
- project.buildCommands.clear()
- }
-
- project.natures += 'org.eclipse.buildship.core.gradleprojectnature'
- project.buildCommand 'org.eclipse.buildship.core.gradleprojectbuilder'
- }
- }
-
- classpath {
- downloadJavadoc = true
- downloadSources = true
- }
-
jdt {
file {
withProperties { properties ->
def formatterPrefs = new Properties()
- rootProject.file("gradle/org.eclipse.jdt.core.formatter.prefs")
+ rootProject.file("../gradle/org.eclipse.jdt.core.formatter.prefs")
.withInputStream { formatterPrefs.load(it) }
properties.putAll(formatterPrefs)
}
diff --git a/buildSrc/settings.gradle b/buildSrc/settings.gradle
deleted file mode 100644
index 3f67e42..0000000
--- a/buildSrc/settings.gradle
+++ /dev/null
@@ -1,7 +0,0 @@
-/*
- * This file was generated by the Gradle 'init' task.
- *
- * This settings file is used to specify which projects to include in your build-logic build.
- */
-
-rootProject.name = 'buildSrc'
diff --git a/buildSrc/src/org.jdrupes.vmoperator.java-common-conventions.gradle b/buildSrc/src/org.jdrupes.vmoperator.java-common-conventions.gradle
index fa85d06..605dc09 100644
--- a/buildSrc/src/org.jdrupes.vmoperator.java-common-conventions.gradle
+++ b/buildSrc/src/org.jdrupes.vmoperator.java-common-conventions.gradle
@@ -5,6 +5,11 @@
*/
plugins {
+ // Apply the common versioning conventions.
+ // Put this at the start, because accessing project.version before
+ // this is applied makes things fail.
+ id 'org.jdrupes.vmoperator.versioning-conventions'
+
// Apply the java Plugin to add support for Java.
id 'java'
@@ -13,15 +18,11 @@ plugins {
// Access to git information
id 'org.ajoberstar.grgit'
-
- // Apply the common versioning conventions.
- id 'org.jdrupes.vmoperator.versioning-conventions'
}
repositories {
// Use Maven Central for resolving dependencies.
mavenCentral()
- mavenLocal()
}
dependencies {
@@ -54,37 +55,25 @@ sourceSets {
java {
toolchain {
- languageVersion = JavaLanguageVersion.of(17)
+ languageVersion = JavaLanguageVersion.of(21)
}
}
-scmVersion {
- versionIncrementer 'incrementMinor'
- tag {
- def shortened = project.name.startsWith(project.group + ".") ?
- project.name.substring(project.group.length() + 1) : project.name
- var p = shortened.replace('.', '-') + "-"
- if (grgit.branch.current.name != "main"
- && !grgit.branch.current.name.startsWith("release")) {
- p = p + grgit.branch.current.name.replace('/', '-') + "-"
- }
- prefix = p
- }
-}
-version = scmVersion.version
-ext.isSnapshot = version.endsWith('-SNAPSHOT')
-
jar {
manifest {
- inputs.property("gitDescriptor", { grgit.describe(always: true) })
+ def matchExpr = [ project.tagName + "*" ]
+
+ inputs.property("gitDescriptor",
+ { grgit.describe(always: true, match: matchExpr) })
// Set Git revision information in the manifests of built bundles
+ def gitDesc = grgit.describe(always: true, match: matchExpr)
attributes([
"Implementation-Title": project.name,
- "Implementation-Version": "$project.version (built from ${grgit.describe(always: true)})",
+ "Implementation-Version": "$project.version (built from ${gitDesc})",
"Implementation-Vendor": grgit.repository.jgit.repository.config.getString("user", null, "name")
+ " (" + grgit.repository.jgit.repository.config.getString("user", null, "email") + ")",
- "Git-Descriptor": grgit.describe(always: true),
+ "Git-Descriptor": gitDesc,
"Git-SHA": grgit.head().id,
])
}
diff --git a/buildSrc/src/org.jdrupes.vmoperator.java-doc-conventions.gradle b/buildSrc/src/org.jdrupes.vmoperator.java-doc-conventions.gradle
index 95d7eff..6af8fa7 100644
--- a/buildSrc/src/org.jdrupes.vmoperator.java-doc-conventions.gradle
+++ b/buildSrc/src/org.jdrupes.vmoperator.java-doc-conventions.gradle
@@ -22,31 +22,28 @@ configurations {
}
dependencies {
- markdownDoclet "org.jdrupes.mdoclet:doclet:3.1.0"
- javadocTaglets "org.jdrupes.taglets:plantuml-taglet:2.1.0"
-}
-
-task javadocResources(type: Copy) {
- into file(docDestinationDir)
- from ("${rootProject.rootDir}/misc") {
- include '*.woff2'
- }
+ markdownDoclet "org.jdrupes.mdoclet:doclet:4.0.0"
+ javadocTaglets "org.jdrupes.taglets:plantuml-taglet:3.0.0"
}
task apidocs (type: JavaExec) {
// Does not work on JitPack, no /usr/bin/dot
- enabled = JavaVersion.current() == JavaVersion.VERSION_17
-
- dependsOn javadocResources
+ enabled = JavaVersion.current() == JavaVersion.VERSION_21
outputs.dir(docDestinationDir)
inputs.file rootProject.file('overview.md')
- inputs.file "${rootProject.rootDir}/misc/stylesheet.css"
+ inputs.file "${rootProject.rootDir}/misc/javadoc-overwrites.css"
- jvmArgs = ['--add-exports=jdk.javadoc/jdk.javadoc.internal.tool=ALL-UNNAMED',
- '--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED']
- main = 'jdk.javadoc.internal.tool.Main'
+ jvmArgs = ['--add-exports=jdk.compiler/com.sun.tools.doclint=ALL-UNNAMED',
+ '--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED',
+ '--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED',
+ '--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED',
+ '--add-exports=jdk.javadoc/jdk.javadoc.internal.tool=ALL-UNNAMED',
+ '--add-exports=jdk.javadoc/jdk.javadoc.internal.doclets.toolkit=ALL-UNNAMED',
+ '--add-opens=jdk.javadoc/jdk.javadoc.internal.doclets.toolkit.resources.releases=ALL-UNNAMED',
+ '-Duser.language=en', '-Duser.region=US']
+ mainClass = 'jdk.javadoc.internal.tool.Main'
gradle.projectsEvaluated {
// Make sure that other projects' compileClasspaths are resolved
@@ -69,8 +66,8 @@ task apidocs (type: JavaExec) {
'-package',
'-use',
'-linksource',
- '-link', 'https://docs.oracle.com/en/java/javase/17/docs/api/',
- '-link', 'https://mnlipp.github.io/jgrapes/latest-release/javadoc/',
+ '-link', 'https://docs.oracle.com/en/java/javase/21/docs/api/',
+ '-link', 'https://jgrapes.org/latest-release/javadoc/',
'-link', 'https://freemarker.apache.org/docs/api/',
'--add-exports', 'jdk.javadoc/jdk.javadoc.internal.tool=ALL-UNNAMED',
'--add-exports', 'jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED',
@@ -88,7 +85,7 @@ task apidocs (type: JavaExec) {
'-bottom', rootProject.file("misc/javadoc.bottom.txt").text,
'--allow-script-in-comments',
'-Xdoclint:-html',
- '--main-stylesheet', "${rootProject.rootDir}/misc/stylesheet.css",
+ '--add-stylesheet', "${rootProject.rootDir}/misc/javadoc-overwrites.css",
'--add-exports=jdk.javadoc/jdk.javadoc.internal.doclets.formats.html=ALL-UNNAMED',
'-quiet'
]
@@ -97,34 +94,27 @@ task apidocs (type: JavaExec) {
ignoreExitValue true
}
+task testJavadoc(type: Javadoc) {
+ enabled = JavaVersion.current() == JavaVersion.VERSION_21
+
+ source = fileTree(dir: 'testfiles', include: '**/*.java')
+ destinationDir = project.file("build/testfiles-gradle")
+ options.docletpath = configurations.markdownDoclet.files.asType(List)
+ options.doclet = 'org.jdrupes.mdoclet.MDoclet'
+ options.overview = 'testfiles/overview.md'
+ options.addStringOption('Xdoclint:-html', '-quiet')
+
+ options.setJFlags([
+ '--add-exports=jdk.compiler/com.sun.tools.doclint=ALL-UNNAMED',
+ '--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED',
+ '--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED',
+ '--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED',
+ '--add-exports=jdk.javadoc/jdk.javadoc.internal.tool=ALL-UNNAMED',
+ '--add-exports=jdk.javadoc/jdk.javadoc.internal.doclets.toolkit=ALL-UNNAMED',
+ '--add-opens=jdk.javadoc/jdk.javadoc.internal.doclets.toolkit.resources.releases=ALL-UNNAMED'])
+}
// Prepare github authentication for plugins
if (System.properties['org.ajoberstar.grgit.auth.username'] == null) {
System.setProperty('org.ajoberstar.grgit.auth.username',
- project.rootProject.properties['repo.access.token'] ?: "nouser")
-}
-
-gitPublish {
- repoUri = 'https://github.com/mnlipp/VM-Operator.git'
- branch = 'gh-pages'
- contents {
- from("${rootProject.buildDir}/javadoc") {
- into 'javadoc'
- }
- if (!findProject(':org.jdrupes.vmoperator.runner.qemu').isSnapshot
- && !findProject(':org.jdrupes.vmoperator.manager').isSnapshot) {
- from("${rootProject.buildDir}/javadoc") {
- into 'latest-release/javadoc'
- }
- }
- }
- preserve { include '**/*' }
- commitMessage = "Updated."
-}
-
-gradle.projectsEvaluated {
- tasks.gitPublishReset.mustRunAfter subprojects.tasks
- .collect { tc -> tc.findByName("build") }.flatten()
- tasks.gitPublishReset.mustRunAfter subprojects.tasks
- .collect { tc -> tc.findByName("test") }.flatten()
- tasks.gitPublishCopy.dependsOn apidocs
+ project.rootProject.properties['website.push.token'] ?: "nouser")
}
diff --git a/buildSrc/src/org.jdrupes.vmoperator.versioning-conventions.gradle b/buildSrc/src/org.jdrupes.vmoperator.versioning-conventions.gradle
index 21006a3..49b6f74 100644
--- a/buildSrc/src/org.jdrupes.vmoperator.versioning-conventions.gradle
+++ b/buildSrc/src/org.jdrupes.vmoperator.versioning-conventions.gradle
@@ -11,18 +11,26 @@ plugins {
id 'pl.allegro.tech.build.axion-release'
}
+def shortened = project.name.startsWith(project.group + ".") ?
+ project.name.substring(project.group.length() + 1) : project.name
+if (shortened == "manager") {
+ shortened = "manager-app";
+}
+var tagName = shortened.replace('.', '-') + "-"
+if (grgit.branch.current.name != "main"
+ && grgit.branch.current.name != "HEAD"
+ && !grgit.branch.current.name.startsWith("testing")
+ && !grgit.branch.current.name.startsWith("release")
+ && !grgit.branch.current.name.startsWith("develop")) {
+ tagName = tagName + grgit.branch.current.name.replace('/', '-') + "-"
+}
+project.ext.tagName = tagName
+
scmVersion {
versionIncrementer 'incrementMinor'
tag {
- def shortened = project.name.startsWith(project.group + ".") ?
- project.name.substring(project.group.length() + 1) : project.name
- var p = shortened.replace('.', '-') + "-"
- if (grgit.branch.current.name != "main"
- && !grgit.branch.current.name.startsWith("release")) {
- p = p + grgit.branch.current.name.replace('/', '-') + "-"
- }
- prefix = p
+ prefix = project.tagName
}
}
-version = scmVersion.version
+project.version = scmVersion.version
ext.isSnapshot = version.endsWith('-SNAPSHOT')
diff --git a/checkstyle.xml b/checkstyle.xml
index 7fa3b95..088e543 100644
--- a/checkstyle.xml
+++ b/checkstyle.xml
@@ -1,7 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/misc/stylesheet.css b/misc/stylesheet.css
deleted file mode 100644
index e21b9b2..0000000
--- a/misc/stylesheet.css
+++ /dev/null
@@ -1,912 +0,0 @@
-/*
- * Javadoc style sheet
- */
-
-@font-face {
- font-family: 'DejaVu Serif';
- src: local('DejaVu Serif'), url('DejaVuSerif.woff2');
-}
-
-@font-face {
- font-family: 'DejaVu Serif';
- font-weight: bold;
- src: local('DejaVu Serif Bold'), url('DejaVuSerif-Bold.woff2');
-}
-
-@font-face {
- font-family: 'DejaVu Sans';
- src: local('DejaVu Sans'), url('DejaVuSans.woff2');
-}
-
-@font-face {
- font-family: 'DejaVu Sans';
- font-weight: bold;
- src: local('DejaVu Sans Bold'), url('DejaVuSans-Bold.woff2');
-}
-
-@font-face {
- font-family: 'DejaVu Sans Mono';
- src: local('DejaVu Sans Mono'), url('DejaVuSansMono.woff2');
-}
-
-@font-face {
- font-family: 'DejaVu Sans Mono';
- font-weight: bold;
- src: local('DejaVu Sans Mono Bold'), url('DejaVuSansMono-Bold.woff2');
-}
-
-/*
- * Styles for individual HTML elements.
- *
- * These are styles that are specific to individual HTML elements. Changing them affects the style of a particular
- * HTML element throughout the page.
- */
-
-body {
- background-color:#ffffff;
- color:#353833;
- font: normal 16px/1.5 "DejaVu Serif", serif;
- margin:0;
- padding:0;
- height:100%;
- width:100%;
-}
-iframe {
- margin:0;
- padding:0;
- height:100%;
- width:100%;
- overflow-y:scroll;
- border:none;
-}
-a:link, a:visited {
- text-decoration:none;
- color:#4A6782;
-}
-a[href]:hover, a[href]:focus {
- text-decoration:none;
- color:#bb7a2a;
-}
-a[name] {
- color:#353833;
-}
-pre {
- font-family: "DejaVu Sans Mono", monospace;
-}
-h1 {
- font-family: "DejaVu Sans", sans;
- font-size:20px;
-}
-h2 {
- font-family: "DejaVu Sans", sans;
- font-size:18px;
-}
-h3 {
- font-family: "DejaVu Sans", sans;
- font-size:16px;
-}
-h4 {
- font-family: "DejaVu Sans", sans;
- font-size:15px;
-}
-h5 {
- font-family: "DejaVu Sans", sans;
- font-size:14px;
-}
-h6 {
- font-family: "DejaVu Sans", sans;
- font-size:13px;
-}
-ul {
- list-style-type:disc;
-}
-code, tt {
- font-family: "DejaVu Sans Mono", monospace;
-}
-:not(h1, h2, h3, h4, h5, h6) > code,
-:not(h1, h2, h3, h4, h5, h6) > tt {
- /* font-size:14px; */
- padding-top:4px;
- margin-top:8px;
- line-height:1.4em;
-}
-dt code {
- font-family: "DejaVu Sans Mono", monospace;
- font-size:14px;
- padding-top:4px;
-}
-.summary-table dt code {
- font-family: "DejaVu Sans Mono", monospace;
- font-size:14px;
- vertical-align:top;
- padding-top:4px;
-}
-sup {
- font-size:8px;
-}
-
-/*
- * Styles for HTML generated by javadoc.
- *
- * These are style classes that are used by the standard doclet to generate HTML documentation.
- */
-
-/*
- * Styles for document title and copyright.
- */
-.clear {
- clear:both;
- height:0;
- overflow:hidden;
-}
-.about-language {
- float:right;
- padding:0 21px 8px 8px;
- font-size:11px;
- margin-top:-9px;
- height:2.9em;
-}
-.legal-copy {
- margin-left:.5em;
-}
-.tab {
- background-color:#0066FF;
- color:#ffffff;
- padding:8px;
- width:5em;
- font-weight:bold;
-}
-/*
- * Styles for navigation bar.
- */
-@media screen {
- .flex-box {
- position:fixed;
- display:flex;
- flex-direction:column;
- height: 100%;
- width: 100%;
- }
- .flex-header {
- flex: 0 0 auto;
- }
- .flex-content {
- flex: 1 1 auto;
- overflow-y: auto;
- }
-}
-.top-nav {
- background-color:#4D7A97;
- color:#FFFFFF;
- float:left;
- padding:0;
- width:100%;
- clear:right;
- min-height:2.8em;
- padding-top:10px;
- overflow:hidden;
- font-family: "DejaVu Sans", sans;
- font-size:80%;
-}
-.sub-nav {
- background-color:#dee3e9;
- float:left;
- width:100%;
- overflow:hidden;
- font-family: "DejaVu Sans", sans;
- font-size:80%;
-}
-.sub-nav div {
- clear:left;
- float:left;
- padding:0 0 5px 6px;
- text-transform:uppercase;
-}
-.sub-nav .nav-list {
- padding-top:5px;
-}
-ul.nav-list {
- display:block;
- margin:0 25px 0 0;
- padding:0;
-}
-ul.sub-nav-list {
- float:left;
- margin:0 25px 0 0;
- padding:0;
-}
-ul.nav-list li {
- list-style:none;
- float:left;
- padding: 5px 6px;
- text-transform:uppercase;
-}
-.sub-nav .nav-list-search {
- float:right;
- margin:0 0 0 0;
- padding:5px 6px;
- clear:none;
-}
-.nav-list-search label {
- position:relative;
- right:-16px;
-}
-ul.sub-nav-list li {
- list-style:none;
- float:left;
- padding-top:10px;
-}
-.top-nav a:link, .top-nav a:active, .top-nav a:visited {
- color:#FFFFFF;
- text-decoration:none;
- text-transform:uppercase;
-}
-.top-nav a:hover {
- text-decoration:none;
- color:#bb7a2a;
- text-transform:uppercase;
-}
-.nav-bar-cell1-rev {
- background-color:#F8981D;
- color:#253441;
- margin: auto 5px;
-}
-.skip-nav {
- position:absolute;
- top:auto;
- left:-9999px;
- overflow:hidden;
-}
-/*
- * Hide navigation links and search box in print layout
- */
-@media print {
- ul.nav-list, div.sub-nav {
- display:none;
- }
-}
-/*
- * Styles for page header and footer.
- */
-.title {
- color:#2c4557;
- margin:10px 0;
-}
-.sub-title {
- margin:5px 0 0 0;
-}
-.header ul {
- margin:0 0 15px 0;
- padding:0;
-}
-.header ul li, .footer ul li {
- list-style:none;
- font-size:80%;
-}
-/*
- * Styles for headings.
- */
-body.class-declaration-page .summary h2,
-body.class-declaration-page .details h2,
-body.class-use-page h2,
-body.module-declaration-page .block-list h2 {
- font-style: italic;
- padding:0;
- margin:15px 0;
-}
-body.class-declaration-page .summary h3,
-body.class-declaration-page .details h3,
-body.class-declaration-page .summary .inherited-list h2 {
- background-color:#dee3e9;
- border:1px solid #d0d9e0;
- margin:0 0 6px -8px;
- padding:7px 5px;
-}
-/*
- * Styles for page layout containers.
- */
-main {
- clear:both;
- padding:10px 20px;
- position:relative;
-}
-dl.notes > dt {
- font-family: "DejaVu Sans", sans;
- font-weight:bold;
- margin:10px 0 0 0;
- color:#4E4E4E;
-}
-dl.notes > dd {
- margin:5px 10px 10px 0;
-}
-dl.name-value > dt {
- margin-left:1px;
- /* font-size:1.1em; */
- display:inline;
- font-weight:bold;
-}
-dl.name-value > dd {
- margin:0 0 0 1px;
- /* font-size:1.1em; */
- display:inline;
-}
-/*
- * Styles for lists.
- */
-li.circle {
- list-style:circle;
-}
-ul.horizontal li {
- display:inline;
- /* font-size:0.9em; */
-}
-div.inheritance {
- margin:0;
- padding:0;
-}
-div.inheritance div.inheritance {
- margin-left:2em;
-}
-ul.block-list,
-ul.details-list,
-ul.member-list,
-ul.summary-list {
- margin:10px 0 10px 0;
- padding:0;
-}
-ul.block-list > li,
-ul.details-list > li,
-ul.member-list > li,
-ul.summary-list > li {
- list-style:none;
- margin-bottom:15px;
- line-height:1.4;
-}
-.summary-table dl, .summary-table dl dt, .summary-table dl dd {
- margin-top:0;
- margin-bottom:1px;
-}
-ul.see-list, ul.see-list-long {
- padding-left: 0;
- list-style: none;
-}
-ul.see-list li {
- display: inline;
-}
-ul.see-list li:not(:last-child):after,
-ul.see-list-long li:not(:last-child):after {
- content: ", ";
- white-space: pre-wrap;
-}
-/*
- * Styles for tables.
- */
-.summary-table, .details-table {
- width:100%;
- border-spacing:0;
- border-left:1px solid #EEE;
- border-right:1px solid #EEE;
- border-bottom:1px solid #EEE;
- padding:0;
-}
-.caption {
- position:relative;
- text-align:left;
- background-repeat:no-repeat;
- color:#253441;
- font-weight:bold;
- clear:none;
- overflow:hidden;
- padding:0;
- padding-top:10px;
- padding-left:1px;
- margin:0;
- white-space:pre;
- font-family: 'DejaVu Sans';
-}
-.caption a:link, .caption a:visited {
- color:#1f389c;
-}
-.caption a:hover,
-.caption a:active {
- color:#FFFFFF;
-}
-.caption span {
- white-space:nowrap;
- padding-top:5px;
- padding-left:12px;
- padding-right:12px;
- padding-bottom:7px;
- display:inline-block;
- float:left;
- background-color:#F8981D;
- border: none;
- height:16px;
-}
-div.table-tabs {
- padding:10px 0 0 1px;
- margin:0;
-}
-div.table-tabs > button {
- border: none;
- cursor: pointer;
- padding: 5px 12px 7px 12px;
- font-weight: bold;
- margin-right: 3px;
-}
-div.table-tabs > button.active-table-tab {
- background: #F8981D;
- color: #253441;
-}
-div.table-tabs > button.table-tab {
- background: #4D7A97;
- color: #FFFFFF;
-}
-.two-column-summary {
- display: grid;
- grid-template-columns: minmax(15%, max-content) minmax(15%, auto);
-}
-.three-column-summary {
- display: grid;
- grid-template-columns: minmax(10%, max-content) minmax(15%, max-content) minmax(15%, auto);
-}
-#method-summary-table .three-column-summary {
- grid-template-columns: minmax(10%, 20%) minmax(15%, max-content) minmax(15%, auto);
-}
-.four-column-summary {
- display: grid;
- grid-template-columns: minmax(10%, max-content) minmax(10%, max-content) minmax(10%, max-content) minmax(10%, auto);
-}
-@media screen and (max-width: 600px) {
- .two-column-summary {
- display: grid;
- grid-template-columns: 1fr;
- }
-}
-@media screen and (max-width: 800px) {
- .three-column-summary {
- display: grid;
- grid-template-columns: minmax(10%, max-content) minmax(25%, auto);
- }
- .three-column-summary .col-last {
- grid-column-end: span 2;
- }
-}
-@media screen and (max-width: 1000px) {
- .four-column-summary {
- display: grid;
- grid-template-columns: minmax(15%, max-content) minmax(15%, auto);
- }
-}
-.summary-table > div, .details-table > div {
- text-align:left;
- padding: 8px 3px 3px 7px;
-}
-.col-first, .col-second, .col-last, .col-constructor-name, .col-summary-item-name {
- vertical-align:top;
- padding-right:0;
- padding-top:8px;
- padding-bottom:3px;
-}
-.table-header {
- background:#dee3e9;
- font-family: 'DejaVu Sans';
- font-weight: bold;
-}
-/*
-.col-first, .col-first {
- font-size:13px;
-}
-.col-second, .col-second, .col-last, .col-constructor-name, .col-summary-item-name, .col-last {
- font-size:13px;
-}
-*/
-.col-first, .col-second, .col-constructor-name {
- vertical-align:top;
- overflow: auto;
-}
-.col-last {
- white-space:normal;
-}
-/*
-.col-first a:link, .col-first a:visited,
-.col-second a:link, .col-second a:visited,
-.col-first a:link, .col-first a:visited,
-.col-second a:link, .col-second a:visited,
-.col-constructor-name a:link, .col-constructor-name a:visited,
-.col-summary-item-name a:link, .col-summary-item-name a:visited,
-.constant-values-container a:link, .constant-values-container a:visited,
-.all-classes-container a:link, .all-classes-container a:visited,
-.all-packages-container a:link, .all-packages-container a:visited {
- font-weight:bold;
-}
-*/
-.table-sub-heading-color {
- background-color:#EEEEFF;
-}
-.even-row-color, .even-row-color .table-header {
- background-color:#FFFFFF;
-}
-.odd-row-color, .odd-row-color .table-header {
- background-color:#EEEEEF;
-}
-/*
- * Styles for contents.
- */
-.deprecated-content {
- margin:0;
- padding:10px 0;
-}
-/*
-div.block {
- font-size:14px;
- font-family:'DejaVu Serif', Georgia, "Times New Roman", Times, serif;
-}
-*/
-.col-last div {
- padding-top:0;
-}
-.col-last a {
- padding-bottom:3px;
-}
-.module-signature,
-.package-signature,
-.type-signature,
-.member-signature {
- font-family: "DejaVu Sans Mono", monospace;
- /* font-size:14px; */
- margin:14px 0;
- white-space: pre-wrap;
-}
-.module-signature,
-.package-signature,
-.type-signature {
- margin-top: 0;
-}
-.member-signature .type-parameters-long,
-.member-signature .parameters,
-.member-signature .exceptions {
- display: inline-block;
- vertical-align: top;
- white-space: pre;
-}
-.member-signature .type-parameters {
- white-space: normal;
-}
-/*
- * Styles for formatting effect.
- */
-.source-line-no {
- color:green;
- padding:0 30px 0 0;
-}
-h1.hidden {
- visibility:hidden;
- overflow:hidden;
- /* font-size:10px; */
-}
-.block {
- display:block;
- margin:0 10px 5px 0;
- color:#474747;
-}
-.deprecated-label, .descfrm-type-label, .implementation-label, .member-name-label, .member-name-link,
-.module-label-in-package, .module-label-in-type, .override-specify-label, .package-label-in-type,
-.package-hierarchy-label, .type-name-label, .type-name-link, .search-tag-link, .preview-label {
- font-family: "DejaVu Sans", sans;
- font-weight:bold;
-}
-.sub-title, .inheritance, .all-packages-table-tab1.col-first,
- .summary-table .col-first {
- font-family: "DejaVu Sans", sans;
-}
-.deprecation-comment, .help-footnote, .preview-comment {
- font-style:italic;
-}
-.deprecation-block {
- /* font-size:14px; */
- font-family:'DejaVu Serif', Georgia, "Times New Roman", Times, serif;
- border-style:solid;
- border-width:thin;
- border-radius:10px;
- padding:10px;
- margin-bottom:10px;
- margin-right:10px;
- display:inline-block;
-}
-.preview-block {
- /* font-size:14px; */
- font-family:'DejaVu Serif', Georgia, "Times New Roman", Times, serif;
- border-style:solid;
- border-width:thin;
- border-radius:10px;
- padding:10px;
- margin-bottom:10px;
- margin-right:10px;
- display:inline-block;
-}
-div.block div.deprecation-comment {
- font-style:normal;
-}
-/*
- * Styles specific to HTML5 elements.
- */
-main, nav, header, footer, section {
- display:block;
-}
-/*
- * Styles for javadoc search.
- */
-.ui-autocomplete-category {
- font-weight:bold;
- /* font-size:15px; */
- padding:7px 0 7px 3px;
- background-color:#4D7A97;
- color:#FFFFFF;
-}
-.result-item {
- /* font-size:13px; */
-}
-.ui-autocomplete {
- max-height:85%;
- max-width:65%;
- overflow-y:scroll;
- overflow-x:scroll;
- white-space:nowrap;
- box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
-}
-ul.ui-autocomplete {
- position:fixed;
- z-index:999999;
-}
-ul.ui-autocomplete li {
- float:left;
- clear:both;
- width:100%;
-}
-.result-highlight {
- font-weight:bold;
-}
-#search-input {
- background-image:url('resources/glass.png');
- background-size:13px;
- background-repeat:no-repeat;
- background-position:2px 3px;
- padding-left:20px;
- position:relative;
- right:-18px;
- width:400px;
-}
-#reset-button {
- background-color: rgb(255,255,255);
- background-image:url('resources/x.png');
- background-position:center;
- background-repeat:no-repeat;
- background-size:12px;
- border:0 none;
- width:16px;
- height:16px;
- position:relative;
- left:-4px;
- top:-4px;
- font-size:0px;
-}
-.watermark {
- color:#545454;
-}
-.search-tag-desc-result {
- font-style:italic;
- /* font-size:11px; */
-}
-.search-tag-holder-result {
- font-style:italic;
- /* font-size:12px; */
-}
-.search-tag-result:target {
- background-color:yellow;
-}
-.module-graph span {
- display:none;
- position:absolute;
-}
-.module-graph:hover span {
- display:block;
- margin: -100px 0 0 100px;
- z-index: 1;
-}
-.inherited-list {
- margin: 10px 0 10px 0;
-}
-section.class-description {
- line-height: 1.4;
-}
-.summary section[class$="-summary"], .details section[class$="-details"],
-.class-uses .detail, .serialized-class-details {
- padding: 0px 20px 5px 10px;
- border: 1px solid #ededed;
- background-color: #f8f8f8;
-}
-.inherited-list, section[class$="-details"] .detail {
- padding:0 0 5px 8px;
- background-color:#ffffff;
- border:none;
-}
-.vertical-separator {
- padding: 0 5px;
-}
-ul.help-section-list {
- margin: 0;
-}
-ul.help-subtoc > li {
- display: inline-block;
- padding-right: 5px;
- /* font-size: smaller; */
-}
-ul.help-subtoc > li::before {
- content: "\2022" ;
- padding-right:2px;
-}
-span.help-note {
- font-style: italic;
-}
-/*
- * Indicator icon for external links.
- */
-main a[href*="://"]::after {
- content:"";
- display:inline-block;
- background-image:url('data:image/svg+xml; utf8, \
- ');
- background-size:100% 100%;
- width:7px;
- height:7px;
- margin-left:2px;
- margin-bottom:4px;
-}
-main a[href*="://"]:hover::after,
-main a[href*="://"]:focus::after {
- background-image:url('data:image/svg+xml; utf8, \
- ');
-}
-
-/*
- * Styles for user-provided tables.
- *
- * borderless:
- * No borders, vertical margins, styled caption.
- * This style is provided for use with existing doc comments.
- * In general, borderless tables should not be used for layout purposes.
- *
- * plain:
- * Plain borders around table and cells, vertical margins, styled caption.
- * Best for small tables or for complex tables for tables with cells that span
- * rows and columns, when the "striped" style does not work well.
- *
- * striped:
- * Borders around the table and vertical borders between cells, striped rows,
- * vertical margins, styled caption.
- * Best for tables that have a header row, and a body containing a series of simple rows.
- */
-
-table.borderless,
-table.plain,
-table.striped {
- margin-top: 10px;
- margin-bottom: 10px;
-}
-table.borderless > caption,
-table.plain > caption,
-table.striped > caption {
- font-weight: bold;
- /* font-size: smaller; */
-}
-table.borderless th, table.borderless td,
-table.plain th, table.plain td,
-table.striped th, table.striped td {
- padding: 2px 5px;
-}
-table.borderless,
-table.borderless > thead > tr > th, table.borderless > tbody > tr > th, table.borderless > tr > th,
-table.borderless > thead > tr > td, table.borderless > tbody > tr > td, table.borderless > tr > td {
- border: none;
-}
-table.borderless > thead > tr, table.borderless > tbody > tr, table.borderless > tr {
- background-color: transparent;
-}
-table.plain {
- border-collapse: collapse;
- border: 1px solid black;
-}
-table.plain > thead > tr, table.plain > tbody tr, table.plain > tr {
- background-color: transparent;
-}
-table.plain > thead > tr > th, table.plain > tbody > tr > th, table.plain > tr > th,
-table.plain > thead > tr > td, table.plain > tbody > tr > td, table.plain > tr > td {
- border: 1px solid black;
-}
-table.striped {
- border-collapse: collapse;
- border: 1px solid black;
-}
-table.striped > thead {
- background-color: #E3E3E3;
-}
-table.striped > thead > tr > th, table.striped > thead > tr > td {
- border: 1px solid black;
-}
-table.striped > tbody > tr:nth-child(even) {
- background-color: #EEE
-}
-table.striped > tbody > tr:nth-child(odd) {
- background-color: #FFF
-}
-table.striped > tbody > tr > th, table.striped > tbody > tr > td {
- border-left: 1px solid black;
- border-right: 1px solid black;
-}
-table.striped > tbody > tr > th {
- font-weight: normal;
-}
-/**
- * Tweak font sizes and paddings for small screens.
- */
-@media screen and (max-width: 1050px) {
- #search-input {
- width: 300px;
- }
-}
-@media screen and (max-width: 800px) {
- #search-input {
- width: 200px;
- }
- .top-nav,
- .bottom-nav {
- font-size: 80%;
- padding-top: 6px;
- }
- .sub-nav {
- font-size: 80%;
- }
- .about-language {
- padding-right: 16px;
- }
- ul.nav-list li,
- .sub-nav .nav-list-search {
- padding: 6px;
- }
- ul.sub-nav-list li {
- padding-top: 5px;
- }
- main {
- padding: 10px;
- }
- .summary section[class$="-summary"], .details section[class$="-details"],
- .class-uses .detail, .serialized-class-details {
- padding: 0 8px 5px 8px;
- }
- body {
- -webkit-text-size-adjust: none;
- }
-}
-@media screen and (max-width: 500px) {
- #search-input {
- width: 150px;
- }
- .top-nav,
- .bottom-nav {
- font-size: 80%;
- }
- .sub-nav {
- font-size: 80%;
- }
- .about-language {
- font-size: 80%;
- padding-right: 12px;
- }
-}
diff --git a/org.jdrupes.vmoperator.common/.checkstyle b/org.jdrupes.vmoperator.common/.checkstyle
new file mode 100644
index 0000000..7f2c604
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/.checkstyle
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/org.jdrupes.vmoperator.common/.eclipse-pmd b/org.jdrupes.vmoperator.common/.eclipse-pmd
new file mode 100644
index 0000000..5d69caa
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/.eclipse-pmd
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/org.jdrupes.vmoperator.common/.settings/net.sf.jautodoc.prefs b/org.jdrupes.vmoperator.common/.settings/net.sf.jautodoc.prefs
new file mode 100644
index 0000000..8b8b906
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/.settings/net.sf.jautodoc.prefs
@@ -0,0 +1,7 @@
+add_header=true
+eclipse.preferences.version=1
+header_text=/*\n * VM-Operator\n * Copyright (C) 2024 Michael N. Lipp\n * \n * This program is free software\: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see .\n */
+project_specific_settings=true
+visibility_package=false
+visibility_private=false
+visibility_protected=false
diff --git a/org.jdrupes.vmoperator.common/.settings/org.eclipse.buildship.core.prefs b/org.jdrupes.vmoperator.common/.settings/org.eclipse.buildship.core.prefs
new file mode 100644
index 0000000..258eb47
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/.settings/org.eclipse.buildship.core.prefs
@@ -0,0 +1,13 @@
+arguments=
+auto.sync=false
+build.scans.enabled=false
+connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
+connection.project.dir=..
+eclipse.preferences.version=1
+gradle.user.home=
+java.home=
+jvm.arguments=
+offline.mode=false
+override.workspace.settings=false
+show.console.view=false
+show.executions.view=false
diff --git a/org.jdrupes.vmoperator.common/.settings/org.eclipse.core.resources.prefs b/org.jdrupes.vmoperator.common/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000..99f26c0
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+encoding/=UTF-8
diff --git a/org.jdrupes.vmoperator.common/.settings/org.eclipse.core.runtime.prefs b/org.jdrupes.vmoperator.common/.settings/org.eclipse.core.runtime.prefs
new file mode 100644
index 0000000..5a0ad22
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/.settings/org.eclipse.core.runtime.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+line.separator=\n
diff --git a/org.jdrupes.vmoperator.common/build.gradle b/org.jdrupes.vmoperator.common/build.gradle
new file mode 100644
index 0000000..e72cb14
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/build.gradle
@@ -0,0 +1,17 @@
+/*
+ * This file was generated by the Gradle 'init' task.
+ *
+ * This project uses @Incubating APIs which are subject to change.
+ */
+
+plugins {
+ id 'org.jdrupes.vmoperator.java-library-conventions'
+}
+
+dependencies {
+ api project(':org.jdrupes.vmoperator.util')
+ api 'org.jgrapes:org.jgrapes.core:[1.22.1,2)'
+ api 'io.kubernetes:client-java:[19.0.0,20.0.0)'
+ api 'org.yaml:snakeyaml'
+ api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:[2.16.1,3]'
+}
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Constants.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Constants.java
new file mode 100644
index 0000000..b9de69f
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Constants.java
@@ -0,0 +1,127 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2023 Michael N. Lipp
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.jdrupes.vmoperator.common;
+
+/**
+ * Some constants.
+ */
+@SuppressWarnings("PMD.DataClass")
+public class Constants {
+
+ /** The Constant APP_NAME. */
+ public static final String APP_NAME = "vm-runner";
+
+ /** The Constant VM_OP_NAME. */
+ public static final String VM_OP_NAME = "vm-operator";
+
+ /**
+ * Constants related to the CRD.
+ */
+ @SuppressWarnings("PMD.ShortClassName")
+ public static class Crd {
+ /** The Constant GROUP. */
+ public static final String GROUP = "vmoperator.jdrupes.org";
+
+ /** The Constant KIND_VM. */
+ public static final String KIND_VM = "VirtualMachine";
+
+ /** The Constant KIND_VM_POOL. */
+ public static final String KIND_VM_POOL = "VmPool";
+ }
+
+ /**
+ * Status related constants.
+ */
+ public static class Status {
+ /** The Constant RUNNER_VERSION. */
+ public static final String RUNNER_VERSION = "runnerVersion";
+
+ /** The Constant CPUS. */
+ public static final String CPUS = "cpus";
+
+ /** The Constant RAM. */
+ public static final String RAM = "ram";
+
+ /** The Constant OSINFO. */
+ public static final String OSINFO = "osinfo";
+
+ /** The Constant DISPLAY_PASSWORD_SERIAL. */
+ public static final String DISPLAY_PASSWORD_SERIAL
+ = "displayPasswordSerial";
+
+ /** The Constant LOGGED_IN_USER. */
+ public static final String LOGGED_IN_USER = "loggedInUser";
+
+ /** The Constant CONSOLE_CLIENT. */
+ public static final String CONSOLE_CLIENT = "consoleClient";
+
+ /** The Constant CONSOLE_USER. */
+ public static final String CONSOLE_USER = "consoleUser";
+
+ /** The Constant ASSIGNMENT. */
+ public static final String ASSIGNMENT = "assignment";
+
+ /**
+ * Conditions used in Status.
+ */
+ public static class Condition {
+ /** The Constant COND_RUNNING. */
+ public static final String RUNNING = "Running";
+
+ /** The Constant COND_BOOTED. */
+ public static final String BOOTED = "Booted";
+
+ /** The Constant COND_VMOP_AGENT. */
+ public static final String VMOP_AGENT = "VmopAgentConnected";
+
+ /** The Constant COND_USER_LOGGED_IN. */
+ public static final String USER_LOGGED_IN = "UserLoggedIn";
+
+ /** The Constant COND_CONSOLE. */
+ public static final String CONSOLE_CONNECTED = "ConsoleConnected";
+
+ /**
+ * Reasons used in conditions.
+ */
+ public static class Reason {
+ /** The Constant NOT_REQUESTED. */
+ public static final String NOT_REQUESTED = "NotRequested";
+
+ /** The Constant USER_LOGGED_IN. */
+ public static final String LOGGED_IN = "LoggedIn";
+ }
+ }
+ }
+
+ /**
+ * DisplaySecret related constants.
+ */
+ public static class DisplaySecret {
+
+ /** The Constant NAME. */
+ public static final String NAME = "display-secret";
+
+ /** The Constant PASSWORD. */
+ public static final String PASSWORD = "display-password";
+
+ /** The Constant EXPIRY. */
+ public static final String EXPIRY = "password-expiry";
+
+ }
+}
diff --git a/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/Convertions.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Convertions.java
similarity index 91%
rename from org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/Convertions.java
rename to org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Convertions.java
index 2dc46e5..68f52eb 100644
--- a/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/Convertions.java
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Convertions.java
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-package org.jdrupes.vmoperator.util;
+package org.jdrupes.vmoperator.common;
import java.math.BigDecimal;
import java.math.BigInteger;
@@ -32,13 +32,11 @@ import java.util.regex.Pattern;
public class Convertions {
@SuppressWarnings({ "PMD.UseConcurrentHashMap",
- "PMD.FieldNamingConventions", "PMD.VariableNamingConventions" })
+ "PMD.FieldNamingConventions" })
private static final Map unitMap = new HashMap<>();
- @SuppressWarnings({ "PMD.FieldNamingConventions",
- "PMD.VariableNamingConventions" })
+ @SuppressWarnings({ "PMD.FieldNamingConventions" })
private static final List> unitMappings;
- @SuppressWarnings({ "PMD.FieldNamingConventions",
- "PMD.VariableNamingConventions" })
+ @SuppressWarnings({ "PMD.FieldNamingConventions" })
private static final Pattern memorySize
= Pattern.compile("^\\s*(\\d+(\\.\\d+)?)\\s*([A-Za-z]*)\\s*");
@@ -69,7 +67,6 @@ public class Convertions {
* @param amount the amount
* @return the big integer
*/
- @SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public static BigInteger parseMemory(Object amount) {
if (amount == null) {
return (BigInteger) amount;
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/DynamicTypeAdapterFactory.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/DynamicTypeAdapterFactory.java
new file mode 100644
index 0000000..d21eed4
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/DynamicTypeAdapterFactory.java
@@ -0,0 +1,197 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2024 Michael N. Lipp
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.jdrupes.vmoperator.common;
+
+import com.google.gson.Gson;
+import com.google.gson.InstanceCreator;
+import com.google.gson.JsonObject;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import io.kubernetes.client.openapi.ApiClient;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Type;
+
+/**
+ * A factory for creating objects.
+ *
+ * @param the generic type
+ * @param the generic type
+ */
+public class DynamicTypeAdapterFactory> implements TypeAdapterFactory {
+
+ private final Class objectClass;
+ private final Class objectListClass;
+
+ /**
+ * Make sure that this adapter is registered.
+ *
+ * @param client the client
+ */
+ public void register(ApiClient client) {
+ if (!ModelCreator.class
+ .equals(client.getJSON().getGson().getAdapter(objectClass)
+ .getClass())
+ || !ModelsCreator.class.equals(client.getJSON().getGson()
+ .getAdapter(objectListClass).getClass())) {
+ Gson gson = client.getJSON().getGson();
+ client.getJSON().setGson(gson.newBuilder()
+ .registerTypeAdapterFactory(this).create());
+ }
+ }
+
+ /**
+ * Instantiates a new generic type adapter factory.
+ *
+ * @param objectClass the object class
+ * @param objectListClass the object list class
+ */
+ public DynamicTypeAdapterFactory(Class objectClass,
+ Class objectListClass) {
+ this.objectClass = objectClass;
+ this.objectListClass = objectListClass;
+ }
+
+ /**
+ * Creates a type adapter for the given type.
+ *
+ * @param the generic type
+ * @param gson the gson
+ * @param typeToken the type token
+ * @return the type adapter or null if the type is not handles by
+ * this factory
+ */
+ @SuppressWarnings("unchecked")
+ @Override
+ public TypeAdapter create(Gson gson, TypeToken typeToken) {
+ if (TypeToken.get(objectClass).equals(typeToken)) {
+ return (TypeAdapter) new ModelCreator(gson);
+ }
+ if (TypeToken.get(objectListClass).equals(typeToken)) {
+ return (TypeAdapter) new ModelsCreator(gson);
+ }
+ return null;
+ }
+
+ /**
+ * The Class ModelCreator.
+ */
+ private class ModelCreator extends TypeAdapter
+ implements InstanceCreator {
+ private final Gson delegate;
+
+ /**
+ * Instantiates a new object state creator.
+ *
+ * @param delegate the delegate
+ */
+ public ModelCreator(Gson delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public O createInstance(Type type) {
+ try {
+ return objectClass.getConstructor(Gson.class, JsonObject.class)
+ .newInstance(delegate, null);
+ } catch (InstantiationException | IllegalAccessException
+ | IllegalArgumentException | InvocationTargetException
+ | NoSuchMethodException | SecurityException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public void write(JsonWriter jsonWriter, O state)
+ throws IOException {
+ jsonWriter.jsonValue(delegate.toJson(state.data()));
+ }
+
+ @Override
+ public O read(JsonReader jsonReader)
+ throws IOException {
+ try {
+ return objectClass.getConstructor(Gson.class, JsonObject.class)
+ .newInstance(delegate,
+ delegate.fromJson(jsonReader, JsonObject.class));
+ } catch (InstantiationException | IllegalAccessException
+ | IllegalArgumentException | InvocationTargetException
+ | NoSuchMethodException | SecurityException e) {
+ return null;
+ }
+ }
+ }
+
+ /**
+ * The Class ModelsCreator.
+ */
+ private class ModelsCreator extends TypeAdapter
+ implements InstanceCreator {
+
+ private final Gson delegate;
+
+ /**
+ * Instantiates a new object states creator.
+ *
+ * @param delegate the delegate
+ */
+ public ModelsCreator(Gson delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public L createInstance(Type type) {
+ try {
+ return objectListClass
+ .getConstructor(Gson.class, JsonObject.class)
+ .newInstance(delegate, null);
+ } catch (InstantiationException | IllegalAccessException
+ | IllegalArgumentException | InvocationTargetException
+ | NoSuchMethodException | SecurityException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public void write(JsonWriter jsonWriter, L states)
+ throws IOException {
+ jsonWriter.jsonValue(delegate.toJson(states.data()));
+ }
+
+ @Override
+ public L read(JsonReader jsonReader)
+ throws IOException {
+ try {
+ return objectListClass
+ .getConstructor(Gson.class, JsonObject.class)
+ .newInstance(delegate,
+ delegate.fromJson(jsonReader, JsonObject.class));
+ } catch (InstantiationException | IllegalAccessException
+ | IllegalArgumentException | InvocationTargetException
+ | NoSuchMethodException | SecurityException e) {
+ return null;
+ }
+ }
+ }
+
+}
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8s.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8s.java
new file mode 100644
index 0000000..3870337
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8s.java
@@ -0,0 +1,249 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2023,2024 Michael N. Lipp
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.jdrupes.vmoperator.common;
+
+import com.google.gson.JsonObject;
+import io.kubernetes.client.Discovery;
+import io.kubernetes.client.Discovery.APIResource;
+import io.kubernetes.client.common.KubernetesListObject;
+import io.kubernetes.client.common.KubernetesObject;
+import io.kubernetes.client.common.KubernetesType;
+import io.kubernetes.client.custom.V1Patch;
+import io.kubernetes.client.openapi.ApiClient;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.apis.EventsV1Api;
+import io.kubernetes.client.openapi.models.EventsV1Event;
+import io.kubernetes.client.openapi.models.V1ObjectMeta;
+import io.kubernetes.client.openapi.models.V1ObjectReference;
+import io.kubernetes.client.util.Strings;
+import io.kubernetes.client.util.generic.GenericKubernetesApi;
+import io.kubernetes.client.util.generic.KubernetesApiResponse;
+import io.kubernetes.client.util.generic.options.PatchOptions;
+import java.io.Reader;
+import java.net.HttpURLConnection;
+import java.time.OffsetDateTime;
+import java.util.Map;
+import java.util.Optional;
+import org.yaml.snakeyaml.LoaderOptions;
+import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.constructor.SafeConstructor;
+
+/**
+ * Helpers for K8s API.
+ */
+@SuppressWarnings({ "PMD.ShortClassName", "PMD.UseUtilityClass" })
+public class K8s {
+
+ /**
+ * Returns the result from an API call as {@link Optional} if the
+ * call was successful. Returns an empty `Optional` if the status
+ * code is 404 (not found). Else throws an exception.
+ *
+ * @param the generic type
+ * @param response the response
+ * @return the optional
+ * @throws ApiException the API exception
+ */
+ public static Optional
+ optional(KubernetesApiResponse response) throws ApiException {
+ if (response.isSuccess()) {
+ return Optional.of(response.getObject());
+ }
+ if (response.getHttpStatusCode() == HttpURLConnection.HTTP_NOT_FOUND) {
+ return Optional.empty();
+ }
+ response.throwsApiException();
+ // Never reached
+ return Optional.empty();
+ }
+
+ /**
+ * Returns a new context with the given version as preferred version.
+ *
+ * @param context the context
+ * @param version the version
+ * @return the API resource
+ */
+ public static APIResource preferred(APIResource context, String version) {
+ assert context.getVersions().contains(version);
+ return new APIResource(context.getGroup(),
+ context.getVersions(), version, context.getKind(),
+ context.getNamespaced(), context.getResourcePlural(),
+ context.getResourceSingular());
+ }
+
+ /**
+ * Return a string representation of the context (API resource).
+ *
+ * @param context the context
+ * @return the string
+ */
+ @SuppressWarnings("PMD.UseLocaleWithCaseConversions")
+ public static String toString(APIResource context) {
+ return (Strings.isNullOrEmpty(context.getGroup()) ? ""
+ : context.getGroup() + "/")
+ + context.getPreferredVersion().toUpperCase()
+ + context.getKind();
+ }
+
+ /**
+ * Convert Yaml to Json.
+ *
+ * @param client the client
+ * @param yaml the yaml
+ * @return the json element
+ */
+ public static JsonObject yamlToJson(ApiClient client, Reader yaml) {
+ // Avoid Yaml.load due to
+ // https://github.com/kubernetes-client/java/issues/2741
+ Map yamlData
+ = new Yaml(new SafeConstructor(new LoaderOptions())).load(yaml);
+
+ // There's no short-cut from Java (collections) to Gson
+ var gson = client.getJSON().getGson();
+ var jsonText = gson.toJson(yamlData);
+ return gson.fromJson(jsonText, JsonObject.class);
+ }
+
+ /**
+ * Lookup the specified API resource. If the version is `null` or
+ * empty, the preferred version in the result is the default
+ * returned from the server.
+ *
+ * @param client the client
+ * @param group the group
+ * @param version the version
+ * @param kind the kind
+ * @return the optional
+ * @throws ApiException the api exception
+ */
+ public static Optional context(ApiClient client,
+ String group, String version, String kind) throws ApiException {
+ var apiMatch = new Discovery(client).findAll().stream()
+ .filter(r -> r.getGroup().equals(group) && r.getKind().equals(kind)
+ && (Strings.isNullOrEmpty(version)
+ || r.getVersions().contains(version)))
+ .findFirst();
+ if (apiMatch.isEmpty()) {
+ return Optional.empty();
+ }
+ var apiRes = apiMatch.get();
+ if (!Strings.isNullOrEmpty(version)) {
+ if (!apiRes.getVersions().contains(version)) {
+ return Optional.empty();
+ }
+ apiRes = new APIResource(apiRes.getGroup(), apiRes.getVersions(),
+ version, apiRes.getKind(), apiRes.getNamespaced(),
+ apiRes.getResourcePlural(), apiRes.getResourceSingular());
+ }
+ return Optional.of(apiRes);
+ }
+
+ /**
+ * Apply the given patch data.
+ *
+ * @param the generic type
+ * @param the generic type
+ * @param api the api
+ * @param existing the existing
+ * @param update the update
+ * @return the t
+ * @throws ApiException the api exception
+ */
+ @SuppressWarnings("PMD.GenericsNaming")
+ public static
+ T apply(GenericKubernetesApi api, T existing, String update)
+ throws ApiException {
+ PatchOptions opts = new PatchOptions();
+ opts.setForce(true);
+ opts.setFieldManager("kubernetes-java-kubectl-apply");
+ var response = api.patch(existing.getMetadata().getNamespace(),
+ existing.getMetadata().getName(), V1Patch.PATCH_FORMAT_APPLY_YAML,
+ new V1Patch(update), opts).throwsApiException();
+ return response.getObject();
+ }
+
+ /**
+ * Create an object reference.
+ *
+ * @param object the object
+ * @return the v 1 object reference
+ */
+ public static V1ObjectReference
+ objectReference(KubernetesObject object) {
+ return new V1ObjectReference().apiVersion(object.getApiVersion())
+ .kind(object.getKind())
+ .namespace(object.getMetadata().getNamespace())
+ .name(object.getMetadata().getName())
+ .resourceVersion(object.getMetadata().getResourceVersion())
+ .uid(object.getMetadata().getUid());
+ }
+
+ /**
+ * Creates an event related to the object, adding reasonable defaults.
+ *
+ * * If `kind` is not set, it is set to "Event".
+ * * If `metadata.namespace` is not set, it is set
+ * to the object's namespace.
+ * * If neither `metadata.name` nor `matadata.generateName` are set,
+ * set `generateName` to the object's name with a dash appended.
+ * * If `reportingInstance` is not set, set it to the object's name.
+ * * If `eventTime` is not set, set it to now.
+ * * If `type` is not set, set it to "Normal"
+ * * If `regarding` is not set, set it to the given object.
+ *
+ * @param client the client
+ * @param object the object
+ * @param event the event
+ * @throws ApiException the api exception
+ */
+ @SuppressWarnings("PMD.NPathComplexity")
+ public static void createEvent(ApiClient client,
+ KubernetesObject object, EventsV1Event event)
+ throws ApiException {
+ if (Strings.isNullOrEmpty(event.getKind())) {
+ event.kind("Event");
+ }
+ if (event.getMetadata() == null) {
+ event.metadata(new V1ObjectMeta());
+ }
+ if (Strings.isNullOrEmpty(event.getMetadata().getNamespace())) {
+ event.getMetadata().namespace(object.getMetadata().getNamespace());
+ }
+ if (Strings.isNullOrEmpty(event.getMetadata().getName())
+ && Strings.isNullOrEmpty(event.getMetadata().getGenerateName())) {
+ event.getMetadata()
+ .generateName(object.getMetadata().getName() + "-");
+ }
+ if (Strings.isNullOrEmpty(event.getReportingInstance())) {
+ event.reportingInstance(object.getMetadata().getName());
+ }
+ if (event.getEventTime() == null) {
+ event.eventTime(OffsetDateTime.now());
+ }
+ if (Strings.isNullOrEmpty(event.getType())) {
+ event.type("Normal");
+ }
+ if (event.getRegarding() == null) {
+ event.regarding(objectReference(object));
+ }
+ new EventsV1Api(client).createNamespacedEvent(
+ object.getMetadata().getNamespace(), event, null, null, null, null);
+ }
+}
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sClient.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sClient.java
new file mode 100644
index 0000000..272da2b
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sClient.java
@@ -0,0 +1,954 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2024 Michael N. Lipp
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.jdrupes.vmoperator.common;
+
+import io.kubernetes.client.openapi.ApiCallback;
+import io.kubernetes.client.openapi.ApiClient;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.ApiResponse;
+import io.kubernetes.client.openapi.JSON;
+import io.kubernetes.client.openapi.Pair;
+import io.kubernetes.client.openapi.auth.Authentication;
+import io.kubernetes.client.util.ClientBuilder;
+import io.kubernetes.client.util.generic.options.PatchOptions;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Type;
+import java.text.DateFormat;
+import java.time.format.DateTimeFormatter;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import javax.net.ssl.KeyManager;
+import okhttp3.Call;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Request.Builder;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+
+/**
+ * A client with some additional properties.
+ */
+@SuppressWarnings({ "PMD.ExcessivePublicCount", "PMD.TooManyMethods",
+ "checkstyle:LineLength", "PMD.CouplingBetweenObjects", "PMD.GodClass" })
+public class K8sClient extends ApiClient {
+
+ private ApiClient apiClient;
+ private PatchOptions defaultPatchOptions;
+
+ /**
+ * Instantiates a new client.
+ *
+ * @throws IOException Signals that an I/O exception has occurred.
+ */
+ public K8sClient() throws IOException {
+ defaultPatchOptions = new PatchOptions();
+ defaultPatchOptions.setFieldManager("kubernetes-java-kubectl-apply");
+ }
+
+ private ApiClient apiClient() {
+ if (apiClient == null) {
+ try {
+ apiClient = ClientBuilder.standard().build();
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ return apiClient;
+ }
+
+ /**
+ * Gets the default patch options.
+ *
+ * @return the defaultPatchOptions
+ */
+ public PatchOptions defaultPatchOptions() {
+ return defaultPatchOptions;
+ }
+
+ /**
+ * Changes the default patch options.
+ *
+ * @param patchOptions the patch options
+ * @return the client
+ */
+ public K8sClient with(PatchOptions patchOptions) {
+ defaultPatchOptions = patchOptions;
+ return this;
+ }
+
+ /**
+ * Gets the base path.
+ *
+ * @return the base path
+ * @see ApiClient#getBasePath()
+ */
+ @Override
+ public String getBasePath() {
+ return apiClient().getBasePath();
+ }
+
+ /**
+ * Sets the base path.
+ *
+ * @param basePath the base path
+ * @return the api client
+ * @see ApiClient#setBasePath(java.lang.String)
+ */
+ @Override
+ public ApiClient setBasePath(String basePath) {
+ return apiClient().setBasePath(basePath);
+ }
+
+ /**
+ * Gets the http client.
+ *
+ * @return the http client
+ * @see ApiClient#getHttpClient()
+ */
+ @Override
+ public OkHttpClient getHttpClient() {
+ return apiClient().getHttpClient();
+ }
+
+ /**
+ * Sets the http client.
+ *
+ * @param newHttpClient the new http client
+ * @return the api client
+ * @see ApiClient#setHttpClient(okhttp3.OkHttpClient)
+ */
+ @Override
+ public ApiClient setHttpClient(OkHttpClient newHttpClient) {
+ return apiClient().setHttpClient(newHttpClient);
+ }
+
+ /**
+ * Gets the json.
+ *
+ * @return the json
+ * @see ApiClient#getJSON()
+ */
+ @SuppressWarnings("abbreviationAsWordInName")
+ @Override
+ public JSON getJSON() {
+ return apiClient().getJSON();
+ }
+
+ /**
+ * Sets the JSON.
+ *
+ * @param json the json
+ * @return the api client
+ * @see ApiClient#setJSON(io.kubernetes.client.openapi.JSON)
+ */
+ @SuppressWarnings("abbreviationAsWordInName")
+ @Override
+ public ApiClient setJSON(JSON json) {
+ return apiClient().setJSON(json);
+ }
+
+ /**
+ * Checks if is verifying ssl.
+ *
+ * @return true, if is verifying ssl
+ * @see ApiClient#isVerifyingSsl()
+ */
+ @Override
+ public boolean isVerifyingSsl() {
+ return apiClient().isVerifyingSsl();
+ }
+
+ /**
+ * Sets the verifying ssl.
+ *
+ * @param verifyingSsl the verifying ssl
+ * @return the api client
+ * @see ApiClient#setVerifyingSsl(boolean)
+ */
+ @Override
+ public ApiClient setVerifyingSsl(boolean verifyingSsl) {
+ return apiClient().setVerifyingSsl(verifyingSsl);
+ }
+
+ /**
+ * Gets the ssl ca cert.
+ *
+ * @return the ssl ca cert
+ * @see ApiClient#getSslCaCert()
+ */
+ @Override
+ public InputStream getSslCaCert() {
+ return apiClient().getSslCaCert();
+ }
+
+ /**
+ * Sets the ssl ca cert.
+ *
+ * @param sslCaCert the ssl ca cert
+ * @return the api client
+ * @see ApiClient#setSslCaCert(java.io.InputStream)
+ */
+ @Override
+ public ApiClient setSslCaCert(InputStream sslCaCert) {
+ return apiClient().setSslCaCert(sslCaCert);
+ }
+
+ /**
+ * Gets the key managers.
+ *
+ * @return the key managers
+ * @see ApiClient#getKeyManagers()
+ */
+ @Override
+ public KeyManager[] getKeyManagers() {
+ return apiClient().getKeyManagers();
+ }
+
+ /**
+ * Sets the key managers.
+ *
+ * @param managers the managers
+ * @return the api client
+ * @see ApiClient#setKeyManagers(javax.net.ssl.KeyManager[])
+ */
+ @Override
+ public ApiClient setKeyManagers(KeyManager[] managers) {
+ return apiClient().setKeyManagers(managers);
+ }
+
+ /**
+ * Gets the date format.
+ *
+ * @return the date format
+ * @see ApiClient#getDateFormat()
+ */
+ @Override
+ public DateFormat getDateFormat() {
+ return apiClient().getDateFormat();
+ }
+
+ /**
+ * Sets the date format.
+ *
+ * @param dateFormat the date format
+ * @return the api client
+ * @see ApiClient#setDateFormat(java.text.DateFormat)
+ */
+ @Override
+ public ApiClient setDateFormat(DateFormat dateFormat) {
+ return apiClient().setDateFormat(dateFormat);
+ }
+
+ /**
+ * Sets the sql date format.
+ *
+ * @param dateFormat the date format
+ * @return the api client
+ * @see ApiClient#setSqlDateFormat(java.text.DateFormat)
+ */
+ @Override
+ public ApiClient setSqlDateFormat(DateFormat dateFormat) {
+ return apiClient().setSqlDateFormat(dateFormat);
+ }
+
+ /**
+ * Sets the offset date time format.
+ *
+ * @param dateFormat the date format
+ * @return the api client
+ * @see ApiClient#setOffsetDateTimeFormat(java.time.format.DateTimeFormatter)
+ */
+ @Override
+ public ApiClient setOffsetDateTimeFormat(DateTimeFormatter dateFormat) {
+ return apiClient().setOffsetDateTimeFormat(dateFormat);
+ }
+
+ /**
+ * Sets the local date format.
+ *
+ * @param dateFormat the date format
+ * @return the api client
+ * @see ApiClient#setLocalDateFormat(java.time.format.DateTimeFormatter)
+ */
+ @Override
+ public ApiClient setLocalDateFormat(DateTimeFormatter dateFormat) {
+ return apiClient().setLocalDateFormat(dateFormat);
+ }
+
+ /**
+ * Sets the lenient on json.
+ *
+ * @param lenientOnJson the lenient on json
+ * @return the api client
+ * @see ApiClient#setLenientOnJson(boolean)
+ */
+ @Override
+ public ApiClient setLenientOnJson(boolean lenientOnJson) {
+ return apiClient().setLenientOnJson(lenientOnJson);
+ }
+
+ /**
+ * Gets the authentications.
+ *
+ * @return the authentications
+ * @see ApiClient#getAuthentications()
+ */
+ @Override
+ public Map getAuthentications() {
+ return apiClient().getAuthentications();
+ }
+
+ /**
+ * Gets the authentication.
+ *
+ * @param authName the auth name
+ * @return the authentication
+ * @see ApiClient#getAuthentication(java.lang.String)
+ */
+ @Override
+ public Authentication getAuthentication(String authName) {
+ return apiClient().getAuthentication(authName);
+ }
+
+ /**
+ * Sets the username.
+ *
+ * @param username the new username
+ * @see ApiClient#setUsername(java.lang.String)
+ */
+ @Override
+ public void setUsername(String username) {
+ apiClient().setUsername(username);
+ }
+
+ /**
+ * Sets the password.
+ *
+ * @param password the new password
+ * @see ApiClient#setPassword(java.lang.String)
+ */
+ @Override
+ public void setPassword(String password) {
+ apiClient().setPassword(password);
+ }
+
+ /**
+ * Sets the api key.
+ *
+ * @param apiKey the new api key
+ * @see ApiClient#setApiKey(java.lang.String)
+ */
+ @Override
+ public void setApiKey(String apiKey) {
+ apiClient().setApiKey(apiKey);
+ }
+
+ /**
+ * Sets the api key prefix.
+ *
+ * @param apiKeyPrefix the new api key prefix
+ * @see ApiClient#setApiKeyPrefix(java.lang.String)
+ */
+ @Override
+ public void setApiKeyPrefix(String apiKeyPrefix) {
+ apiClient().setApiKeyPrefix(apiKeyPrefix);
+ }
+
+ /**
+ * Sets the access token.
+ *
+ * @param accessToken the new access token
+ * @see ApiClient#setAccessToken(java.lang.String)
+ */
+ @Override
+ public void setAccessToken(String accessToken) {
+ apiClient().setAccessToken(accessToken);
+ }
+
+ /**
+ * Sets the user agent.
+ *
+ * @param userAgent the user agent
+ * @return the api client
+ * @see ApiClient#setUserAgent(java.lang.String)
+ */
+ @Override
+ public ApiClient setUserAgent(String userAgent) {
+ return apiClient().setUserAgent(userAgent);
+ }
+
+ /**
+ * To string.
+ *
+ * @return the string
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return apiClient().toString();
+ }
+
+ /**
+ * Adds the default header.
+ *
+ * @param key the key
+ * @param value the value
+ * @return the api client
+ * @see ApiClient#addDefaultHeader(java.lang.String, java.lang.String)
+ */
+ @Override
+ public ApiClient addDefaultHeader(String key, String value) {
+ return apiClient().addDefaultHeader(key, value);
+ }
+
+ /**
+ * Adds the default cookie.
+ *
+ * @param key the key
+ * @param value the value
+ * @return the api client
+ * @see ApiClient#addDefaultCookie(java.lang.String, java.lang.String)
+ */
+ @Override
+ public ApiClient addDefaultCookie(String key, String value) {
+ return apiClient().addDefaultCookie(key, value);
+ }
+
+ /**
+ * Checks if is debugging.
+ *
+ * @return true, if is debugging
+ * @see ApiClient#isDebugging()
+ */
+ @Override
+ public boolean isDebugging() {
+ return apiClient().isDebugging();
+ }
+
+ /**
+ * Sets the debugging.
+ *
+ * @param debugging the debugging
+ * @return the api client
+ * @see ApiClient#setDebugging(boolean)
+ */
+ @Override
+ public ApiClient setDebugging(boolean debugging) {
+ return apiClient().setDebugging(debugging);
+ }
+
+ /**
+ * Gets the temp folder path.
+ *
+ * @return the temp folder path
+ * @see ApiClient#getTempFolderPath()
+ */
+ @Override
+ public String getTempFolderPath() {
+ return apiClient().getTempFolderPath();
+ }
+
+ /**
+ * Sets the temp folder path.
+ *
+ * @param tempFolderPath the temp folder path
+ * @return the api client
+ * @see ApiClient#setTempFolderPath(java.lang.String)
+ */
+ @Override
+ public ApiClient setTempFolderPath(String tempFolderPath) {
+ return apiClient().setTempFolderPath(tempFolderPath);
+ }
+
+ /**
+ * Gets the connect timeout.
+ *
+ * @return the connect timeout
+ * @see ApiClient#getConnectTimeout()
+ */
+ @Override
+ public int getConnectTimeout() {
+ return apiClient().getConnectTimeout();
+ }
+
+ /**
+ * Sets the connect timeout.
+ *
+ * @param connectionTimeout the connection timeout
+ * @return the api client
+ * @see ApiClient#setConnectTimeout(int)
+ */
+ @Override
+ public ApiClient setConnectTimeout(int connectionTimeout) {
+ return apiClient().setConnectTimeout(connectionTimeout);
+ }
+
+ /**
+ * Gets the read timeout.
+ *
+ * @return the read timeout
+ * @see ApiClient#getReadTimeout()
+ */
+ @Override
+ public int getReadTimeout() {
+ return apiClient().getReadTimeout();
+ }
+
+ /**
+ * Sets the read timeout.
+ *
+ * @param readTimeout the read timeout
+ * @return the api client
+ * @see ApiClient#setReadTimeout(int)
+ */
+ @Override
+ public ApiClient setReadTimeout(int readTimeout) {
+ return apiClient().setReadTimeout(readTimeout);
+ }
+
+ /**
+ * Gets the write timeout.
+ *
+ * @return the write timeout
+ * @see ApiClient#getWriteTimeout()
+ */
+ @Override
+ public int getWriteTimeout() {
+ return apiClient().getWriteTimeout();
+ }
+
+ /**
+ * Sets the write timeout.
+ *
+ * @param writeTimeout the write timeout
+ * @return the api client
+ * @see ApiClient#setWriteTimeout(int)
+ */
+ @Override
+ public ApiClient setWriteTimeout(int writeTimeout) {
+ return apiClient().setWriteTimeout(writeTimeout);
+ }
+
+ /**
+ * Parameter to string.
+ *
+ * @param param the param
+ * @return the string
+ * @see ApiClient#parameterToString(java.lang.Object)
+ */
+ @Override
+ public String parameterToString(Object param) {
+ return apiClient().parameterToString(param);
+ }
+
+ /**
+ * Parameter to pair.
+ *
+ * @param name the name
+ * @param value the value
+ * @return the list
+ * @see ApiClient#parameterToPair(java.lang.String, java.lang.Object)
+ */
+ @Override
+ public List parameterToPair(String name, Object value) {
+ return apiClient().parameterToPair(name, value);
+ }
+
+ /**
+ * Parameter to pairs.
+ *
+ * @param collectionFormat the collection format
+ * @param name the name
+ * @param value the value
+ * @return the list
+ * @see ApiClient#parameterToPairs(java.lang.String, java.lang.String, java.util.Collection)
+ */
+ @SuppressWarnings({ "rawtypes", "PMD.AvoidDuplicateLiterals" })
+ @Override
+ public List parameterToPairs(String collectionFormat, String name,
+ Collection value) {
+ return apiClient().parameterToPairs(collectionFormat, name, value);
+ }
+
+ /**
+ * Collection path parameter to string.
+ *
+ * @param collectionFormat the collection format
+ * @param value the value
+ * @return the string
+ * @see ApiClient#collectionPathParameterToString(java.lang.String, java.util.Collection)
+ */
+ @SuppressWarnings("rawtypes")
+ @Override
+ public String collectionPathParameterToString(String collectionFormat,
+ Collection value) {
+ return apiClient().collectionPathParameterToString(collectionFormat,
+ value);
+ }
+
+ /**
+ * Sanitize filename.
+ *
+ * @param filename the filename
+ * @return the string
+ * @see ApiClient#sanitizeFilename(java.lang.String)
+ */
+ @Override
+ public String sanitizeFilename(String filename) {
+ return apiClient().sanitizeFilename(filename);
+ }
+
+ /**
+ * Checks if is json mime.
+ *
+ * @param mime the mime
+ * @return true, if is json mime
+ * @see ApiClient#isJsonMime(java.lang.String)
+ */
+ @Override
+ public boolean isJsonMime(String mime) {
+ return apiClient().isJsonMime(mime);
+ }
+
+ /**
+ * Select header accept.
+ *
+ * @param accepts the accepts
+ * @return the string
+ * @see ApiClient#selectHeaderAccept(java.lang.String[])
+ */
+ @Override
+ public String selectHeaderAccept(String[] accepts) {
+ return apiClient().selectHeaderAccept(accepts);
+ }
+
+ /**
+ * Select header content type.
+ *
+ * @param contentTypes the content types
+ * @return the string
+ * @see ApiClient#selectHeaderContentType(java.lang.String[])
+ */
+ @Override
+ public String selectHeaderContentType(String[] contentTypes) {
+ return apiClient().selectHeaderContentType(contentTypes);
+ }
+
+ /**
+ * Escape string.
+ *
+ * @param str the str
+ * @return the string
+ * @see ApiClient#escapeString(java.lang.String)
+ */
+ @Override
+ public String escapeString(String str) {
+ return apiClient().escapeString(str);
+ }
+
+ /**
+ * Deserialize.
+ *
+ * @param the generic type
+ * @param response the response
+ * @param returnType the return type
+ * @return the t
+ * @throws ApiException the api exception
+ * @see ApiClient#deserialize(okhttp3.Response, java.lang.reflect.Type)
+ */
+ @Override
+ public T deserialize(Response response, Type returnType)
+ throws ApiException {
+ return apiClient().deserialize(response, returnType);
+ }
+
+ /**
+ * Serialize.
+ *
+ * @param obj the obj
+ * @param contentType the content type
+ * @return the request body
+ * @throws ApiException the api exception
+ * @see ApiClient#serialize(java.lang.Object, java.lang.String)
+ */
+ @Override
+ public RequestBody serialize(Object obj, String contentType)
+ throws ApiException {
+ return apiClient().serialize(obj, contentType);
+ }
+
+ /**
+ * Download file from response.
+ *
+ * @param response the response
+ * @return the file
+ * @throws ApiException the api exception
+ * @see ApiClient#downloadFileFromResponse(okhttp3.Response)
+ */
+ @Override
+ public File downloadFileFromResponse(Response response)
+ throws ApiException {
+ return apiClient().downloadFileFromResponse(response);
+ }
+
+ /**
+ * Prepare download file.
+ *
+ * @param response the response
+ * @return the file
+ * @throws IOException Signals that an I/O exception has occurred.
+ * @see ApiClient#prepareDownloadFile(okhttp3.Response)
+ */
+ @Override
+ public File prepareDownloadFile(Response response) throws IOException {
+ return apiClient().prepareDownloadFile(response);
+ }
+
+ /**
+ * Execute.
+ *
+ * @param the generic type
+ * @param call the call
+ * @return the api response
+ * @throws ApiException the api exception
+ * @see ApiClient#execute(okhttp3.Call)
+ */
+ @Override
+ public ApiResponse execute(Call call) throws ApiException {
+ return apiClient().execute(call);
+ }
+
+ /**
+ * Execute.
+ *
+ * @param the generic type
+ * @param call the call
+ * @param returnType the return type
+ * @return the api response
+ * @throws ApiException the api exception
+ * @see ApiClient#execute(okhttp3.Call, java.lang.reflect.Type)
+ */
+ @Override
+ public ApiResponse execute(Call call, Type returnType)
+ throws ApiException {
+ return apiClient().execute(call, returnType);
+ }
+
+ /**
+ * Execute async.
+ *
+ * @param the generic type
+ * @param call the call
+ * @param callback the callback
+ * @see ApiClient#executeAsync(okhttp3.Call, io.kubernetes.client.openapi.ApiCallback)
+ */
+ @Override
+ public void executeAsync(Call call, ApiCallback callback) {
+ apiClient().executeAsync(call, callback);
+ }
+
+ /**
+ * Execute async.
+ *
+ * @param the generic type
+ * @param call the call
+ * @param returnType the return type
+ * @param callback the callback
+ * @see ApiClient#executeAsync(okhttp3.Call, java.lang.reflect.Type, io.kubernetes.client.openapi.ApiCallback)
+ */
+ @Override
+ public void executeAsync(Call call, Type returnType,
+ ApiCallback callback) {
+ apiClient().executeAsync(call, returnType, callback);
+ }
+
+ /**
+ * Handle response.
+ *
+ * @param the generic type
+ * @param response the response
+ * @param returnType the return type
+ * @return the t
+ * @throws ApiException the api exception
+ * @see ApiClient#handleResponse(okhttp3.Response, java.lang.reflect.Type)
+ */
+ @Override
+ public T handleResponse(Response response, Type returnType)
+ throws ApiException {
+ return apiClient().handleResponse(response, returnType);
+ }
+
+ /**
+ * Builds the call.
+ *
+ * @param path the path
+ * @param method the method
+ * @param queryParams the query params
+ * @param collectionQueryParams the collection query params
+ * @param body the body
+ * @param headerParams the header params
+ * @param cookieParams the cookie params
+ * @param formParams the form params
+ * @param authNames the auth names
+ * @param callback the callback
+ * @return the call
+ * @throws ApiException the api exception
+ * @see ApiClient#buildCall(java.lang.String, java.lang.String, java.util.List, java.util.List, java.lang.Object, java.util.Map, java.util.Map, java.util.Map, java.lang.String[], io.kubernetes.client.openapi.ApiCallback)
+ */
+ @SuppressWarnings({ "rawtypes" })
+ @Override
+ public Call buildCall(String path, String method, List queryParams,
+ List collectionQueryParams, Object body,
+ Map headerParams, Map cookieParams,
+ Map formParams, String[] authNames,
+ ApiCallback callback) throws ApiException {
+ return apiClient().buildCall(path, method, queryParams,
+ collectionQueryParams, body, headerParams, cookieParams, formParams,
+ authNames, callback);
+ }
+
+ /**
+ * Builds the request.
+ *
+ * @param path the path
+ * @param method the method
+ * @param queryParams the query params
+ * @param collectionQueryParams the collection query params
+ * @param body the body
+ * @param headerParams the header params
+ * @param cookieParams the cookie params
+ * @param formParams the form params
+ * @param authNames the auth names
+ * @param callback the callback
+ * @return the request
+ * @throws ApiException the api exception
+ * @see ApiClient#buildRequest(java.lang.String, java.lang.String, java.util.List, java.util.List, java.lang.Object, java.util.Map, java.util.Map, java.util.Map, java.lang.String[], io.kubernetes.client.openapi.ApiCallback)
+ */
+ @SuppressWarnings({ "rawtypes" })
+ @Override
+ public Request buildRequest(String path, String method,
+ List queryParams, List collectionQueryParams,
+ Object body, Map headerParams,
+ Map cookieParams, Map formParams,
+ String[] authNames, ApiCallback callback) throws ApiException {
+ return apiClient().buildRequest(path, method, queryParams,
+ collectionQueryParams, body, headerParams, cookieParams, formParams,
+ authNames, callback);
+ }
+
+ /**
+ * Builds the url.
+ *
+ * @param path the path
+ * @param queryParams the query params
+ * @param collectionQueryParams the collection query params
+ * @return the string
+ * @see ApiClient#buildUrl(java.lang.String, java.util.List, java.util.List)
+ */
+ @Override
+ public String buildUrl(String path, List queryParams,
+ List collectionQueryParams) {
+ return apiClient().buildUrl(path, queryParams, collectionQueryParams);
+ }
+
+ /**
+ * Process header params.
+ *
+ * @param headerParams the header params
+ * @param reqBuilder the req builder
+ * @see ApiClient#processHeaderParams(java.util.Map, okhttp3.Request.Builder)
+ */
+ @Override
+ public void processHeaderParams(Map headerParams,
+ Builder reqBuilder) {
+ apiClient().processHeaderParams(headerParams, reqBuilder);
+ }
+
+ /**
+ * Process cookie params.
+ *
+ * @param cookieParams the cookie params
+ * @param reqBuilder the req builder
+ * @see ApiClient#processCookieParams(java.util.Map, okhttp3.Request.Builder)
+ */
+ @Override
+ public void processCookieParams(Map cookieParams,
+ Builder reqBuilder) {
+ apiClient().processCookieParams(cookieParams, reqBuilder);
+ }
+
+ /**
+ * Update params for auth.
+ *
+ * @param authNames the auth names
+ * @param queryParams the query params
+ * @param headerParams the header params
+ * @param cookieParams the cookie params
+ * @see ApiClient#updateParamsForAuth(java.lang.String[], java.util.List, java.util.Map, java.util.Map)
+ */
+ @Override
+ public void updateParamsForAuth(String[] authNames, List queryParams,
+ Map headerParams,
+ Map cookieParams) {
+ apiClient().updateParamsForAuth(authNames, queryParams, headerParams,
+ cookieParams);
+ }
+
+ /**
+ * Builds the request body form encoding.
+ *
+ * @param formParams the form params
+ * @return the request body
+ * @see ApiClient#buildRequestBodyFormEncoding(java.util.Map)
+ */
+ @Override
+ public RequestBody
+ buildRequestBodyFormEncoding(Map formParams) {
+ return apiClient().buildRequestBodyFormEncoding(formParams);
+ }
+
+ /**
+ * Builds the request body multipart.
+ *
+ * @param formParams the form params
+ * @return the request body
+ * @see ApiClient#buildRequestBodyMultipart(java.util.Map)
+ */
+ @Override
+ public RequestBody
+ buildRequestBodyMultipart(Map formParams) {
+ return apiClient().buildRequestBodyMultipart(formParams);
+ }
+
+ /**
+ * Guess content type from file.
+ *
+ * @param file the file
+ * @return the string
+ * @see ApiClient#guessContentTypeFromFile(java.io.File)
+ */
+ @Override
+ public String guessContentTypeFromFile(File file) {
+ return apiClient().guessContentTypeFromFile(file);
+ }
+
+}
\ No newline at end of file
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sClusterGenericStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sClusterGenericStub.java
new file mode 100644
index 0000000..59b4d12
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sClusterGenericStub.java
@@ -0,0 +1,396 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2024 Michael N. Lipp
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.jdrupes.vmoperator.common;
+
+import io.kubernetes.client.Discovery.APIResource;
+import io.kubernetes.client.apimachinery.GroupVersionKind;
+import io.kubernetes.client.common.KubernetesListObject;
+import io.kubernetes.client.common.KubernetesObject;
+import io.kubernetes.client.custom.V1Patch;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.util.Strings;
+import io.kubernetes.client.util.generic.GenericKubernetesApi;
+import io.kubernetes.client.util.generic.options.GetOptions;
+import io.kubernetes.client.util.generic.options.ListOptions;
+import io.kubernetes.client.util.generic.options.PatchOptions;
+import java.net.HttpURLConnection;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+
+/**
+ * A stub for cluster scoped objects. This stub provides the
+ * functions common to all Kubernetes objects, but uses variables
+ * for all types. This class should be used as base class only.
+ *
+ * @param the generic type
+ * @param the generic type
+ */
+@SuppressWarnings({ "PMD.CouplingBetweenObjects" })
+public class K8sClusterGenericStub {
+ protected final K8sClient client;
+ private final GenericKubernetesApi api;
+ protected final APIResource context;
+ protected final String name;
+
+ /**
+ * Instantiates a new stub for the object specified. If the object
+ * exists in the context specified, the version (see
+ * {@link #version()} is bound to the existing object's version.
+ * Else the stub is dangling with the version set to the context's
+ * preferred version.
+ *
+ * @param objectClass the object class
+ * @param objectListClass the object list class
+ * @param client the client
+ * @param context the context
+ * @param name the name
+ */
+ @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
+ protected K8sClusterGenericStub(Class objectClass,
+ Class objectListClass, K8sClient client, APIResource context,
+ String name) {
+ this.client = client;
+ this.name = name;
+
+ // Bind version
+ var foundVersion = context.getPreferredVersion();
+ GenericKubernetesApi testApi = null;
+ GetOptions mdOpts
+ = new GetOptions().isPartialObjectMetadataRequest(true);
+ for (var version : candidateVersions(context)) {
+ testApi = new GenericKubernetesApi<>(objectClass, objectListClass,
+ context.getGroup(), version, context.getResourcePlural(),
+ client);
+ if (testApi.get(name, mdOpts).isSuccess()) {
+ foundVersion = version;
+ break;
+ }
+ }
+ if (foundVersion.equals(context.getPreferredVersion())) {
+ this.context = context;
+ } else {
+ this.context = K8s.preferred(context, foundVersion);
+ }
+
+ api = Optional.ofNullable(testApi)
+ .orElseGet(() -> new GenericKubernetesApi<>(objectClass,
+ objectListClass, group(), version(), plural(), client));
+ }
+
+ /**
+ * Gets the context.
+ *
+ * @return the context
+ */
+ public APIResource context() {
+ return context;
+ }
+
+ /**
+ * Gets the group.
+ *
+ * @return the group
+ */
+ public String group() {
+ return context.getGroup();
+ }
+
+ /**
+ * Gets the version.
+ *
+ * @return the version
+ */
+ public String version() {
+ return context.getPreferredVersion();
+ }
+
+ /**
+ * Gets the kind.
+ *
+ * @return the kind
+ */
+ public String kind() {
+ return context.getKind();
+ }
+
+ /**
+ * Gets the plural.
+ *
+ * @return the plural
+ */
+ public String plural() {
+ return context.getResourcePlural();
+ }
+
+ /**
+ * Gets the name.
+ *
+ * @return the name
+ */
+ public String name() {
+ return name;
+ }
+
+ /**
+ * Delete the Kubernetes object.
+ *
+ * @throws ApiException the API exception
+ */
+ public void delete() throws ApiException {
+ var result = api.delete(name);
+ if (result.isSuccess()
+ || result.getHttpStatusCode() == HttpURLConnection.HTTP_NOT_FOUND) {
+ return;
+ }
+ result.throwsApiException();
+ }
+
+ /**
+ * Retrieves and returns the current state of the object.
+ *
+ * @return the object's state
+ * @throws ApiException the api exception
+ */
+ public Optional model() throws ApiException {
+ return K8s.optional(api.get(name));
+ }
+
+ /**
+ * Updates the object's status.
+ *
+ * @param object the current state of the object (passed to `status`)
+ * @param status function that returns the new status
+ * @return the updated model or empty if not successful
+ * @throws ApiException the api exception
+ */
+ public Optional updateStatus(O object,
+ Function status) throws ApiException {
+ return K8s.optional(api.updateStatus(object, status));
+ }
+
+ /**
+ * Updates the status.
+ *
+ * @param status the status
+ * @return the kubernetes api response
+ * the updated model or empty if not successful
+ * @throws ApiException the api exception
+ */
+ public Optional updateStatus(Function status)
+ throws ApiException {
+ return updateStatus(api.get(name).throwsApiException().getObject(),
+ status);
+ }
+
+ /**
+ * Patch the object.
+ *
+ * @param patchType the patch type
+ * @param patch the patch
+ * @param options the options
+ * @return the kubernetes api response
+ * @throws ApiException the api exception
+ */
+ public Optional patch(String patchType, V1Patch patch,
+ PatchOptions options) throws ApiException {
+ return K8s
+ .optional(api.patch(name, patchType, patch, options));
+ }
+
+ /**
+ * Patch the object using default options.
+ *
+ * @param patchType the patch type
+ * @param patch the patch
+ * @return the kubernetes api response
+ * @throws ApiException the api exception
+ */
+ public Optional
+ patch(String patchType, V1Patch patch) throws ApiException {
+ PatchOptions opts = new PatchOptions();
+ return patch(patchType, patch, opts);
+ }
+
+ /**
+ * A supplier for generic stubs.
+ *
+ * @param the object type
+ * @param the object list type
+ * @param the result type
+ */
+ @FunctionalInterface
+ public interface GenericSupplier> {
+
+ /**
+ * Gets a new stub.
+ *
+ * @param objectClass the object class
+ * @param objectListClass the object list class
+ * @param client the client
+ * @param context the API resource
+ * @param name the name
+ * @return the result
+ */
+ R get(Class objectClass, Class objectListClass, K8sClient client,
+ APIResource context, String name);
+ }
+
+ @Override
+ @SuppressWarnings("PMD.UseLocaleWithCaseConversions")
+ public String toString() {
+ return (Strings.isNullOrEmpty(group()) ? "" : group() + "/")
+ + version().toUpperCase() + kind() + " " + name;
+ }
+
+ /**
+ * Get an object stub. If the version in parameter
+ * `gvk` is an empty string, the stub refers to the first object
+ * found with matching group and kind.
+ *
+ * @param the object type
+ * @param the object list type
+ * @param the stub type
+ * @param objectClass the object class
+ * @param objectListClass the object list class
+ * @param client the client
+ * @param gvk the group, version and kind
+ * @param name the name
+ * @param provider the provider
+ * @return the stub if the object exists
+ * @throws ApiException the api exception
+ */
+ public static >
+ R get(Class objectClass, Class objectListClass,
+ K8sClient client, GroupVersionKind gvk, String name,
+ GenericSupplier provider) throws ApiException {
+ var context = K8s.context(client, gvk.getGroup(), gvk.getVersion(),
+ gvk.getKind());
+ if (context.isEmpty()) {
+ throw new ApiException("No known API for " + gvk.getGroup()
+ + "/" + gvk.getVersion() + " " + gvk.getKind());
+ }
+ return provider.get(objectClass, objectListClass, client, context.get(),
+ name);
+ }
+
+ /**
+ * Get an object stub.
+ *
+ * @param the object type
+ * @param the object list type
+ * @param the stub type
+ * @param objectClass the object class
+ * @param objectListClass the object list class
+ * @param client the client
+ * @param context the context
+ * @param name the name
+ * @param provider the provider
+ * @return the stub if the object exists
+ * @throws ApiException the api exception
+ */
+ public static >
+ R get(Class objectClass, Class objectListClass,
+ K8sClient client, APIResource context, String name,
+ GenericSupplier provider) {
+ return provider.get(objectClass, objectListClass, client, context,
+ name);
+ }
+
+ /**
+ * Get an object stub for a newly created object.
+ *
+ * @param the object type
+ * @param the object list type
+ * @param the stub type
+ * @param objectClass the object class
+ * @param objectListClass the object list class
+ * @param client the client
+ * @param context the context
+ * @param model the model
+ * @param provider the provider
+ * @return the stub if the object exists
+ * @throws ApiException the api exception
+ */
+ public static >
+ R create(Class objectClass, Class objectListClass,
+ K8sClient client, APIResource context, O model,
+ GenericSupplier provider) throws ApiException {
+ var api = new GenericKubernetesApi<>(objectClass, objectListClass,
+ context.getGroup(), context.getPreferredVersion(),
+ context.getResourcePlural(), client);
+ api.create(model).throwsApiException();
+ return provider.get(objectClass, objectListClass, client,
+ context, model.getMetadata().getName());
+ }
+
+ /**
+ * Get the stubs for the objects that match
+ * the criteria from the given options.
+ *
+ * @param the object type
+ * @param the object list type
+ * @param the stub type
+ * @param objectClass the object class
+ * @param objectListClass the object list class
+ * @param client the client
+ * @param context the context
+ * @param options the options
+ * @param provider the provider
+ * @return the collection
+ * @throws ApiException the api exception
+ */
+ public static >
+ Collection list(Class objectClass, Class objectListClass,
+ K8sClient client, APIResource context,
+ ListOptions options, GenericSupplier provider)
+ throws ApiException {
+ var result = new ArrayList();
+ for (var version : candidateVersions(context)) {
+ @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
+ var api = new GenericKubernetesApi<>(objectClass, objectListClass,
+ context.getGroup(), version, context.getResourcePlural(),
+ client);
+ var objs = api.list(options).throwsApiException();
+ for (var item : objs.getObject().getItems()) {
+ result.add(provider.get(objectClass, objectListClass, client,
+ context, item.getMetadata().getName()));
+ }
+ }
+ return result;
+ }
+
+ private static List candidateVersions(APIResource context) {
+ var result = new LinkedList<>(context.getVersions());
+ result.remove(context.getPreferredVersion());
+ result.add(0, context.getPreferredVersion());
+ return result;
+ }
+
+}
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModel.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModel.java
new file mode 100644
index 0000000..2392d3e
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModel.java
@@ -0,0 +1,113 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2024 Michael N. Lipp
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.jdrupes.vmoperator.common;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import io.kubernetes.client.common.KubernetesObject;
+import io.kubernetes.client.openapi.models.V1ObjectMeta;
+
+/**
+ * Represents a Kubernetes object using a JSON data structure.
+ * Some information that is common to all Kubernetes objects,
+ * notably the metadata, is made available through the methods
+ * defined by {@link KubernetesObject}.
+ */
+public class K8sDynamicModel implements KubernetesObject {
+
+ private final V1ObjectMeta metadata;
+ private final JsonObject data;
+
+ /**
+ * Instantiates a new model from the JSON representation.
+ *
+ * @param delegate the gson instance to use for extracting structured data
+ * @param json the JSON
+ */
+ public K8sDynamicModel(Gson delegate, JsonObject json) {
+ this.data = json;
+ metadata = delegate.fromJson(data.get("metadata"), V1ObjectMeta.class);
+ }
+
+ @Override
+ public String getApiVersion() {
+ return apiVersion();
+ }
+
+ /**
+ * Gets the API version. (Abbreviated method name for convenience.)
+ *
+ * @return the API version
+ */
+ public String apiVersion() {
+ return data.get("apiVersion").getAsString();
+ }
+
+ @Override
+ public String getKind() {
+ return kind();
+ }
+
+ /**
+ * Gets the kind. (Abbreviated method name for convenience.)
+ *
+ * @return the kind
+ */
+ public String kind() {
+ return data.get("kind").getAsString();
+ }
+
+ @Override
+ public V1ObjectMeta getMetadata() {
+ return metadata;
+ }
+
+ /**
+ * Gets the metadata. (Abbreviated method name for convenience.)
+ *
+ * @return the metadata
+ */
+ public V1ObjectMeta metadata() {
+ return metadata;
+ }
+
+ /**
+ * Gets the data.
+ *
+ * @return the data
+ */
+ public JsonObject data() {
+ return data;
+ }
+
+ /**
+ * Convenience method for getting the status.
+ *
+ * @return the JSON object describing the status
+ */
+ public JsonObject statusJson() {
+ return data.getAsJsonObject("status");
+ }
+
+ @Override
+ public String toString() {
+ return data.toString();
+ }
+
+}
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModels.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModels.java
new file mode 100644
index 0000000..d165c10
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModels.java
@@ -0,0 +1,44 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2024 Michael N. Lipp
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.jdrupes.vmoperator.common;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import io.kubernetes.client.common.KubernetesListObject;
+
+/**
+ * Represents a list of Kubernetes objects each of which is
+ * represented using a JSON data structure.
+ * Some information that is common to all Kubernetes objects,
+ * notably the metadata, is made available through the methods
+ * defined by {@link KubernetesListObject}.
+ */
+public class K8sDynamicModels extends K8sDynamicModelsBase {
+
+ /**
+ * Initialize the object list using the given JSON data.
+ *
+ * @param delegate the gson instance to use for extracting structured data
+ * @param data the data
+ */
+ public K8sDynamicModels(Gson delegate, JsonObject data) {
+ super(K8sDynamicModel.class, delegate, data);
+ }
+
+}
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModelsBase.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModelsBase.java
new file mode 100644
index 0000000..4e21c0e
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModelsBase.java
@@ -0,0 +1,174 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2024 Michael N. Lipp
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.jdrupes.vmoperator.common;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import io.kubernetes.client.common.KubernetesListObject;
+import io.kubernetes.client.openapi.Configuration;
+import io.kubernetes.client.openapi.models.V1ListMeta;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents a list of Kubernetes objects each of which is
+ * represented using a JSON data structure.
+ * Some information that is common to all Kubernetes objects,
+ * notably the metadata, is made available through the methods
+ * defined by {@link KubernetesListObject}.
+ */
+public class K8sDynamicModelsBase
+ implements KubernetesListObject {
+
+ private final JsonObject data;
+ private final V1ListMeta metadata;
+ private final List items;
+
+ /**
+ * Initialize the object list using the given JSON data.
+ *
+ * @param itemClass the item class
+ * @param delegate the gson instance to use for extracting structured data
+ * @param data the data
+ */
+ public K8sDynamicModelsBase(Class itemClass, Gson delegate,
+ JsonObject data) {
+ this.data = data;
+ metadata = delegate.fromJson(data.get("metadata"), V1ListMeta.class);
+ items = new ArrayList<>();
+ for (JsonElement e : data.get("items").getAsJsonArray()) {
+ try {
+ items.add(itemClass.getConstructor(Gson.class, JsonObject.class)
+ .newInstance(delegate, e.getAsJsonObject()));
+ } catch (InstantiationException | IllegalAccessException
+ | IllegalArgumentException | InvocationTargetException
+ | NoSuchMethodException | SecurityException exc) {
+ throw new IllegalArgumentException(exc);
+ }
+ }
+ }
+
+ @Override
+ public String getApiVersion() {
+ return apiVersion();
+ }
+
+ /**
+ * Gets the API version. (Abbreviated method name for convenience.)
+ *
+ * @return the API version
+ */
+ public String apiVersion() {
+ return data.get("apiVersion").getAsString();
+ }
+
+ @Override
+ public String getKind() {
+ return kind();
+ }
+
+ /**
+ * Gets the kind. (Abbreviated method name for convenience.)
+ *
+ * @return the kind
+ */
+ public String kind() {
+ return data.get("kind").getAsString();
+ }
+
+ @Override
+ public V1ListMeta getMetadata() {
+ return metadata;
+ }
+
+ /**
+ * Gets the metadata. (Abbreviated method name for convenience.)
+ *
+ * @return the metadata
+ */
+ public V1ListMeta metadata() {
+ return metadata;
+ }
+
+ /**
+ * Returns the JSON representation of this object.
+ *
+ * @return the JOSN representation
+ */
+ public JsonObject data() {
+ return data;
+ }
+
+ @Override
+ public List getItems() {
+ return items;
+ }
+
+ /**
+ * Sets the api version.
+ *
+ * @param apiVersion the new api version
+ */
+ public void setApiVersion(String apiVersion) {
+ data.addProperty("apiVersion", apiVersion);
+ }
+
+ /**
+ * Sets the kind.
+ *
+ * @param kind the new kind
+ */
+ public void setKind(String kind) {
+ data.addProperty("kind", kind);
+ }
+
+ /**
+ * Sets the metadata.
+ *
+ * @param objectMeta the new metadata
+ */
+ public void setMetadata(V1ListMeta objectMeta) {
+ data.add("metadata",
+ Configuration.getDefaultApiClient().getJSON().getGson()
+ .toJsonTree(objectMeta));
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(data);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ K8sDynamicModelsBase> other = (K8sDynamicModelsBase>) obj;
+ return Objects.equals(data, other.data);
+ }
+}
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicStub.java
new file mode 100644
index 0000000..c0303c2
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicStub.java
@@ -0,0 +1,152 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2024 Michael N. Lipp
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.jdrupes.vmoperator.common;
+
+import io.kubernetes.client.Discovery.APIResource;
+import io.kubernetes.client.apimachinery.GroupVersionKind;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.util.generic.options.ListOptions;
+import java.io.Reader;
+import java.util.Collection;
+
+/**
+ * A stub for namespaced custom objects. It uses a dynamic model
+ * (see {@link K8sDynamicModel}) for representing the object's
+ * state and can therefore be used for any kind of object, especially
+ * custom objects.
+ */
+public class K8sDynamicStub
+ extends K8sDynamicStubBase {
+
+ private static DynamicTypeAdapterFactory taf = new K8sDynamicModelTypeAdapterFactory();
+
+ /**
+ * Instantiates a new dynamic stub.
+ *
+ * @param client the client
+ * @param context the context
+ * @param namespace the namespace
+ * @param name the name
+ */
+ public K8sDynamicStub(K8sClient client,
+ APIResource context, String namespace, String name) {
+ super(K8sDynamicModel.class, K8sDynamicModels.class, taf, client,
+ context, namespace, name);
+ }
+
+ /**
+ * Get a dynamic object stub. If the version in parameter
+ * `gvk` is an empty string, the stub refers to the first object with
+ * matching group and kind.
+ *
+ * @param client the client
+ * @param gvk the group, version and kind
+ * @param namespace the namespace
+ * @param name the name
+ * @return the stub if the object exists
+ * @throws ApiException the api exception
+ */
+ public static K8sDynamicStub get(K8sClient client,
+ GroupVersionKind gvk, String namespace, String name)
+ throws ApiException {
+ return new K8sDynamicStub(client, apiResource(client, gvk), namespace,
+ name);
+ }
+
+ /**
+ * Get a dynamic object stub.
+ *
+ * @param client the client
+ * @param context the context
+ * @param namespace the namespace
+ * @param name the name
+ * @return the stub if the object exists
+ * @throws ApiException the api exception
+ */
+ public static K8sDynamicStub get(K8sClient client,
+ APIResource context, String namespace, String name) {
+ return new K8sDynamicStub(client, context, namespace, name);
+ }
+
+ /**
+ * Creates a stub from yaml.
+ *
+ * @param client the client
+ * @param context the context
+ * @param yaml the yaml
+ * @return the k 8 s dynamic stub
+ * @throws ApiException the api exception
+ */
+ public static K8sDynamicStub createFromYaml(K8sClient client,
+ APIResource context, Reader yaml) throws ApiException {
+ var model = new K8sDynamicModel(client.getJSON().getGson(),
+ K8s.yamlToJson(client, yaml));
+ return K8sGenericStub.create(K8sDynamicModel.class,
+ K8sDynamicModels.class, client, context, model,
+ (c, ns, n) -> new K8sDynamicStub(c, context, ns, n));
+ }
+
+ /**
+ * Get the stubs for the objects in the given namespace that match
+ * the criteria from the given options.
+ *
+ * @param client the client
+ * @param namespace the namespace
+ * @param options the options
+ * @return the collection
+ * @throws ApiException the api exception
+ */
+ public static Collection list(K8sClient client,
+ APIResource context, String namespace, ListOptions options)
+ throws ApiException {
+ return K8sGenericStub.list(K8sDynamicModel.class,
+ K8sDynamicModels.class, client, context, namespace, options,
+ (c, ns, n) -> new K8sDynamicStub(c, context, ns, n));
+ }
+
+ /**
+ * Get the stubs for the objects in the given namespace.
+ *
+ * @param client the client
+ * @param namespace the namespace
+ * @return the collection
+ * @throws ApiException the api exception
+ */
+ public static Collection list(K8sClient client,
+ APIResource context, String namespace)
+ throws ApiException {
+ return list(client, context, namespace, new ListOptions());
+ }
+
+ /**
+ * A factory for creating K8sDynamicModel(s) objects.
+ */
+ public static class K8sDynamicModelTypeAdapterFactory extends
+ DynamicTypeAdapterFactory {
+
+ /**
+ * Instantiates a new dynamic model type adapter factory.
+ */
+ public K8sDynamicModelTypeAdapterFactory() {
+ super(K8sDynamicModel.class, K8sDynamicModels.class);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicStubBase.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicStubBase.java
new file mode 100644
index 0000000..ae3f012
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicStubBase.java
@@ -0,0 +1,49 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2024 Michael N. Lipp
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.jdrupes.vmoperator.common;
+
+import io.kubernetes.client.Discovery.APIResource;
+
+/**
+ * A stub for namespaced custom objects. It uses a dynamic model
+ * (see {@link K8sDynamicModel}) for representing the object's
+ * state and can therefore be used for any kind of object, especially
+ * custom objects.
+ */
+public abstract class K8sDynamicStubBase> extends K8sGenericStub {
+
+ /**
+ * Instantiates a new dynamic stub.
+ *
+ * @param objectClass the object class
+ * @param objectListClass the object list class
+ * @param client the client
+ * @param context the context
+ * @param namespace the namespace
+ * @param name the name
+ */
+ public K8sDynamicStubBase(Class objectClass,
+ Class objectListClass, DynamicTypeAdapterFactory taf,
+ K8sClient client, APIResource context, String namespace,
+ String name) {
+ super(objectClass, objectListClass, client, context, namespace, name);
+ taf.register(client);
+ }
+}
\ No newline at end of file
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sGenericStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sGenericStub.java
new file mode 100644
index 0000000..9ba376f
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sGenericStub.java
@@ -0,0 +1,474 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2024 Michael N. Lipp
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.jdrupes.vmoperator.common;
+
+import io.kubernetes.client.Discovery.APIResource;
+import io.kubernetes.client.apimachinery.GroupVersionKind;
+import io.kubernetes.client.common.KubernetesListObject;
+import io.kubernetes.client.common.KubernetesObject;
+import io.kubernetes.client.custom.V1Patch;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.util.Strings;
+import io.kubernetes.client.util.generic.GenericKubernetesApi;
+import io.kubernetes.client.util.generic.KubernetesApiResponse;
+import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
+import io.kubernetes.client.util.generic.options.GetOptions;
+import io.kubernetes.client.util.generic.options.ListOptions;
+import io.kubernetes.client.util.generic.options.PatchOptions;
+import io.kubernetes.client.util.generic.options.UpdateOptions;
+import java.net.HttpURLConnection;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+
+/**
+ * A stub for namespaced custom objects. This stub provides the
+ * functions common to all Kubernetes objects, but uses variables
+ * for all types. This class should be used as base class only.
+ *
+ * @param the generic type
+ * @param the generic type
+ */
+@SuppressWarnings({ "PMD.TooManyMethods" })
+public class K8sGenericStub {
+ protected final K8sClient client;
+ private final GenericKubernetesApi api;
+ protected final APIResource context;
+ protected final String namespace;
+ protected final String name;
+
+ /**
+ * Instantiates a new stub for the object specified. If the object
+ * exists in the context specified, the version (see
+ * {@link #version()} is bound to the existing object's version.
+ * Else the stub is dangling with the version set to the context's
+ * preferred version.
+ *
+ * @param objectClass the object class
+ * @param objectListClass the object list class
+ * @param client the client
+ * @param context the context
+ * @param namespace the namespace
+ * @param name the name
+ */
+ @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
+ protected K8sGenericStub(Class objectClass, Class objectListClass,
+ K8sClient client, APIResource context, String namespace,
+ String name) {
+ this.client = client;
+ this.namespace = namespace;
+ this.name = name;
+
+ // Bind version
+ var foundVersion = context.getPreferredVersion();
+ GenericKubernetesApi testApi = null;
+ GetOptions mdOpts
+ = new GetOptions().isPartialObjectMetadataRequest(true);
+ for (var version : candidateVersions(context)) {
+ testApi = new GenericKubernetesApi<>(objectClass, objectListClass,
+ context.getGroup(), version, context.getResourcePlural(),
+ client);
+ if (testApi.get(namespace, name, mdOpts)
+ .isSuccess()) {
+ foundVersion = version;
+ break;
+ }
+ }
+ if (foundVersion.equals(context.getPreferredVersion())) {
+ this.context = context;
+ } else {
+ this.context = K8s.preferred(context, foundVersion);
+ }
+
+ api = Optional.ofNullable(testApi)
+ .orElseGet(() -> new GenericKubernetesApi<>(objectClass,
+ objectListClass, group(), version(), plural(), client));
+ }
+
+ /**
+ * Gets the context.
+ *
+ * @return the context
+ */
+ public APIResource context() {
+ return context;
+ }
+
+ /**
+ * Gets the group.
+ *
+ * @return the group
+ */
+ public String group() {
+ return context.getGroup();
+ }
+
+ /**
+ * Gets the version.
+ *
+ * @return the version
+ */
+ public String version() {
+ return context.getPreferredVersion();
+ }
+
+ /**
+ * Gets the kind.
+ *
+ * @return the kind
+ */
+ public String kind() {
+ return context.getKind();
+ }
+
+ /**
+ * Gets the plural.
+ *
+ * @return the plural
+ */
+ public String plural() {
+ return context.getResourcePlural();
+ }
+
+ /**
+ * Gets the namespace.
+ *
+ * @return the namespace
+ */
+ public String namespace() {
+ return namespace;
+ }
+
+ /**
+ * Gets the name.
+ *
+ * @return the name
+ */
+ public String name() {
+ return name;
+ }
+
+ /**
+ * Delete the Kubernetes object.
+ *
+ * @throws ApiException the API exception
+ */
+ public void delete() throws ApiException {
+ var result = api.delete(namespace, name);
+ if (result.isSuccess()
+ || result.getHttpStatusCode() == HttpURLConnection.HTTP_NOT_FOUND) {
+ return;
+ }
+ result.throwsApiException();
+ }
+
+ /**
+ * Retrieves and returns the current state of the object.
+ *
+ * @return the object's state
+ * @throws ApiException the api exception
+ */
+ public Optional model() throws ApiException {
+ return K8s.optional(api.get(namespace, name));
+ }
+
+ /**
+ * Updates the object's status. Does not retry in case of conflict.
+ *
+ * @param object the current state of the object (passed to `status`)
+ * @param updater function that returns the new status
+ * @return the updated model or empty if the object was not found
+ * @throws ApiException the api exception
+ */
+ public Optional updateStatus(O object, Function updater)
+ throws ApiException {
+ return K8s.optional(api.updateStatus(object, updater));
+ }
+
+ /**
+ * Updates the status of the given object. In case of conflict,
+ * get the current version of the object and tries again. Retries
+ * up to `retries` times.
+ *
+ * @param updater the function updating the status
+ * @param current the current state of the object, used for the first
+ * attempt to update
+ * @param retries the retries in case of conflict
+ * @return the updated model or empty if the object was not found
+ * @throws ApiException the api exception
+ */
+ @SuppressWarnings({ "PMD.AssignmentInOperand" })
+ public Optional updateStatus(Function updater, O current,
+ int retries) throws ApiException {
+ while (true) {
+ try {
+ if (current == null) {
+ current = api.get(namespace, name)
+ .throwsApiException().getObject();
+ }
+ return updateStatus(current, updater);
+ } catch (ApiException e) {
+ if (HttpURLConnection.HTTP_CONFLICT != e.getCode()
+ || retries-- <= 0) {
+ throw e;
+ }
+ // Get current version for new attempt
+ current = null;
+ }
+ }
+ }
+
+ /**
+ * Gets the object and updates the status. In case of conflict, retries
+ * up to `retries` times.
+ *
+ * @param updater the function updating the status
+ * @param retries the retries in case of conflict
+ * @return the updated model or empty if the object was not found
+ * @throws ApiException the api exception
+ */
+ public Optional updateStatus(Function updater, int retries)
+ throws ApiException {
+ return updateStatus(updater, null, retries);
+ }
+
+ /**
+ * Updates the status of the given object. In case of conflict,
+ * get the current version of the object and tries again. Retries
+ * up to `retries` times.
+ *
+ * @param updater the function updating the status
+ * @param current the current
+ * @return the kubernetes api response
+ * the updated model or empty if not successful
+ * @throws ApiException the api exception
+ */
+ public Optional updateStatus(Function updater, O current)
+ throws ApiException {
+ return updateStatus(updater, current, 16);
+ }
+
+ /**
+ * Updates the status. In case of conflict, retries up to 16 times.
+ *
+ * @param updater the function updating the status
+ * @return the kubernetes api response
+ * the updated model or empty if not successful
+ * @throws ApiException the api exception
+ */
+ public Optional updateStatus(Function updater)
+ throws ApiException {
+ return updateStatus(updater, null);
+ }
+
+ /**
+ * Patch the object.
+ *
+ * @param patchType the patch type
+ * @param patch the patch
+ * @param options the options
+ * @return the kubernetes api response if successful
+ * @throws ApiException the api exception
+ */
+ public Optional patch(String patchType, V1Patch patch,
+ PatchOptions options) throws ApiException {
+ return K8s
+ .optional(api.patch(namespace, name, patchType, patch, options)
+ .throwsApiException());
+ }
+
+ /**
+ * Patch the object using default options.
+ *
+ * @param patchType the patch type
+ * @param patch the patch
+ * @return the kubernetes api response if successful
+ * @throws ApiException the api exception
+ */
+ public Optional
+ patch(String patchType, V1Patch patch) throws ApiException {
+ PatchOptions opts = new PatchOptions();
+ return patch(patchType, patch, opts);
+ }
+
+ /**
+ * Apply the given definition.
+ *
+ * @param def the def
+ * @return the kubernetes api response if successful
+ * @throws ApiException the api exception
+ */
+ public Optional apply(DynamicKubernetesObject def) throws ApiException {
+ PatchOptions opts = new PatchOptions();
+ opts.setForce(true);
+ opts.setFieldManager("kubernetes-java-kubectl-apply");
+ return patch(V1Patch.PATCH_FORMAT_APPLY_YAML,
+ new V1Patch(client.getJSON().serialize(def)), opts);
+ }
+
+ /**
+ * Update the object.
+ *
+ * @param object the object
+ * @return the kubernetes api response
+ * @throws ApiException the api exception
+ */
+ public KubernetesApiResponse update(O object) throws ApiException {
+ return api.update(object).throwsApiException();
+ }
+
+ /**
+ * Update the object.
+ *
+ * @param object the object
+ * @param options the options
+ * @return the kubernetes api response
+ * @throws ApiException the api exception
+ */
+ public KubernetesApiResponse update(O object, UpdateOptions options)
+ throws ApiException {
+ return api.update(object, options).throwsApiException();
+ }
+
+ /**
+ * A supplier for generic stubs.
+ *
+ * @param the object type
+ * @param the object list type
+ * @param the result type
+ */
+ @FunctionalInterface
+ public interface GenericSupplier> {
+
+ /**
+ * Gets a new stub.
+ *
+ * @param client the client
+ * @param namespace the namespace
+ * @param name the name
+ * @return the result
+ */
+ R get(K8sClient client, String namespace, String name);
+ }
+
+ @Override
+ @SuppressWarnings("PMD.UseLocaleWithCaseConversions")
+ public String toString() {
+ return (Strings.isNullOrEmpty(group()) ? "" : group() + "/")
+ + version().toUpperCase() + kind() + " " + namespace + ":" + name;
+ }
+
+ /**
+ * Get a namespaced object stub for a newly created object.
+ *
+ * @param the object type
+ * @param the object list type
+ * @param the stub type
+ * @param objectClass the object class
+ * @param objectListClass the object list class
+ * @param client the client
+ * @param context the context
+ * @param model the model
+ * @param provider the provider
+ * @return the stub if the object exists
+ * @throws ApiException the api exception
+ */
+ public static >
+ R create(Class objectClass, Class objectListClass,
+ K8sClient client, APIResource context, O model,
+ GenericSupplier provider) throws ApiException {
+ var api = new GenericKubernetesApi<>(objectClass, objectListClass,
+ context.getGroup(), context.getPreferredVersion(),
+ context.getResourcePlural(), client);
+ api.create(model).throwsApiException();
+ return provider.get(client, model.getMetadata().getNamespace(),
+ model.getMetadata().getName());
+ }
+
+ /**
+ * Get the stubs for the objects in the given namespace that match
+ * the criteria from the given options.
+ *
+ * @param the object type
+ * @param the object list type
+ * @param the stub type
+ * @param objectClass the object class
+ * @param objectListClass the object list class
+ * @param client the client
+ * @param context the context
+ * @param namespace the namespace
+ * @param options the options
+ * @param provider the provider
+ * @return the collection
+ * @throws ApiException the api exception
+ */
+ public static >
+ Collection list(Class objectClass, Class objectListClass,
+ K8sClient client, APIResource context, String namespace,
+ ListOptions options, GenericSupplier provider)
+ throws ApiException {
+ var result = new ArrayList();
+ for (var version : candidateVersions(context)) {
+ @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
+ var api = new GenericKubernetesApi<>(objectClass, objectListClass,
+ context.getGroup(), version, context.getResourcePlural(),
+ client);
+ var objs = api.list(namespace, options).throwsApiException();
+ for (var item : objs.getObject().getItems()) {
+ result.add(provider.get(client, namespace,
+ item.getMetadata().getName()));
+ }
+ }
+ return result;
+ }
+
+ private static List candidateVersions(APIResource context) {
+ var result = new LinkedList<>(context.getVersions());
+ result.remove(context.getPreferredVersion());
+ result.add(0, context.getPreferredVersion());
+ return result;
+ }
+
+ /**
+ * Api resource.
+ *
+ * @param client the client
+ * @param gvk the gvk
+ * @return the API resource
+ * @throws ApiException the api exception
+ */
+ public static APIResource apiResource(K8sClient client,
+ GroupVersionKind gvk) throws ApiException {
+ var context = K8s.context(client, gvk.getGroup(), gvk.getVersion(),
+ gvk.getKind());
+ if (context.isEmpty()) {
+ throw new ApiException("No known API for " + gvk.getGroup()
+ + "/" + gvk.getVersion() + " " + gvk.getKind());
+ }
+ return context.get();
+ }
+
+}
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sObserver.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sObserver.java
new file mode 100644
index 0000000..9e22382
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sObserver.java
@@ -0,0 +1,237 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2024 Michael N. Lipp
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.jdrupes.vmoperator.common;
+
+import io.kubernetes.client.Discovery.APIResource;
+import io.kubernetes.client.common.KubernetesListObject;
+import io.kubernetes.client.common.KubernetesObject;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.util.Watch.Response;
+import io.kubernetes.client.util.generic.GenericKubernetesApi;
+import io.kubernetes.client.util.generic.options.ListOptions;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Optional;
+import java.util.function.BiConsumer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.jgrapes.core.Components;
+
+/**
+ * An observer that watches namespaced resources in a given context and
+ * invokes a handler on changes.
+ *
+ * @param the object type for the context
+ * @param the object list type for the context
+ */
+public class K8sObserver {
+
+ /**
+ * The type of change reported by {@link Response} as enum.
+ */
+ public enum ResponseType {
+ ADDED, MODIFIED, DELETED
+ }
+
+ protected final Logger logger = Logger.getLogger(getClass().getName());
+
+ protected final K8sClient client;
+ protected final GenericKubernetesApi api;
+ protected final APIResource context;
+ protected final String namespace;
+ protected final ListOptions options;
+ protected final Thread thread;
+ protected BiConsumer> handler;
+ protected BiConsumer, Throwable> onTerminated;
+
+ /**
+ * Create and start a new observer for objects in the given context
+ * (using preferred version) and namespace with the given options.
+ *
+ * @param objectClass the object class
+ * @param objectListClass the object list class
+ * @param client the client
+ * @param context the context
+ * @param namespace the namespace
+ * @param options the options
+ */
+ @SuppressWarnings({ "PMD.AvoidCatchingThrowable",
+ "PMD.CognitiveComplexity", "PMD.AvoidCatchingGenericException" })
+ public K8sObserver(Class objectClass, Class objectListClass,
+ K8sClient client, APIResource context, String namespace,
+ ListOptions options) {
+ this.client = client;
+ this.context = context;
+ this.namespace = namespace;
+ this.options = options;
+
+ api = new GenericKubernetesApi<>(objectClass, objectListClass,
+ context.getGroup(), context.getPreferredVersion(),
+ context.getResourcePlural(), client);
+ thread = (Components.useVirtualThreads() ? Thread.ofVirtual()
+ : Thread.ofPlatform()).unstarted(() -> {
+ try {
+ logger.fine(() -> "Observing " + context.getResourcePlural()
+ + " (" + context.getPreferredVersion() + ")"
+ + Optional.ofNullable(options.getLabelSelector())
+ .map(ls -> " with labels " + ls).orElse("")
+ + " in " + namespace);
+
+ // Watch sometimes terminates without apparent reason.
+ while (!Thread.currentThread().isInterrupted()) {
+ Instant startedAt = Instant.now();
+ try {
+ var changed
+ = api.watch(namespace, options).iterator();
+ while (changed.hasNext()) {
+ var response = changed.next();
+ logger.fine(() -> "Resource "
+ + context.getKind() + "/"
+ + response.object.getMetadata().getName()
+ + " " + response.type);
+ handler.accept(client, response);
+ }
+ } catch (ApiException | RuntimeException e) {
+ logger.log(Level.FINE, e, () -> "Problem watching"
+ + " resource " + context.getKind()
+ + " (will retry): " + e.getMessage());
+ delayRestart(startedAt);
+ }
+ }
+ if (onTerminated != null) {
+ onTerminated.accept(this, null);
+ }
+ } catch (Throwable e) {
+ logger.log(Level.SEVERE, e, () -> "Probem watching: "
+ + e.getMessage());
+ if (onTerminated != null) {
+ onTerminated.accept(this, e);
+ }
+ }
+ });
+ }
+
+ @SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
+ private void delayRestart(Instant started) {
+ var runningFor = Duration
+ .between(started, Instant.now()).toMillis();
+ if (runningFor < 5000) {
+ logger.log(Level.FINE, () -> "Waiting... ");
+ try {
+ Thread.sleep(5000 - runningFor);
+ } catch (InterruptedException e1) { // NOPMD
+ // Retry
+ }
+ logger.log(Level.FINE, () -> "Retrying");
+ }
+ }
+
+ /**
+ * Sets the handler.
+ *
+ * @param handler the handler
+ * @return the observer
+ */
+ public K8sObserver
+ handler(BiConsumer> handler) {
+ this.handler = handler;
+ return this;
+ }
+
+ /**
+ * Sets a function to invoke if the observer terminates. First argument
+ * is this observer, the second is the throwable that caused the
+ * abnormal termination or `null` if the observer was terminated
+ * by {@link #stop()}.
+ *
+ * @param onTerminated the on terminated
+ * @return the observer
+ */
+ public K8sObserver onTerminated(
+ BiConsumer, Throwable> onTerminated) {
+ this.onTerminated = onTerminated;
+ return this;
+ }
+
+ /**
+ * Start the observer.
+ *
+ * @return the observer
+ */
+ public K8sObserver start() {
+ if (handler == null) {
+ throw new IllegalStateException("No handler defined");
+ }
+ thread.start();
+ return this;
+ }
+
+ /**
+ * Stops the observer.
+ *
+ * @return the observer
+ */
+ public K8sObserver stop() {
+ thread.interrupt();
+ return this;
+ }
+
+ /**
+ * Returns the client.
+ *
+ * @return the client
+ */
+ public K8sClient client() {
+ return client;
+ }
+
+ /**
+ * Returns the context.
+ *
+ * @return the context
+ */
+ public APIResource context() {
+ return context;
+ }
+
+ /**
+ * Returns the observed namespace.
+ *
+ * @return the namespace
+ */
+ public String getNamespace() {
+ return namespace;
+ }
+
+ /**
+ * Returns the options for object selection.
+ *
+ * @return the list options
+ */
+ public ListOptions options() {
+ return options;
+ }
+
+ @Override
+ public String toString() {
+ return "Observer for " + K8s.toString(context) + " " + namespace;
+ }
+
+}
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1ConfigMapStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1ConfigMapStub.java
new file mode 100644
index 0000000..07c59b2
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1ConfigMapStub.java
@@ -0,0 +1,60 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2024 Michael N. Lipp
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.jdrupes.vmoperator.common;
+
+import io.kubernetes.client.Discovery.APIResource;
+import io.kubernetes.client.openapi.models.V1ConfigMap;
+import io.kubernetes.client.openapi.models.V1ConfigMapList;
+import java.util.List;
+
+/**
+ * A stub for config maps (v1).
+ */
+public class K8sV1ConfigMapStub
+ extends K8sGenericStub {
+
+ public static final APIResource CONTEXT = new APIResource("", List.of("v1"),
+ "v1", "ConfigMap", true, "configmaps", "configmap");
+
+ /**
+ * Instantiates a new stub.
+ *
+ * @param client the client
+ * @param namespace the namespace
+ * @param name the name
+ */
+ protected K8sV1ConfigMapStub(K8sClient client, String namespace,
+ String name) {
+ super(V1ConfigMap.class, V1ConfigMapList.class, client,
+ CONTEXT, namespace, name);
+ }
+
+ /**
+ * Gets the stub for the given namespace and name.
+ *
+ * @param client the client
+ * @param namespace the namespace
+ * @param name the name
+ * @return the config map stub
+ */
+ public static K8sV1ConfigMapStub get(K8sClient client, String namespace,
+ String name) {
+ return new K8sV1ConfigMapStub(client, namespace, name);
+ }
+}
\ No newline at end of file
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1DeploymentStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1DeploymentStub.java
new file mode 100644
index 0000000..9075a84
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1DeploymentStub.java
@@ -0,0 +1,78 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2024 Michael N. Lipp
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.jdrupes.vmoperator.common;
+
+import io.kubernetes.client.Discovery.APIResource;
+import io.kubernetes.client.custom.V1Patch;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.models.V1Deployment;
+import io.kubernetes.client.openapi.models.V1DeploymentList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * A stub for pods (v1).
+ */
+public class K8sV1DeploymentStub
+ extends K8sGenericStub {
+
+ /** The deployment's context. */
+ public static final APIResource CONTEXT = new APIResource("apps",
+ List.of("v1"), "v1", "Pod", true, "deployments", "deployment");
+
+ /**
+ * Instantiates a new stub.
+ *
+ * @param client the client
+ * @param namespace the namespace
+ * @param name the name
+ */
+ protected K8sV1DeploymentStub(K8sClient client, String namespace,
+ String name) {
+ super(V1Deployment.class, V1DeploymentList.class, client,
+ CONTEXT, namespace, name);
+ }
+
+ /**
+ * Scales the deployment.
+ *
+ * @param replicas the replicas
+ * @return the new model or empty if not successful
+ * @throws ApiException the API exception
+ */
+ public Optional scale(int replicas) throws ApiException {
+ return patch(V1Patch.PATCH_FORMAT_JSON_PATCH,
+ new V1Patch("[{\"op\": \"replace\", \"path\": \"/spec/replicas"
+ + "\", \"value\": " + replicas + "}]"),
+ client.defaultPatchOptions());
+ }
+
+ /**
+ * Gets the stub for the given namespace and name.
+ *
+ * @param client the client
+ * @param namespace the namespace
+ * @param name the name
+ * @return the deployment stub
+ */
+ public static K8sV1DeploymentStub get(K8sClient client, String namespace,
+ String name) {
+ return new K8sV1DeploymentStub(client, namespace, name);
+ }
+}
\ No newline at end of file
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1NodeStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1NodeStub.java
new file mode 100644
index 0000000..ea1237d
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1NodeStub.java
@@ -0,0 +1,83 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2024 Michael N. Lipp
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.jdrupes.vmoperator.common;
+
+import io.kubernetes.client.Discovery.APIResource;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.models.V1Node;
+import io.kubernetes.client.openapi.models.V1NodeList;
+import io.kubernetes.client.util.generic.options.ListOptions;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * A stub for nodes (v1).
+ */
+public class K8sV1NodeStub extends K8sClusterGenericStub {
+
+ public static final APIResource CONTEXT = new APIResource("", List.of("v1"),
+ "v1", "Node", true, "nodes", "node");
+
+ /**
+ * Instantiates a new stub.
+ *
+ * @param client the client
+ * @param name the name
+ */
+ protected K8sV1NodeStub(K8sClient client, String name) {
+ super(V1Node.class, V1NodeList.class, client, CONTEXT, name);
+ }
+
+ /**
+ * Gets the stub for the given name.
+ *
+ * @param client the client
+ * @param name the name
+ * @return the config map stub
+ */
+ public static K8sV1NodeStub get(K8sClient client, String name) {
+ return new K8sV1NodeStub(client, name);
+ }
+
+ /**
+ * Get the stubs for the objects that match
+ * the criteria from the given options.
+ *
+ * @param client the client
+ * @param options the options
+ * @return the collection
+ * @throws ApiException the api exception
+ */
+ public static Collection list(K8sClient client,
+ ListOptions options) throws ApiException {
+ return K8sClusterGenericStub.list(V1Node.class, V1NodeList.class,
+ client, CONTEXT, options, K8sV1NodeStub::getGeneric);
+ }
+
+ /**
+ * Provide {@link GenericSupplier}.
+ */
+ @SuppressWarnings({ "PMD.UnusedFormalParameter" })
+ private static K8sV1NodeStub getGeneric(Class objectClass,
+ Class objectListClass, K8sClient client,
+ APIResource context, String name) {
+ return new K8sV1NodeStub(client, name);
+ }
+
+}
\ No newline at end of file
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1PodStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1PodStub.java
new file mode 100644
index 0000000..f21bb47
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1PodStub.java
@@ -0,0 +1,78 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2024 Michael N. Lipp
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.jdrupes.vmoperator.common;
+
+import io.kubernetes.client.Discovery.APIResource;
+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.generic.options.ListOptions;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * A stub for pods (v1).
+ */
+public class K8sV1PodStub extends K8sGenericStub {
+
+ /** The pods' context. */
+ public static final APIResource CONTEXT
+ = new APIResource("", List.of("v1"), "v1", "Pod", true, "pods", "pod");
+
+ /**
+ * Instantiates a new stub.
+ *
+ * @param client the client
+ * @param namespace the namespace
+ * @param name the name
+ */
+ protected K8sV1PodStub(K8sClient client, String namespace, String name) {
+ super(V1Pod.class, V1PodList.class, client, CONTEXT, namespace, name);
+ }
+
+ /**
+ * Gets the stub for the given namespace and name.
+ *
+ * @param client the client
+ * @param namespace the namespace
+ * @param name the name
+ * @return the kpod stub
+ */
+ public static K8sV1PodStub get(K8sClient client, String namespace,
+ String name) {
+ return new K8sV1PodStub(client, namespace, name);
+ }
+
+ /**
+ * Get the stubs for the objects in the given namespace that match
+ * the criteria from the given options.
+ *
+ * @param client the client
+ * @param namespace the namespace
+ * @param options the options
+ * @return the collection
+ * @throws ApiException the api exception
+ */
+ public static Collection list(K8sClient client,
+ String namespace, ListOptions options) throws ApiException {
+ return K8sGenericStub.list(V1Pod.class, V1PodList.class, client,
+ CONTEXT, namespace, options, (clnt, nscp,
+ name) -> new K8sV1PodStub(clnt, nscp, name));
+ }
+}
\ No newline at end of file
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1PvcStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1PvcStub.java
new file mode 100644
index 0000000..c46a60f
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1PvcStub.java
@@ -0,0 +1,81 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2024 Michael N. Lipp
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.jdrupes.vmoperator.common;
+
+import io.kubernetes.client.Discovery.APIResource;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.models.V1PersistentVolumeClaim;
+import io.kubernetes.client.openapi.models.V1PersistentVolumeClaimList;
+import io.kubernetes.client.util.generic.options.ListOptions;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * A stub for pods (v1).
+ */
+public class K8sV1PvcStub extends
+ K8sGenericStub {
+
+ /** The pods' context. */
+ public static final APIResource CONTEXT
+ = new APIResource("", List.of("v1"), "v1", "PersistentVolumeClaim",
+ true, "persistentvolumeclaims", "persistentvolumeclaim");
+
+ /**
+ * Instantiates a new stub.
+ *
+ * @param client the client
+ * @param namespace the namespace
+ * @param name the name
+ */
+ protected K8sV1PvcStub(K8sClient client, String namespace, String name) {
+ super(V1PersistentVolumeClaim.class, V1PersistentVolumeClaimList.class,
+ client, CONTEXT, namespace, name);
+ }
+
+ /**
+ * Gets the stub for the given namespace and name.
+ *
+ * @param client the client
+ * @param namespace the namespace
+ * @param name the name
+ * @return the kpod stub
+ */
+ public static K8sV1PvcStub get(K8sClient client, String namespace,
+ String name) {
+ return new K8sV1PvcStub(client, namespace, name);
+ }
+
+ /**
+ * Get the stubs for the objects in the given namespace that match
+ * the criteria from the given options.
+ *
+ * @param client the client
+ * @param namespace the namespace
+ * @param options the options
+ * @return the collection
+ * @throws ApiException the api exception
+ */
+ public static Collection list(K8sClient client,
+ String namespace, ListOptions options) throws ApiException {
+ return K8sGenericStub.list(V1PersistentVolumeClaim.class,
+ V1PersistentVolumeClaimList.class, client, CONTEXT, namespace,
+ options, (clnt, nscp, name) -> new K8sV1PvcStub(clnt, nscp, name));
+ }
+}
\ No newline at end of file
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1SecretStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1SecretStub.java
new file mode 100644
index 0000000..9c1c086
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1SecretStub.java
@@ -0,0 +1,92 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2024 Michael N. Lipp
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.jdrupes.vmoperator.common;
+
+import io.kubernetes.client.Discovery.APIResource;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.models.V1Secret;
+import io.kubernetes.client.openapi.models.V1SecretList;
+import io.kubernetes.client.util.generic.options.ListOptions;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * A stub for secrets (v1).
+ */
+public class K8sV1SecretStub extends K8sGenericStub {
+
+ public static final APIResource CONTEXT = new APIResource("", List.of("v1"),
+ "v1", "Secret", true, "secrets", "secret");
+
+ /**
+ * Instantiates a new stub.
+ *
+ * @param client the client
+ * @param namespace the namespace
+ * @param name the name
+ */
+ protected K8sV1SecretStub(K8sClient client, String namespace,
+ String name) {
+ super(V1Secret.class, V1SecretList.class, client,
+ CONTEXT, namespace, name);
+ }
+
+ /**
+ * Gets the stub for the given namespace and name.
+ *
+ * @param client the client
+ * @param namespace the namespace
+ * @param name the name
+ * @return the config map stub
+ */
+ public static K8sV1SecretStub get(K8sClient client, String namespace,
+ String name) {
+ return new K8sV1SecretStub(client, namespace, name);
+ }
+
+ /**
+ * Creates an object stub from a model.
+ *
+ * @param client the client
+ * @param model the model
+ * @return the k 8 s dynamic stub
+ * @throws ApiException the api exception
+ */
+ public static K8sV1SecretStub create(K8sClient client, V1Secret model)
+ throws ApiException {
+ return K8sGenericStub.create(V1Secret.class,
+ V1SecretList.class, client, CONTEXT, model, K8sV1SecretStub::new);
+ }
+
+ /**
+ * Get the stubs for the objects in the given namespace that match
+ * the criteria from the given options.
+ *
+ * @param client the client
+ * @param namespace the namespace
+ * @param options the options
+ * @return the collection
+ * @throws ApiException the api exception
+ */
+ public static Collection list(K8sClient client,
+ String namespace, ListOptions options) throws ApiException {
+ return K8sGenericStub.list(V1Secret.class, V1SecretList.class, client,
+ CONTEXT, namespace, options, K8sV1SecretStub::new);
+ }
+}
\ No newline at end of file
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1ServiceStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1ServiceStub.java
new file mode 100644
index 0000000..863f86f
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1ServiceStub.java
@@ -0,0 +1,79 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2024 Michael N. Lipp
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.jdrupes.vmoperator.common;
+
+import io.kubernetes.client.Discovery.APIResource;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.models.V1Service;
+import io.kubernetes.client.openapi.models.V1ServiceList;
+import io.kubernetes.client.util.generic.options.ListOptions;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * A stub for secrets (v1).
+ */
+public class K8sV1ServiceStub extends K8sGenericStub {
+
+ public static final APIResource CONTEXT = new APIResource("", List.of("v1"),
+ "v1", "Service", true, "services", "service");
+
+ /**
+ * Instantiates a new stub.
+ *
+ * @param client the client
+ * @param namespace the namespace
+ * @param name the name
+ */
+ protected K8sV1ServiceStub(K8sClient client, String namespace,
+ String name) {
+ super(V1Service.class, V1ServiceList.class, client, CONTEXT, namespace,
+ name);
+ }
+
+ /**
+ * Gets the stub for the given namespace and name.
+ *
+ * @param client the client
+ * @param namespace the namespace
+ * @param name the name
+ * @return the config map stub
+ */
+ public static K8sV1ServiceStub get(K8sClient client, String namespace,
+ String name) {
+ return new K8sV1ServiceStub(client, namespace, name);
+ }
+
+ /**
+ * Get the stubs for the objects in the given namespace that match
+ * the criteria from the given options.
+ *
+ * @param client the client
+ * @param namespace the namespace
+ * @param options the options
+ * @return the collection
+ * @throws ApiException the api exception
+ */
+ public static Collection list(K8sClient client,
+ String namespace, ListOptions options) throws ApiException {
+ return K8sGenericStub.list(V1Service.class, V1ServiceList.class, client,
+ CONTEXT, namespace, options,
+ (clnt, nscp, name) -> new K8sV1ServiceStub(clnt, nscp, name));
+ }
+}
\ No newline at end of file
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1StatefulSetStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1StatefulSetStub.java
new file mode 100644
index 0000000..be30b00
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1StatefulSetStub.java
@@ -0,0 +1,62 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2024 Michael N. Lipp
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.jdrupes.vmoperator.common;
+
+import io.kubernetes.client.Discovery.APIResource;
+import io.kubernetes.client.openapi.models.V1StatefulSet;
+import io.kubernetes.client.openapi.models.V1StatefulSetList;
+import java.util.List;
+
+/**
+ * A stub for stateful sets (v1).
+ */
+public class K8sV1StatefulSetStub
+ extends K8sGenericStub {
+
+ /** The stateful sets' context */
+ public static final APIResource CONTEXT
+ = new APIResource("apps", List.of("v1"), "v1", "StatefulSet", true,
+ "statefulsets", "statefulset");
+
+ /**
+ * Instantiates a new stub.
+ *
+ * @param client the client
+ * @param namespace the namespace
+ * @param name the name
+ */
+ protected K8sV1StatefulSetStub(K8sClient client, String namespace,
+ String name) {
+ super(V1StatefulSet.class, V1StatefulSetList.class, client, CONTEXT,
+ namespace, name);
+ }
+
+ /**
+ * Gets the stub for the given namespace and name.
+ *
+ * @param client the client
+ * @param namespace the namespace
+ * @param name the name
+ * @return the stateful set stub
+ */
+ public static K8sV1StatefulSetStub get(K8sClient client, String namespace,
+ String name) {
+ return new K8sV1StatefulSetStub(client, namespace, name);
+ }
+}
\ No newline at end of file
diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinition.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinition.java
new file mode 100644
index 0000000..a0b66bf
--- /dev/null
+++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinition.java
@@ -0,0 +1,499 @@
+/*
+ * 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 .
+ */
+
+package org.jdrupes.vmoperator.common;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import io.kubernetes.client.openapi.JSON;
+import io.kubernetes.client.openapi.models.V1Condition;
+import java.time.Instant;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import org.jdrupes.vmoperator.common.Constants.Status;
+import org.jdrupes.vmoperator.common.Constants.Status.Condition;
+import org.jdrupes.vmoperator.common.Constants.Status.Condition.Reason;
+import org.jdrupes.vmoperator.util.DataPath;
+
+/**
+ * Represents a VM definition.
+ */
+@SuppressWarnings({ "PMD.DataClass", "PMD.TooManyMethods" })
+public class VmDefinition extends K8sDynamicModel {
+
+ @SuppressWarnings({ "unused" })
+ private static final Logger logger
+ = Logger.getLogger(VmDefinition.class.getName());
+ @SuppressWarnings("PMD.FieldNamingConventions")
+ private static final Gson gson = new JSON().getGson();
+ @SuppressWarnings("PMD.FieldNamingConventions")
+ private static final ObjectMapper objectMapper
+ = new ObjectMapper().registerModule(new JavaTimeModule());
+
+ private final Model model;
+ private VmExtraData extraData;
+
+ /**
+ * The VM state from the VM definition.
+ */
+ public enum RequestedVmState {
+ STOPPED, RUNNING
+ }
+
+ /**
+ * Permissions for accessing and manipulating the VM.
+ */
+ public enum Permission {
+ START("start"), STOP("stop"), RESET("reset"),
+ ACCESS_CONSOLE("accessConsole"), TAKE_CONSOLE("takeConsole");
+
+ @SuppressWarnings("PMD.UseConcurrentHashMap")
+ private static Map reprs = new HashMap<>();
+
+ static {
+ for (var value : EnumSet.allOf(Permission.class)) {
+ reprs.put(value.repr, value);
+ }
+ }
+
+ private final String repr;
+
+ Permission(String repr) {
+ this.repr = repr;
+ }
+
+ /**
+ * Create permission from representation in CRD.
+ *
+ * @param value the value
+ * @return the permission
+ */
+ @SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
+ public static Set parse(String value) {
+ if ("*".equals(value)) {
+ return EnumSet.allOf(Permission.class);
+ }
+ return Set.of(reprs.get(value));
+ }
+
+ /**
+ * To string.
+ *
+ * @return the string
+ */
+ @Override
+ public String toString() {
+ return repr;
+ }
+ }
+
+ /**
+ * Permissions granted to a user or role.
+ *
+ * @param user the user
+ * @param role the role
+ * @param may the may
+ */
+ public record Grant(String user, String role, Set may) {
+
+ /**
+ * To string.
+ *
+ * @return the string
+ */
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ if (user != null) {
+ builder.append("User ").append(user);
+ } else {
+ builder.append("Role ").append(role);
+ }
+ builder.append(" may=").append(may).append(']');
+ return builder.toString();
+ }
+ }
+
+ /**
+ * The assignment information.
+ *
+ * @param pool the pool
+ * @param user the user
+ * @param lastUsed the last used
+ */
+ public record Assignment(String pool, String user, Instant lastUsed) {
+ }
+
+ /**
+ * Instantiates a new vm definition.
+ *
+ * @param delegate the delegate
+ * @param json the json
+ */
+ public VmDefinition(Gson delegate, JsonObject json) {
+ super(delegate, json);
+ model = gson.fromJson(json, Model.class);
+ }
+
+ /**
+ * Gets the spec.
+ *
+ * @return the spec
+ */
+ public Map spec() {
+ return model.getSpec();
+ }
+
+ /**
+ * Get a value from the spec using {@link DataPath#get}.
+ *
+ * @param the generic type
+ * @param selectors the selectors
+ * @return the value, if found
+ */
+ public Optional fromSpec(Object... selectors) {
+ return DataPath.get(spec(), selectors);
+ }
+
+ /**
+ * The pools that this VM belongs to.
+ *
+ * @return the list
+ */
+ public List pools() {
+ return this.> fromSpec("pools")
+ .orElse(Collections.emptyList());
+ }
+
+ /**
+ * Get a value from the `spec().get("vm")` using {@link DataPath#get}.
+ *
+ * @param the generic type
+ * @param selectors the selectors
+ * @return the value, if found
+ */
+ public Optional fromVm(Object... selectors) {
+ return DataPath.get(spec(), "vm")
+ .flatMap(vm -> DataPath.get(vm, selectors));
+ }
+
+ /**
+ * Gets the status.
+ *
+ * @return the status
+ */
+ public Map status() {
+ return model.getStatus();
+ }
+
+ /**
+ * Get a value from the status using {@link DataPath#get}.
+ *
+ * @param the generic type
+ * @param selectors the selectors
+ * @return the value, if found
+ */
+ public Optional fromStatus(Object... selectors) {
+ return DataPath.get(status(), selectors);
+ }
+
+ /**
+ * The assignment information.
+ *
+ * @return the optional
+ */
+ public Optional assignment() {
+ return this.