diff --git a/.editorconfig b/.editorconfig index ad8e2c3..7e375e1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,7 +5,7 @@ charset = utf-8 insert_final_newline = true trim_trailing_whitespace = true -[*.{html,md,yml,yaml}] +[*.{md,yml,yaml}] indent_size = 2 indent_style = space diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 943329e..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "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/gradle.yml b/.github/workflows/gradle.yml index f47366a..547c1a4 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 21 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: - java-version: '21' + java-version: '17' distribution: 'temurin' - name: Build with Gradle - run: ./gradlew -Pwebsite.push.token=${{ secrets.WEBSITE_PUSH_TOKEN }} stage + run: ./gradlew -Prepo.access.token=${{ secrets.REPO_ACCESS_TOKEN }} stage diff --git a/.github/workflows/jekyll.yml b/.github/workflows/jekyll.yml deleted file mode 100644 index d0e4ec9..0000000 --- a/.github/workflows/jekyll.yml +++ /dev/null @@ -1,89 +0,0 @@ -# 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 beab0c4..3e6b3c9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,9 +18,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3 with: fetch-depth: 0 + ref: main - name: Install graphviz run: sudo apt-get install graphviz - name: Install podman @@ -31,10 +32,10 @@ jobs: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Set up JDK 21 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: - java-version: '21' + java-version: '17' distribution: 'temurin' - name: Push with Gradle - run: ./gradlew -Pwebsite.push.token=${{ secrets.WEBSITE_PUSH_TOKEN }} -Pdocker.registry=ghcr.io/${{ github.actor }} stage publishImage + run: ./gradlew -Prepo.access.token=${{ secrets.REPO_ACCESS_TOKEN }} -Pdocker.registry=ghcr.io/${{ github.actor }} stage pushImages diff --git a/.markdownlint.yaml b/.markdownlint.yaml deleted file mode 100644 index 6ed5002..0000000 --- a/.markdownlint.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# 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 72733d9..d0fed22 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.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 +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 auto.sync=false build.scans.enabled=false connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) diff --git a/.woodpecker/build.yaml b/.woodpecker/build.yaml deleted file mode 100644 index 56a575c..0000000 --- a/.woodpecker/build.yaml +++ /dev/null @@ -1,38 +0,0 @@ -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 09fcd25..1895bbb 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,13 @@ -[![Java CI with Gradle](https://github.com/mnlipp/VM-Operator/actions/workflows/gradle.yml/badge.svg)](https://github.com/mnlipp/VM-Operator/actions/workflows/gradle.yml) +[![Java CI with Gradle](https://github.com/mnlipp/VM-Operator/actions/workflows/gradle.yml/badge.svg)](https://github.com/mnlipp/VM-Operator/actions/workflows/gradle.yml) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/2277842dac894de4b663c6aa2779077e)](https://app.codacy.com/gh/mnlipp/VM-Operator/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) ![Latest Manager](https://img.shields.io/github/v/tag/mnlipp/vm-operator?filter=manager*&label=latest) ![Latest Runner](https://img.shields.io/github/v/tag/mnlipp/vm-operator?filter=runner-qemu*&label=latest) -# Run QEMU/KVM in Kubernetes Pods +# Run Qemu in Kubernetes Pods -![Overview picture](webpages/index-pic.svg) +The goal of this project is to provide the means for running Qemu +based VMs in Kubernetes pods. -This project provides an easy to use and flexible solution for running -QEMU/KVM based VMs in Kubernetes pods. - -The central component of this solution is the kubernetes operator that -manages "runners". These run in pods and are used to start and manage -the QEMU/KVM process for the VMs (optionally together with a SW-TPM). - -A web GUI for administrators provides an overview of the VMs together -with some basic control over the VMs. A web GUI for users provides an -interface to access and optionally start, stop and reset the VMs. - -Advanced features of the operator include pooling of VMs and automatic -login. - -See the [project's home page](https://vm-operator.jdrupes.org/) +See the [project's home page](https://mnlipp.github.io/VM-Operator/) for details. + diff --git a/build.gradle b/build.gradle index eb8e59a..1a11881 100644 --- a/build.gradle +++ b/build.gradle @@ -5,10 +5,9 @@ buildscript { } plugins { - id 'org.ajoberstar.grgit' version '5.2.0' + id 'org.ajoberstar.grgit' version '5.2.0' apply false id 'org.ajoberstar.git-publish' version '4.2.0' apply false - id 'pl.allegro.tech.build.axion-release' version '1.17.2' apply false - id 'org.jdrupes.vmoperator.versioning-conventions' + id 'pl.allegro.tech.build.axion-release' version '1.15.0' apply false id 'org.jdrupes.vmoperator.java-doc-conventions' id 'eclipse' id "com.github.node-gradle.node" version "7.0.1" @@ -19,7 +18,7 @@ allprojects { } task stage { - description = 'To be executed by CI.' + description = 'To be executed by CI, build and update JavaDoc.' group = 'build' // Build everything first @@ -27,6 +26,11 @@ 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/.settings/org.eclipse.jdt.core.prefs b/buildSrc/.settings/org.eclipse.jdt.core.prefs index b25073a..68fda12 100644 --- a/buildSrc/.settings/org.eclipse.jdt.core.prefs +++ b/buildSrc/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,9 @@ -# -#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 @@ -9,5 +11,12 @@ 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 71b5e37..bf0ca13 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=-1 +groovy.compiler.level=40 groovy.script.filters=**/*.dsld,y,**/*.gradle,n diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 4a5db6d..a9fb634 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -1,3 +1,9 @@ +/* + * 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 @@ -8,24 +14,52 @@ plugins { id 'eclipse' } +repositories { + // Use the plugin portal to apply community plugins in convention plugins. + gradlePluginPortal() +} + sourceSets { - main { - groovy { - srcDirs = ['src'] - } - resources { - srcDirs = ['resources'] - } - } + main { + groovy { + srcDirs = ['src'] + } + } + + test { + groovy { + srcDirs = ['test'] + } + } } eclipse { + project { + file { + // closure executed after .project content is loaded from existing file + // and before gradle build information is merged + beforeMerged { project -> + project.natures.clear() + project.buildCommands.clear() + } + + project.natures += 'org.eclipse.buildship.core.gradleprojectnature' + // Don't build, result not used by Eclipse anyway + // project.buildCommand 'org.eclipse.buildship.core.gradleprojectbuilder' + } + } + + classpath { + downloadJavadoc = true + downloadSources = true + } + jdt { 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 new file mode 100644 index 0000000..3f67e42 --- /dev/null +++ b/buildSrc/settings.gradle @@ -0,0 +1,7 @@ +/* + * 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 605dc09..50aebae 100644 --- a/buildSrc/src/org.jdrupes.vmoperator.java-common-conventions.gradle +++ b/buildSrc/src/org.jdrupes.vmoperator.java-common-conventions.gradle @@ -5,11 +5,6 @@ */ 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' @@ -18,11 +13,15 @@ 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 { @@ -55,25 +54,21 @@ sourceSets { java { toolchain { - languageVersion = JavaLanguageVersion.of(21) + languageVersion = JavaLanguageVersion.of(17) } } jar { manifest { - def matchExpr = [ project.tagName + "*" ] - - inputs.property("gitDescriptor", - { grgit.describe(always: true, match: matchExpr) }) + inputs.property("gitDescriptor", { grgit.describe(always: true) }) // 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 ${gitDesc})", + "Implementation-Version": "$project.version (built from ${grgit.describe(always: true)})", "Implementation-Vendor": grgit.repository.jgit.repository.config.getString("user", null, "name") + " (" + grgit.repository.jgit.repository.config.getString("user", null, "email") + ")", - "Git-Descriptor": gitDesc, + "Git-Descriptor": grgit.describe(always: true), "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 6af8fa7..95d7eff 100644 --- a/buildSrc/src/org.jdrupes.vmoperator.java-doc-conventions.gradle +++ b/buildSrc/src/org.jdrupes.vmoperator.java-doc-conventions.gradle @@ -22,28 +22,31 @@ configurations { } dependencies { - markdownDoclet "org.jdrupes.mdoclet:doclet:4.0.0" - javadocTaglets "org.jdrupes.taglets:plantuml-taglet:3.0.0" + 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' + } } task apidocs (type: JavaExec) { // Does not work on JitPack, no /usr/bin/dot - enabled = JavaVersion.current() == JavaVersion.VERSION_21 + enabled = JavaVersion.current() == JavaVersion.VERSION_17 + + dependsOn javadocResources outputs.dir(docDestinationDir) inputs.file rootProject.file('overview.md') - inputs.file "${rootProject.rootDir}/misc/javadoc-overwrites.css" + inputs.file "${rootProject.rootDir}/misc/stylesheet.css" - 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' + 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' gradle.projectsEvaluated { // Make sure that other projects' compileClasspaths are resolved @@ -66,8 +69,8 @@ task apidocs (type: JavaExec) { '-package', '-use', '-linksource', - '-link', 'https://docs.oracle.com/en/java/javase/21/docs/api/', - '-link', 'https://jgrapes.org/latest-release/javadoc/', + '-link', 'https://docs.oracle.com/en/java/javase/17/docs/api/', + '-link', 'https://mnlipp.github.io/jgrapes/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', @@ -85,7 +88,7 @@ task apidocs (type: JavaExec) { '-bottom', rootProject.file("misc/javadoc.bottom.txt").text, '--allow-script-in-comments', '-Xdoclint:-html', - '--add-stylesheet', "${rootProject.rootDir}/misc/javadoc-overwrites.css", + '--main-stylesheet', "${rootProject.rootDir}/misc/stylesheet.css", '--add-exports=jdk.javadoc/jdk.javadoc.internal.doclets.formats.html=ALL-UNNAMED', '-quiet' ] @@ -94,27 +97,34 @@ 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['website.push.token'] ?: "nouser") + 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 } diff --git a/buildSrc/src/org.jdrupes.vmoperator.versioning-conventions.gradle b/buildSrc/src/org.jdrupes.vmoperator.versioning-conventions.gradle index 49b6f74..114db51 100644 --- a/buildSrc/src/org.jdrupes.vmoperator.versioning-conventions.gradle +++ b/buildSrc/src/org.jdrupes.vmoperator.versioning-conventions.gradle @@ -11,26 +11,21 @@ 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 { - prefix = project.tagName + def shortened = project.name.startsWith(project.group + ".") ? + project.name.substring(project.group.length() + 1) : project.name + if (shortened == "manager") { + shortened = "manager-app"; + } + 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 } } -project.version = scmVersion.version +version = scmVersion.version ext.isSnapshot = version.endsWith('-SNAPSHOT') diff --git a/checkstyle.xml b/checkstyle.xml index 088e543..b5a60d3 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -30,11 +30,8 @@ - - - @@ -53,9 +50,10 @@ + - + diff --git a/deploy/crds/vmpools-crd.yaml b/deploy/crds/vmpools-crd.yaml deleted file mode 100644 index 2144940..0000000 --- a/deploy/crds/vmpools-crd.yaml +++ /dev/null @@ -1,74 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: vmpools.vmoperator.jdrupes.org -spec: - group: vmoperator.jdrupes.org - # list of versions supported by this CustomResourceDefinition - versions: - - name: v1 - served: true - storage: true - schema: - openAPIV3Schema: - type: object - properties: - spec: - type: object - properties: - retention: - description: >- - Defines the timeout for assignments. The time may be - specified as ISO 8601 time or duration. When specifying - a duration, it will be added to the last time the VM's - console was used to obtain the timeout. - type: string - pattern: '^(?:\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d{1,9})?(?:Z|[+-](?:[01]\d|2[0-3])(?:|:?[0-5]\d))|P(?:\d+Y)?(?:\d+M)?(?:\d+W)?(?:\d+D)?(?:T(?:\d+[Hh])?(?:\d+[Mm])?(?:\d+(?:\.\d{1,9})?[Ss])?)?)$' - default: "PT1h" - loginOnAssignment: - description: >- - If set to true, the user will be automatically logged in - to the VM's console when the VM is assigned to him. - type: boolean - default: false - permissions: - type: array - description: >- - Defines permissions for accessing and manipulating the Pool. - items: - type: object - description: >- - Permissions can be granted to a user or to a role. - oneOf: - - required: - - user - - required: - - role - properties: - user: - type: string - role: - type: string - may: - type: array - items: - type: string - enum: - - start - - stop - - reset - - accessConsole - - "*" - default: ["accessConsole"] - required: - - permissions - # either Namespaced or Cluster - scope: Namespaced - names: - # plural name to be used in the URL: /apis/// - plural: vmpools - # singular name to be used as an alias on the CLI and for display - singular: vmpool - # kind is normally the CamelCased singular type. Your resource manifests use this. - kind: VmPool - listKind: VmPoolList diff --git a/deploy/crds/vms-crd.yaml b/deploy/crds/vms-crd.yaml index c2a7a66..7e321da 100644 --- a/deploy/crds/vms-crd.yaml +++ b/deploy/crds/vms-crd.yaml @@ -933,12 +933,6 @@ spec: update: type: boolean default: true - guestShutdownStops: - description: >- - If true, sets the VM's state to "Stopped" when - the VM terminates due to a shutdown by the guest. - type: boolean - default: false loadBalancerService: description: >- Data to be merged with the loadBalancerService @@ -971,71 +965,6 @@ spec: additionalProperties: type: string nullable: true - cloudInit: - type: object - description: >- - Provides data for generating a cloud-init ISO - image that is attached to the VM. - properties: - metaData: - description: Copied to cloud-init's meta-data file. - type: object - additionalProperties: - type: string - userData: - description: Copied to cloud-init's user-data file. - type: object - x-kubernetes-preserve-unknown-fields: true - networkConfig: - description: Copied to cloud-init's network-config file. - type: object - x-kubernetes-preserve-unknown-fields: true - permissions: - type: array - description: >- - Defines permissions for accessing and manipulating the VM. - The meaning of most permissions should be obvious. The - difference between "accessConsole" and "takeConsole" is - that "takeConsole" allows the user to take control of - the console even if it is already in use by another user. - items: - type: object - description: >- - Permissions can be granted to a user or to a role. - oneOf: - - required: - - user - - required: - - role - properties: - user: - type: string - role: - type: string - may: - type: array - items: - type: string - enum: - - start - - stop - - reset - - accessConsole - - takeConsole - - "*" - default: [] - pools: - type: array - description: >- - List of pools this VM belongs to. - items: - type: string - default: [] - loggingProperties: - type: string - description: >- - Override the default logging properties for - the runner for this VM. vm: type: object description: Defines the VM. @@ -1427,36 +1356,13 @@ spec: display: type: object properties: - outputs: - type: integer - default: 1 - loggedInUser: - description: >- - The name of a user that should be automatically - logged in on the display. Note that this requires - support from an agent in the guest OS. - type: string spice: type: object properties: port: - description: >- - Port number used for the Spice server. type: integer default: 5900 - server: - description: >- - Server (address) to use for connecting - to the spice server. Defaults to the address - of the node that the VM is running on. - type: string - generateSecret: - type: boolean - default: true - proxyUrl: - description: >- - If specified, is copied to the generated - viewer configuration files. + ticket: type: string streamingVideo: type: string @@ -1470,10 +1376,6 @@ spec: type: object default: {} properties: - runnerVersion: - description: >- - The version string of the runner. - type: string cpus: description: >- Number of CPUs currently in use. @@ -1484,50 +1386,6 @@ spec: Amount of memory in use. type: string default: "0" - consoleClient: - description: >- - The hostname of the currently connected client. - type: string - default: "" - consoleUser: - description: >- - The id of the user who has last requested a console - connection. - type: string - default: "" - loggedInUser: - description: >- - The name of a user that is currently logged in by the - VM operator agent. - type: string - displayPasswordSerial: - description: >- - Counts changes of the display password. Set to -1 - by the runner if password protection is not enabled. - type: integer - default: 0 - osinfo: - description: Copy of the OS info provided by the guest agent. - type: object - x-kubernetes-preserve-unknown-fields: true - assignment: - description: >- - The assignment of this VM to a a particular user. - type: object - properties: - pool: - description: >- - The pool this VM is taken from. - type: string - user: - description: >- - The user this VM is assigned to. - type: string - lastUsed: - description: >- - The last time this VM was used by the user. - type: string - default: {} conditions: description: >- List of component conditions observed @@ -1538,30 +1396,6 @@ spec: lastTransitionTime: "1970-01-01T00:00:00Z" reason: Creation message: "Creation of CR" - - type: Booted - status: "False" - observedGeneration: 1 - lastTransitionTime: "1970-01-01T00:00:00Z" - reason: Creation - message: "Creation of CR" - - type: VmopAgentConnected - status: "False" - observedGeneration: 1 - lastTransitionTime: "1970-01-01T00:00:00Z" - reason: Creation - message: "Creation of CR" - - type: UserLoggedIn - status: "False" - observedGeneration: 1 - lastTransitionTime: "1970-01-01T00:00:00Z" - reason: Creation - message: "Creation of CR" - - type: ConsoleConnected - status: "False" - observedGeneration: 1 - lastTransitionTime: "1970-01-01T00:00:00Z" - reason: Creation - message: "Creation of CR" type: array items: type: object diff --git a/deploy/kustomization.yaml b/deploy/kustomization.yaml index bc9e17a..a988f88 100644 --- a/deploy/kustomization.yaml +++ b/deploy/kustomization.yaml @@ -8,7 +8,6 @@ resources: - vmop-image-repository-pvc.yaml - vmop-config-map.yaml - vmop-deployment.yaml -- vmop-service.yaml - vmrunner-role.yaml - vmrunner-service-account.yaml - vmrunner-role-binding.yaml diff --git a/deploy/vmop-config-map.yaml b/deploy/vmop-config-map.yaml index 12d9ccf..2b94f19 100644 --- a/deploy/vmop-config-map.yaml +++ b/deploy/vmop-config-map.yaml @@ -20,5 +20,5 @@ data: java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter java.util.logging.SimpleFormatter.format=%1$tb %1$td %1$tT %4$s %5$s%6$s%n - org.jgrapes.webconlet.logviewer.LogViewerHandler.level=CONFIG + org.jgrapes.webconlet.logviewer.LogViewerHandler.level=FINE \ No newline at end of file diff --git a/deploy/vmop-deployment.yaml b/deploy/vmop-deployment.yaml index 08316f6..648cc39 100644 --- a/deploy/vmop-deployment.yaml +++ b/deploy/vmop-deployment.yaml @@ -21,31 +21,22 @@ spec: - name: vm-operator image: >- ghcr.io/mnlipp/org.jdrupes.vmoperator.manager:latest - imagePullPolicy: Always - env: - - name: JAVA_OPTS - # The VM operator needs about 25 MB of memory, plus 1 MB for - # each VM. The reason is that for the sake of effeciency, we - # have to keep a parsed representation of the CRD in memory, - # which requires about 512 KB per VM. While handling updates, - # we temporarily have the old and the new version of the CRD - # in memory, so we need another 512 KB per VM. - value: "-Xmx128m" - resources: - requests: - cpu: 100m - memory: 128Mi volumeMounts: - name: config mountPath: /etc/opt/vmoperator - name: vmop-image-repository mountPath: /var/local/vmop-image-repository + imagePullPolicy: Always securityContext: capabilities: drop: - ALL readOnlyRootFilesystem: true allowPrivilegeEscalation: false + resources: + requests: + cpu: 100m + memory: 128Mi volumes: - name: config configMap: diff --git a/deploy/vmop-role.yaml b/deploy/vmop-role.yaml index e1ae7bc..e1ea85b 100644 --- a/deploy/vmop-role.yaml +++ b/deploy/vmop-role.yaml @@ -9,15 +9,8 @@ rules: - vmoperator.jdrupes.org resources: - vms - - vmpools verbs: - '*' -- apiGroups: - - vmoperator.jdrupes.org - resources: - - vms/status - verbs: - - patch - apiGroups: - apps resources: @@ -35,12 +28,8 @@ rules: - apiGroups: - "" resources: - - persistentvolumeclaims - pods verbs: - - watch - list - - get - - create - delete - patch diff --git a/deploy/vmop-service.yaml b/deploy/vmop-service.yaml deleted file mode 100644 index ea5cf66..0000000 --- a/deploy/vmop-service.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: vm-operator -spec: - ports: - - port: 8080 - protocol: TCP - targetPort: 8080 - selector: - app.kubernetes.io/name: vm-operator - app.kubernetes.io/component: manager diff --git a/deploy/vmrunner-role.yaml b/deploy/vmrunner-role.yaml index c6df666..54e8742 100644 --- a/deploy/vmrunner-role.yaml +++ b/deploy/vmrunner-role.yaml @@ -12,16 +12,9 @@ rules: verbs: - list - get - - patch - apiGroups: - vmoperator.jdrupes.org resources: - vms/status verbs: - patch -- apiGroups: - - events.k8s.io - resources: - - events - verbs: - - create diff --git a/dev-example/.gitignore b/dev-example/.gitignore deleted file mode 100644 index 1e31cc5..0000000 --- a/dev-example/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -/test-vm-ci.yaml -/kubeconfig.yaml -/crds/ -/.vm-operator-cmd.rc diff --git a/dev-example/Readme.md b/dev-example/Readme.md index d794b24..dfcd3e8 100644 --- a/dev-example/Readme.md +++ b/dev-example/Readme.md @@ -1,16 +1,16 @@ # Example setup for development -The CRD must be deployed independently. Apart from that, the -`kustomize.yaml` - - * creates a small cdrom image repository and - - * deploys the operator in namespace `vmop-dev` with a replica of 0. +The CRD must be deployed independently. Apart from that, the +`kustomize.yaml` +* creates a small cdrom image repository and + +* deploys the operator in namespace `vmop-dev` with a replica of 0. + This allows you to run the manager in your IDE. The `kustomize.yaml` also changes the container image repository for -the operator to a private repository for development. You have to +the operator to a private repository for development. You have to adapt this to your own repository if you also want to test your development version in a container. diff --git a/dev-example/config.yaml b/dev-example/config.yaml index 2a72bc8..26d53d4 100644 --- a/dev-example/config.yaml +++ b/dev-example/config.yaml @@ -1,34 +1,12 @@ # Used for running manager outside Kubernetes. # Keep in sync with kustomize.yaml "/Manager": - # If provided, is shown at top left before namespace - # clusterName: "test" # The controller manages the VM "/Controller": namespace: vmop-dev "/Reconciler": - runnerDataPvc: - storageClassName: rook-cephfs - loadBalancerService: - labels: - label1: label1 - label2: toBeReplaced - annotations: - metallb.universe.tf/loadBalancerIPs: 192.168.168.1 - metallb.universe.tf/ip-allocated-from-pool: single-common - metallb.universe.tf/allow-shared-ip: single-common - loggingProperties: | - # Defaults for namespace (VM domain) - handlers=java.util.logging.ConsoleHandler - - #org.jgrapes.level=FINE - #org.jgrapes.core.handlerTracking.level=FINER - - org.jdrupes.vmoperator.runner.qemu.level=FINEST - - java.util.logging.ConsoleHandler.level=ALL - java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter - java.util.logging.SimpleFormatter.format=%1$tb %1$td %1$tT %4$s %5$s%6$s%n + runnerData: + storageClassName: null "/GuiSocketServer": port: 8888 "/GuiHttpServer": @@ -37,54 +15,23 @@ "/WebConsole": "/LoginConlet": users: - - name: admin - fullName: Administrator - password: "$2b$05$NiBd74ZGdplLC63ePZf1f.UtjMKkbQ23cQoO2OKOFalDBHWAOy21." - - name: operator - fullName: Operator - password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2" - - name: test1 - fullName: Test Account 1 - password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2" - - name: test2 - fullName: Test Account 2 - password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2" - - name: test3 - fullName: Test Account 3 - password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2" + admin: + fullName: Administrator + password: "$2b$05$NiBd74ZGdplLC63ePZf1f.UtjMKkbQ23cQoO2OKOFalDBHWAOy21." + test: + fullName: Test Account + password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2" "/RoleConfigurator": rolesByUser: - # User admin has role admin admin: - admin - operator: - - operator - test1: - - user - test2: - - user - test3: - - user - # All users have role other "*": - - other + - user replace: false "/RoleConletFilter": conletTypesByRole: - # Admins can use all conlets + user: + - "!org.jgrapes.webconlet.sysinfo.SysInfoConlet" + - "*" admin: - "*" - operator: - - org.jdrupes.vmoperator.vmmgmt.VmMgmt - - org.jdrupes.vmoperator.vmaccess.VmAccess - user: - - org.jdrupes.vmoperator.vmaccess.VmAccess - # Others cannot use any conlet (except login conlet to log out) - other: - - org.jgrapes.webconlet.oidclogin.LoginConlet - "/ComponentCollector": - "/VmAccess": - displayResource: - preferredIpVersion: ipv4 - syncPreviewsFor: - - role: user diff --git a/dev-example/gen-pool-vm-crds b/dev-example/gen-pool-vm-crds deleted file mode 100755 index f9cf692..0000000 --- a/dev-example/gen-pool-vm-crds +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash - -function usage() { - cat >&2 <&2 "Unknown option: $1"; exit 1;; - *) template="$1";; - esac - shift -done - -if [ -z "$template" ]; then - usage -fi - -if [ "$count" = "0" ]; then - exit 0 -fi -for number in $(seq 1 $count); do - if [ -z "$prefix" ]; then - prefix=$(basename $template .tpl.yaml) - fi - name="$prefix$(printf %03d $number)" - index=$(($number - 1)) - esh -o $destination/$name.yaml $template number=$number index=$index -done diff --git a/dev-example/kustomization.yaml b/dev-example/kustomization.yaml index 975d95f..5bb4a62 100644 --- a/dev-example/kustomization.yaml +++ b/dev-example/kustomization.yaml @@ -29,71 +29,12 @@ patches: # Keep in sync with config.yaml config.yaml: | "/Manager": - # clusterName: "test" "/Controller": namespace: vmop-dev "/Reconciler": runnerData: storageClassName: null - loadBalancerService: - labels: - label1: label1 - label2: toBeReplaced - annotations: - metallb.universe.tf/loadBalancerIPs: 192.168.168.1 - metallb.universe.tf/ip-allocated-from-pool: single-common - metallb.universe.tf/allow-shared-ip: single-common - "/GuiSocketServer": - port: 8888 - "/GuiHttpServer": - # This configures the GUI - "/ConsoleWeblet": - "/WebConsole": - "/LoginConlet": - users: - - name: admin - fullName: Administrator - password: "$2b$05$NiBd74ZGdplLC63ePZf1f.UtjMKkbQ23cQoO2OKOFalDBHWAOy21." - - name: test1 - fullName: Test Account - password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2" - - name: test2 - fullName: Test Account - password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2" - - name: test3 - fullName: Test Account - password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2" - "/RoleConfigurator": - rolesByUser: - # User admin has role admin - admin: - - admin - test1: - - user - test2: - - user - test3: - - user - # All users have role other - "*": - - other - replace: false - "/RoleConletFilter": - conletTypesByRole: - # Admins can use all conlets - admin: - - "*" - user: - - org.jdrupes.vmoperator.vmviewer.VmViewer - # Others cannot use any conlet (except login conlet to log out) - other: - - org.jgrapes.webconlet.locallogin.LoginConlet - "/ComponentCollector": - "/VmAccess": - displayResource: - preferredIpVersion: ipv4 - syncPreviewsFor: - - role: user + - target: group: apps version: v1 diff --git a/dev-example/pool-action b/dev-example/pool-action deleted file mode 100755 index bc8fbce..0000000 --- a/dev-example/pool-action +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/bash - -function usage() { - cat >&2 <&2 "Unknown option: $1"; exit 1;; - *) if [ ! -v pool ]; then - pool="$1" - elif [ ! -v action ]; then - action="$1" - else - usage - fi;; - esac - shift -done - -if [ ! -v pool -o ! -v "action" -o ! -v context ]; then - echo >&2 "Missing arguments or context not set." - echo >&2 - usage -fi -case "$action" in - "start"|"stop"|"delete"|"delete-disks") ;; - *) usage;; -esac - -kubectl --context="$context" -n "$namespace" get vms -o json \ - | jq -r '.items[] | select(.spec.pools | contains(["'${pool}'"])) | .metadata.name' \ -| while read vmName; do - case "$action" in - start) kubectl --context="$context" -n "$namespace" patch vms "$vmName" \ - --type='merge' -p '{"spec":{"vm":{"state":"Running"}}}';; - stop) kubectl --context="$context" -n "$namespace" patch vms "$vmName" \ - --type='merge' -p '{"spec":{"vm":{"state":"Stopped"}}}';; - delete) kubectl --context="$context" -n "$namespace" delete vm/"$vmName";; - delete-disks) kubectl --context="$context" -n "$namespace" delete \ - pvc -l app.kubernetes.io/instance="$vmName" ;; - esac -done diff --git a/dev-example/test-pool.yaml b/dev-example/test-pool.yaml deleted file mode 100644 index 497aaf7..0000000 --- a/dev-example/test-pool.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: "vmoperator.jdrupes.org/v1" -kind: VmPool -metadata: - namespace: vmop-dev - name: test-vms -spec: - retention: "PT1m" - loginOnAssignment: true - permissions: - - user: admin - may: - - accessConsole - - start - - role: user - may: - - accessConsole - - start diff --git a/dev-example/test-vm-display-secret.yaml b/dev-example/test-vm-display-secret.yaml deleted file mode 100644 index a6f0fe6..0000000 --- a/dev-example/test-vm-display-secret.yaml +++ /dev/null @@ -1,13 +0,0 @@ -kind: Secret -apiVersion: v1 -metadata: - name: test-vm-display-secret - namespace: vmop-dev - labels: - app.kubernetes.io/name: vm-runner - app.kubernetes.io/instance: test-vm - app.kubernetes.io/component: display-secret -type: Opaque -data: - display-password: dGVzdC12bQ== - password-expiry: KzMw diff --git a/dev-example/test-vm-shell.yaml b/dev-example/test-vm-shell.yaml deleted file mode 100644 index 8137694..0000000 --- a/dev-example/test-vm-shell.yaml +++ /dev/null @@ -1,30 +0,0 @@ -kind: Pod -apiVersion: v1 -metadata: - name: test-vm-shell - namespace: vmop-dev -spec: - volumes: - - name: test-vm-system-disk - persistentVolumeClaim: - claimName: system-disk-test-vm-0 - - name: vmop-image-repository - persistentVolumeClaim: - claimName: vmop-image-repository - containers: - - name: test-vm-shell - image: archlinux/archlinux - args: - - bash - imagePullPolicy: Always - stdin: true - stdinOnce: true - tty: true - volumeDevices: - - name: test-vm-system-disk - devicePath: /dev/test-vm-system-disk - volumeMounts: - - name: vmop-image-repository - mountPath: /var/local/vmop-image-repository - securityContext: - privileged: true diff --git a/dev-example/test-vm-snapshot.yaml b/dev-example/test-vm-snapshot.yaml deleted file mode 100644 index fd60a25..0000000 --- a/dev-example/test-vm-snapshot.yaml +++ /dev/null @@ -1,10 +0,0 @@ ---- -apiVersion: snapshot.storage.k8s.io/v1 -kind: VolumeSnapshot -metadata: - namespace: vmop-dev - name: test-vm-system-disk-snapshot -spec: - volumeSnapshotClassName: csi-rbdplugin-snapclass - source: - persistentVolumeClaimName: test-vm-system-disk diff --git a/dev-example/test-vm.tpl.yaml b/dev-example/test-vm.tpl.yaml deleted file mode 100644 index 76adfba..0000000 --- a/dev-example/test-vm.tpl.yaml +++ /dev/null @@ -1,66 +0,0 @@ -apiVersion: "vmoperator.jdrupes.org/v1" -kind: VirtualMachine -metadata: - namespace: vmop-dev - name: test-vm<%= $(printf "%02d" ${number}) %> - annotations: - argocd.argoproj.io/sync-wave: "20" - -spec: - image: - source: ghcr.io/mnlipp/org.jdrupes.vmoperator.runner.qemu-arch:latest -# source: registry.mnl.de/org/jdrupes/vm-operator/org.jdrupes.vmoperator.runner.qemu-arch:testing -# source: docker-registry.lan.mnl.de/vmoperator/org.jdrupes.vmoperator.runner.qemu-arch:latest - pullPolicy: Always - - runnerTemplate: - update: true - - permissions: - - role: admin - may: - - "*" - - guestShutdownStops: true - - cloudInit: - metaData: {} - - pools: - - test-vms - - vm: - # state: Running - bootMenu: true - maximumCpus: 4 - currentCpus: 2 - maximumRam: 6Gi - currentRam: 4Gi - - networks: - # No bridge on TC1 - # - tap: {} - - user: {} - - disks: - - volumeClaimTemplate: - metadata: - name: system - spec: - storageClassName: ceph-rbd3slow - dataSource: - name: test-vm-system-disk-snapshot - kind: VolumeSnapshot - apiGroup: snapshot.storage.k8s.io - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 40Gi - - cdrom: - image: "" - # image: https://download.fedoraproject.org/pub/fedora/linux/releases/38/Workstation/x86_64/iso/Fedora-Workstation-Live-x86_64-38-1.6.iso - - display: - spice: - port: <%= $((5910 + number)) %> diff --git a/dev-example/test-vm.yaml b/dev-example/test-vm.yaml index aa75bc3..0cd820b 100644 --- a/dev-example/test-vm.yaml +++ b/dev-example/test-vm.yaml @@ -5,23 +5,15 @@ metadata: name: test-vm spec: image: - source: registry.mnl.de/org/jdrupes/vm-operator/org.jdrupes.vmoperator.runner.qemu-arch:testing + repository: docker-registry.lan.mnl.de + path: vmoperator/org.jdrupes.vmoperator.runner.qemu-alpine pullPolicy: Always - permissions: - - user: admin - may: - - "*" - resources: requests: cpu: 1 memory: 2Gi - - guestShutdownStops: true - - cloudInit: {} - + vm: # state: Running bootMenu: yes @@ -32,9 +24,8 @@ spec: currentCpus: 4 networks: - # No bridge on test cluster - - user: {} - + - tap: + mac: "00:16:3e:33:58:10" disks: - volumeClaimTemplate: metadata: @@ -57,6 +48,3 @@ spec: display: spice: port: 5810 - generateSecret: true - - loadBalancerService: {} diff --git a/dev-example/vmop-agent/99-vmop-agent.rules b/dev-example/vmop-agent/99-vmop-agent.rules deleted file mode 100644 index 4a18472..0000000 --- a/dev-example/vmop-agent/99-vmop-agent.rules +++ /dev/null @@ -1,2 +0,0 @@ -SUBSYSTEM=="virtio-ports", ATTR{name}=="org.jdrupes.vmop_agent.0", \ - TAG+="systemd" ENV{SYSTEMD_WANTS}="vmop-agent.service" diff --git a/dev-example/vmop-agent/gdm/PostLogin/Default b/dev-example/vmop-agent/gdm/PostLogin/Default deleted file mode 100755 index 8a70890..0000000 --- a/dev-example/vmop-agent/gdm/PostLogin/Default +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -sed -i '/AutomaticLogin/d' /etc/gdm/custom.conf diff --git a/dev-example/vmop-agent/vmop-agent b/dev-example/vmop-agent/vmop-agent deleted file mode 100755 index 9f4d9e7..0000000 --- a/dev-example/vmop-agent/vmop-agent +++ /dev/null @@ -1,146 +0,0 @@ -#!/usr/bin/bash - -# Note that this script requires "jq" to be installed and a version -# of loginctl that accepts the "-j" option. - -while [ "$#" -gt 0 ]; do - case "$1" in - --path) shift; ttyPath="$1";; - --path=*) IFS='=' read -r option value <<< "$1"; ttyPath="$value";; - esac - shift -done - -ttyPath="${ttyPath:-/dev/virtio-ports/org.jdrupes.vmop_agent.0}" - -if [ ! -w "$ttyPath" ]; then - echo >&2 "Device $ttyPath not writable" - exit 1 -fi - -# Create fd for the tty in variable con -if ! exec {con}<>"$ttyPath"; then - echo >&2 "Cannot open device $ttyPath" - exit 1 -fi - -# Temporary file for logging error messages, clear tty and signal ready -temperr=$(mktemp) -clear >/dev/tty1 -echo >&${con} "220 Hello" - -# This script uses the (shared) home directory as "dictonary" for -# synchronizing the username and the uid between hosts. -# -# Every user has a directory with his username. The directory is -# owned by root to prevent changes of access rights by the user. -# The uid and gid of the directory are equal. Thus the name of the -# directory and the id from the group ownership also provide the -# association between the username and the uid. - -# Add the user with name $1 to the host's "user database". This -# may not be invoked concurrently. -createUser() { - local missing=$1 - local uid - local userHome="/home/$missing" - local createOpts="" - - # Retrieve or create the uid for the username - if [ -d "$userHome" ]; then - # If a home directory exists, use the id from the group ownership as uid - uid=$(ls -ldn "$userHome" | head -n 1 | awk '{print $4}') - createOpts="--no-create-home" - else - # Else get the maximum of all ids from the group ownership +1 - uid=$(ls -ln "/home" | tail -n +2 | awk '{print $4}' | sort | tail -1) - uid=$(( $uid + 1 )) - if [ $uid -lt 1100 ]; then - uid=1100 - fi - createOpts="--create-home" - fi - groupadd -g $uid $missing - useradd $missing -u $uid -g $uid $createOpts -} - -# Login the user, i.e. create a desktopn for the user. -doLogin() { - user=$1 - if [ "$user" = "root" ]; then - echo >&${con} "504 Won't log in root" - return - fi - - # Check if this user is already logged in on tty2 - curUser=$(loginctl -j | jq -r '.[] | select(.tty=="tty2") | .user') - if [ "$curUser" = "$user" ]; then - echo >&${con} "201 User already logged in" - return - fi - - # Terminate a running desktop (fail safe) - attemptLogout - - # Check if username is known on this host. If not, create user - uid=$(id -u ${user} 2>/dev/null) - if [ $? != 0 ]; then - ( flock 200 - createUser ${user} - ) 200>/home/.gen-uid-lock - - # This should now work, else something went wrong - uid=$(id -u ${user} 2>/dev/null) - if [ $? != 0 ]; then - echo >&${con} "451 Cannot determine uid" - return - fi - fi - - # Configure user as auto login user - sed -i '/AutomaticLogin/d' /etc/gdm/custom.conf - sed -i '/\[daemon\]/a AutomaticLoginEnable=true\nAutomaticLogin='$user \ - /etc/gdm/custom.conf - - # Activate user - systemctl restart gdm - if [ $? -eq 0 ]; then - echo >&${con} "201 User logged in successfully" - else - echo >&${con} "451 $(tr '\n' ' ' <${temperr})" - fi -} - -# Attempt to log out a user currently using tty1. This is an intermediate -# operation that can be invoked from other operations -attemptLogout() { - sed -i '/AutomaticLogin/d' /etc/gdm/custom.conf - systemctl stop gdm - echo >&${con} "102 Desktop stopped" -} - -# Log out any user currently using tty1. This is invoked when executing -# the logout command and therefore sends back a 2xx return code. -# Also try to restart gdm, if it is not running. -doLogout() { - attemptLogout - systemctl restart gdm - echo >&${con} "202 User logged out" -} - -while read line <&${con}; do - case $line in - "login "*) IFS=' ' read -ra args <<< "$line"; doLogin ${args[1]};; - "logout") doLogout;; - esac -done - -onExit() { - doLogout - if [ -n "$temperr" ]; then - rm -f $temperr - fi - echo >&${con} "240 Quit" -} - -trap onExit EXIT diff --git a/dev-example/vmop-agent/vmop-agent.service b/dev-example/vmop-agent/vmop-agent.service deleted file mode 100644 index 11c64f2..0000000 --- a/dev-example/vmop-agent/vmop-agent.service +++ /dev/null @@ -1,15 +0,0 @@ -[Unit] -Description=VM-Operator (Guest) Agent -BindsTo=dev-virtio\x2dports-org.jdrupes.vmop_agent.0.device -After=dev-virtio\x2dports-org.jdrupes.vmop_agent.0.device multi-user.target -IgnoreOnIsolate=True - -[Service] -UMask=0077 -#EnvironmentFile=/etc/sysconfig/vmop-agent -ExecStart=/usr/local/libexec/vmop-agent -Restart=always -RestartSec=0 - -[Install] -WantedBy=dev-virtio\x2dports-org.jdrupes.vmop_agent.0.device diff --git a/example/local-path/Readme.md b/example/local-path/Readme.md index bdba8cc..7afb948 100644 --- a/example/local-path/Readme.md +++ b/example/local-path/Readme.md @@ -1,17 +1,17 @@ # Example setup -The CRD must be deployed independently. +The CRD must be deployed independently. ```sh kubectl apply -f https://github.com/mnlipp/VM-Operator/raw/main/deploy/crds/vms-crd.yaml ``` -Apart from that, the `kustomize.yaml` defines a namespace for the manager +Apart from that, the `kustomize.yaml` defines a namespace for the manager (and the VMs managed by it) and patches the repository PVC to create a small volume using local-path. -A second patch provides a new configuration file for the manager -that makes it use the local-path storage class when creating the +A second patch provides a new configuration file for the manager +that makes it use the local-path storage class when creating the small volume for a runner's data. The `kustomize.yaml` does not include the test VM. Before creating diff --git a/example/rook-ceph/Readme.md b/example/rook-ceph/Readme.md index 3756e93..1d2cfc6 100644 --- a/example/rook-ceph/Readme.md +++ b/example/rook-ceph/Readme.md @@ -1,12 +1,12 @@ # Example setup -The CRD must be deployed independently. +The CRD must be deployed independently. ```sh kubectl apply -f https://github.com/mnlipp/VM-Operator/raw/main/deploy/crds/vms-crd.yaml ``` -Apart from that, the `kustomize.yaml` defines a namespace for the manager +Apart from that, the `kustomize.yaml` defines a namespace for the manager (and the VMs managed by it) and applies patches to use `rook-cephfs` as storage class (instead of the default storage class). diff --git a/gradle.properties b/gradle.properties deleted file mode 100644 index f97ebb7..0000000 --- a/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.parallel=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e644113..ccebba7 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a441313..8707e8b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip networkTimeout=10000 -validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index b740cf1..79a61d4 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -83,8 +83,10 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -131,13 +133,10 @@ location of your Java installation." fi else JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." - fi fi # Increase the maximum file descriptors if we can. @@ -145,7 +144,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 + # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -153,7 +152,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 + # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -198,15 +197,11 @@ if "$cygwin" || "$msys" ; then done fi - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, -# and any embedded shellness will be escaped. -# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be -# treated as '${Hostname}' itself on the command line. +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradlew.bat b/gradlew.bat index 25da30d..93e3f59 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -43,11 +43,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. goto fail @@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. goto fail diff --git a/misc/DejaVuSans-Bold.woff2 b/misc/DejaVuSans-Bold.woff2 new file mode 100644 index 0000000..373095f Binary files /dev/null and b/misc/DejaVuSans-Bold.woff2 differ diff --git a/misc/DejaVuSans.woff2 b/misc/DejaVuSans.woff2 new file mode 100644 index 0000000..8437d4e Binary files /dev/null and b/misc/DejaVuSans.woff2 differ diff --git a/misc/DejaVuSansMono-Bold.woff2 b/misc/DejaVuSansMono-Bold.woff2 new file mode 100644 index 0000000..f2b469a Binary files /dev/null and b/misc/DejaVuSansMono-Bold.woff2 differ diff --git a/misc/DejaVuSansMono.woff2 b/misc/DejaVuSansMono.woff2 new file mode 100644 index 0000000..cf200e1 Binary files /dev/null and b/misc/DejaVuSansMono.woff2 differ diff --git a/misc/DejaVuSerif-Bold.woff2 b/misc/DejaVuSerif-Bold.woff2 new file mode 100644 index 0000000..655ac56 Binary files /dev/null and b/misc/DejaVuSerif-Bold.woff2 differ diff --git a/misc/DejaVuSerif.woff2 b/misc/DejaVuSerif.woff2 new file mode 100644 index 0000000..238566d Binary files /dev/null and b/misc/DejaVuSerif.woff2 differ diff --git a/misc/javadoc-overwrites.css b/misc/javadoc-overwrites.css deleted file mode 100644 index 7eed81f..0000000 --- a/misc/javadoc-overwrites.css +++ /dev/null @@ -1,2 +0,0 @@ -:root { --body-font-size: 16px;} -:root { --code-font-size: 16px;} diff --git a/misc/javadoc.bottom.txt b/misc/javadoc.bottom.txt index d5589ac..bf7dd56 100644 --- a/misc/javadoc.bottom.txt +++ b/misc/javadoc.bottom.txt @@ -4,33 +4,26 @@ TermsPrivacy

- - - - - \ No newline at end of file diff --git a/misc/stylesheet.css b/misc/stylesheet.css new file mode 100644 index 0000000..e21b9b2 --- /dev/null +++ b/misc/stylesheet.css @@ -0,0 +1,912 @@ +/* + * 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/.eclipse-pmd b/org.jdrupes.vmoperator.common/.eclipse-pmd index 5d69caa..8b394f8 100644 --- a/org.jdrupes.vmoperator.common/.eclipse-pmd +++ b/org.jdrupes.vmoperator.common/.eclipse-pmd @@ -2,6 +2,6 @@ - + diff --git a/org.jdrupes.vmoperator.common/.settings/net.sf.jautodoc.prefs b/org.jdrupes.vmoperator.common/.settings/net.sf.jautodoc.prefs deleted file mode 100644 index 8b8b906..0000000 --- a/org.jdrupes.vmoperator.common/.settings/net.sf.jautodoc.prefs +++ /dev/null @@ -1,7 +0,0 @@ -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/build.gradle b/org.jdrupes.vmoperator.common/build.gradle index e72cb14..ed082a1 100644 --- a/org.jdrupes.vmoperator.common/build.gradle +++ b/org.jdrupes.vmoperator.common/build.gradle @@ -10,8 +10,5 @@ plugins { 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]' + api 'io.kubernetes:client-java:[18.0.0,19)' } 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 index b9de69f..22decd3 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Constants.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Constants.java @@ -21,107 +21,14 @@ 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 VM_OP_GROUP. */ + public static final String VM_OP_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"; - - } + /** The Constant VM_OP_KIND_VM. */ + public static final String VM_OP_KIND_VM = "VirtualMachine"; } diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Convertions.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Convertions.java index 68f52eb..47b7208 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Convertions.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Convertions.java @@ -32,11 +32,13 @@ import java.util.regex.Pattern; public class Convertions { @SuppressWarnings({ "PMD.UseConcurrentHashMap", - "PMD.FieldNamingConventions" }) + "PMD.FieldNamingConventions", "PMD.VariableNamingConventions" }) private static final Map unitMap = new HashMap<>(); - @SuppressWarnings({ "PMD.FieldNamingConventions" }) + @SuppressWarnings({ "PMD.FieldNamingConventions", + "PMD.VariableNamingConventions" }) private static final List> unitMappings; - @SuppressWarnings({ "PMD.FieldNamingConventions" }) + @SuppressWarnings({ "PMD.FieldNamingConventions", + "PMD.VariableNamingConventions" }) private static final Pattern memorySize = Pattern.compile("^\\s*(\\d+(\\.\\d+)?)\\s*([A-Za-z]*)\\s*"); @@ -67,6 +69,7 @@ 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 deleted file mode 100644 index d21eed4..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/DynamicTypeAdapterFactory.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -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 index 3870337..5a87ecd 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8s.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8s.java @@ -1,6 +1,6 @@ /* * VM-Operator - * Copyright (C) 2023,2024 Michael N. Lipp + * 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 @@ -18,141 +18,168 @@ 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.apis.ApisApi; +import io.kubernetes.client.openapi.apis.CustomObjectsApi; +import io.kubernetes.client.openapi.models.V1APIGroup; +import io.kubernetes.client.openapi.models.V1ConfigMap; +import io.kubernetes.client.openapi.models.V1ConfigMapList; +import io.kubernetes.client.openapi.models.V1GroupVersionForDiscovery; import io.kubernetes.client.openapi.models.V1ObjectMeta; -import io.kubernetes.client.openapi.models.V1ObjectReference; -import io.kubernetes.client.util.Strings; +import io.kubernetes.client.openapi.models.V1PersistentVolumeClaim; +import io.kubernetes.client.openapi.models.V1PersistentVolumeClaimList; +import io.kubernetes.client.openapi.models.V1Pod; +import io.kubernetes.client.openapi.models.V1PodList; import io.kubernetes.client.util.generic.GenericKubernetesApi; -import io.kubernetes.client.util.generic.KubernetesApiResponse; +import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi; +import io.kubernetes.client.util.generic.options.DeleteOptions; 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" }) +@SuppressWarnings({ "PMD.ShortClassName", "PMD.UseUtilityClass", + "PMD.DataflowAnomalyAnalysis" }) 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. + * Given a groupVersion, returns only the version. * - * @param the generic type - * @param response the response - * @return the optional - * @throws ApiException the API exception + * @param groupVersion the group version + * @return the string */ - public static Optional - optional(KubernetesApiResponse response) throws ApiException { - if (response.isSuccess()) { - return Optional.of(response.getObject()); + public static String version(String groupVersion) { + return groupVersion.substring(groupVersion.lastIndexOf('/') + 1); + } + + /** + * Get PVC API. + * + * @param client the client + * @return the generic kubernetes api + */ + public static GenericKubernetesApi pvcApi(ApiClient client) { + return new GenericKubernetesApi<>(V1PersistentVolumeClaim.class, + V1PersistentVolumeClaimList.class, "", "v1", + "persistentvolumeclaims", client); + } + + /** + * Get config map API. + * + * @param client the client + * @return the generic kubernetes api + */ + public static GenericKubernetesApi cmApi(ApiClient client) { + return new GenericKubernetesApi<>(V1ConfigMap.class, + V1ConfigMapList.class, "", "v1", "configmaps", client); + } + + /** + * Get pod API. + * + * @param client the client + * @return the generic kubernetes api + */ + public static GenericKubernetesApi + podApi(ApiClient client) { + return new GenericKubernetesApi<>(V1Pod.class, V1PodList.class, "", + "v1", "pods", client); + } + + /** + * Get the API for a custom resource. + * + * @param client the client + * @param group the group + * @param kind the kind + * @param namespace the namespace + * @param name the name + * @return the dynamic kubernetes api + * @throws ApiException the api exception + */ + @SuppressWarnings("PMD.UseObjectForClearerAPI") + public static Optional crApi(ApiClient client, + String group, String kind, String namespace, String name) + throws ApiException { + var apis = new ApisApi(client).getAPIVersions(); + var crdVersions = apis.getGroups().stream() + .filter(g -> g.getName().equals(group)).findFirst() + .map(V1APIGroup::getVersions).stream().flatMap(l -> l.stream()) + .map(V1GroupVersionForDiscovery::getVersion).toList(); + var coa = new CustomObjectsApi(client); + for (var crdVersion : crdVersions) { + var crdApiRes = coa.getAPIResources(group, crdVersion) + .getResources().stream().filter(r -> kind.equals(r.getKind())) + .findFirst(); + if (crdApiRes.isEmpty()) { + continue; + } + @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") + var crApi = new DynamicKubernetesApi(group, + crdVersion, crdApiRes.get().getName(), client); + var customResource = crApi.get(namespace, name); + if (customResource.isSuccess()) { + return Optional.of(crApi); + } } - 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. + * Get an object from its metadata. * - * @param context the context - * @param version the version - * @return the API resource + * @param the generic type + * @param the generic type + * @param api the api + * @param meta the meta + * @return the object */ - 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(); + public static + Optional + get(GenericKubernetesApi api, V1ObjectMeta meta) { + var response = api.get(meta.getNamespace(), meta.getName()); + if (response.isSuccess()) { + return Optional.of(response.getObject()); } - 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); + return Optional.empty(); + } + + /** + * Delete an object. + * + * @param the generic type + * @param the generic type + * @param api the api + * @param object the object + */ + public static + void delete(GenericKubernetesApi api, T object) + throws ApiException { + api.delete(object.getMetadata().getNamespace(), + object.getMetadata().getName()).throwsApiException(); + } + + /** + * Delete an object. + * + * @param the generic type + * @param the generic type + * @param api the api + * @param object the object + */ + public static + void delete(GenericKubernetesApi api, T object, + DeleteOptions options) throws ApiException { + api.delete(object.getMetadata().getNamespace(), + object.getMetadata().getName(), options).throwsApiException(); } /** @@ -163,10 +190,8 @@ public class K8s { * @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 { @@ -179,71 +204,4 @@ public class K8s { 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 deleted file mode 100644 index 272da2b..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sClient.java +++ /dev/null @@ -1,954 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -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 deleted file mode 100644 index 59b4d12..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sClusterGenericStub.java +++ /dev/null @@ -1,396 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -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 deleted file mode 100644 index 2392d3e..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModel.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -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 deleted file mode 100644 index d165c10..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModels.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -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 deleted file mode 100644 index 4e21c0e..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModelsBase.java +++ /dev/null @@ -1,174 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -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 deleted file mode 100644 index c0303c2..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicStub.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -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 deleted file mode 100644 index ae3f012..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicStubBase.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -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 deleted file mode 100644 index 9ba376f..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sGenericStub.java +++ /dev/null @@ -1,474 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -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 deleted file mode 100644 index 9e22382..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sObserver.java +++ /dev/null @@ -1,237 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -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 deleted file mode 100644 index 07c59b2..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1ConfigMapStub.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -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 deleted file mode 100644 index 9075a84..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1DeploymentStub.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -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 deleted file mode 100644 index ea1237d..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1NodeStub.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -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 deleted file mode 100644 index f21bb47..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1PodStub.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -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 deleted file mode 100644 index c46a60f..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1PvcStub.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -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 deleted file mode 100644 index 9c1c086..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1SecretStub.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -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 deleted file mode 100644 index 863f86f..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1ServiceStub.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -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 deleted file mode 100644 index be30b00..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1StatefulSetStub.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -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 deleted file mode 100644 index a0b66bf..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinition.java +++ /dev/null @@ -1,499 +0,0 @@ -/* - * 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.> fromStatus(Status.ASSIGNMENT) - .filter(m -> !m.isEmpty()).map(a -> new Assignment( - a.get("pool").toString(), a.get("user").toString(), - Instant.parse(a.get("lastUsed").toString()))); - } - - /** - * Return a condition from the status. - * - * @param name the condition's name - * @return the status, if the condition is defined - */ - public Optional condition(String name) { - return this.>> fromStatus("conditions") - .orElse(Collections.emptyList()).stream() - .filter(cond -> DataPath.get(cond, "type") - .map(name::equals).orElse(false)) - .findFirst() - .map(cond -> objectMapper.convertValue(cond, V1Condition.class)); - } - - /** - * Return a condition's status. - * - * @param name the condition's name - * @return the status, if the condition is defined - */ - public Optional conditionStatus(String name) { - return this.>> fromStatus("conditions") - .orElse(Collections.emptyList()).stream() - .filter(cond -> DataPath.get(cond, "type") - .map(name::equals).orElse(false)) - .findFirst().map(cond -> DataPath.get(cond, "status") - .map("True"::equals).orElse(false)); - } - - /** - * Return true if the console is in use. - * - * @return true, if successful - */ - public boolean consoleConnected() { - return conditionStatus("ConsoleConnected").orElse(false); - } - - /** - * Return the last known console user. - * - * @return the optional - */ - public Optional consoleUser() { - return this. fromStatus(Status.CONSOLE_USER); - } - - /** - * Set extra data (unknown to kubernetes). - * @return the VM definition - */ - /* default */ VmDefinition extra(VmExtraData extraData) { - this.extraData = extraData; - return this; - } - - /** - * Return the extra data. - * - * @return the data - */ - public VmExtraData extra() { - return extraData; - } - - /** - * Returns the definition's name. - * - * @return the string - */ - public String name() { - return metadata().getName(); - } - - /** - * Returns the definition's namespace. - * - * @return the string - */ - public String namespace() { - return metadata().getNamespace(); - } - - /** - * Return the requested VM state. - * - * @return the string - */ - public RequestedVmState vmState() { - return fromVm("state") - .map(s -> "Running".equals(s) ? RequestedVmState.RUNNING - : RequestedVmState.STOPPED) - .orElse(RequestedVmState.STOPPED); - } - - /** - * Collect all permissions for the given user with the given roles. - * If permission "takeConsole" is granted, the result will also - * contain "accessConsole" to simplify checks. - * - * @param user the user - * @param roles the roles - * @return the sets the - */ - public Set permissionsFor(String user, - Collection roles) { - var result = this.>> fromSpec("permissions") - .orElse(Collections.emptyList()).stream() - .filter(p -> DataPath.get(p, "user").map(u -> u.equals(user)) - .orElse(false) - || DataPath.get(p, "role").map(roles::contains).orElse(false)) - .map(p -> DataPath.> get(p, "may") - .orElse(Collections.emptyList()).stream()) - .flatMap(Function.identity()) - .map(Permission::parse).map(Set::stream) - .flatMap(Function.identity()) - .collect(Collectors.toCollection(HashSet::new)); - - // Take console implies access console, simplify checks - if (result.contains(Permission.TAKE_CONSOLE)) { - result.add(Permission.ACCESS_CONSOLE); - } - return result; - } - - /** - * Check if the console is accessible. Always returns `true` if - * the VM is running and the permissions allow taking over the - * console. Else, returns `true` if - * - * * the permissions allow access to the console and - * - * * the VM is running and - * - * * the console is currently unused or used by the given user and - * - * * if user login is requested, the given user is logged in. - * - * @param user the user - * @param permissions the permissions - * @return true, if successful - */ - @SuppressWarnings("PMD.SimplifyBooleanReturns") - public boolean consoleAccessible(String user, Set permissions) { - // Basic checks - if (!conditionStatus(Condition.RUNNING).orElse(false)) { - return false; - } - if (permissions.contains(Permission.TAKE_CONSOLE)) { - return true; - } - if (!permissions.contains(Permission.ACCESS_CONSOLE)) { - return false; - } - - // If the console is in use by another user, deny access - if (conditionStatus(Condition.CONSOLE_CONNECTED).orElse(false) - && !consoleUser().map(cu -> cu.equals(user)).orElse(false)) { - return false; - } - - // If no login is requested, allow access, else check if user matches - if (condition(Condition.USER_LOGGED_IN).map(V1Condition::getReason) - .map(r -> Reason.NOT_REQUESTED.equals(r)).orElse(false)) { - return true; - } - return user.equals(status().get(Status.LOGGED_IN_USER)); - } - - /** - * Get the display password serial. - * - * @return the optional - */ - public Optional displayPasswordSerial() { - return this. fromStatus(Status.DISPLAY_PASSWORD_SERIAL) - .map(Number::longValue); - } - - /** - * Hash code. - * - * @return the int - */ - @Override - public int hashCode() { - return Objects.hash(metadata().getNamespace(), metadata().getName()); - } - - /** - * Equals. - * - * @param obj the obj - * @return true, if successful - */ - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - VmDefinition other = (VmDefinition) obj; - return Objects.equals(metadata().getNamespace(), - other.metadata().getNamespace()) - && Objects.equals(metadata().getName(), other.metadata().getName()); - } - - /** - * The Class Model. - */ - public static class Model { - - private Map spec; - private Map status; - - /** - * Gets the spec. - * - * @return the spec - */ - public Map getSpec() { - return spec; - } - - /** - * Sets the spec. - * - * @param spec the spec to set - */ - public void setSpec(Map spec) { - this.spec = spec; - } - - /** - * Gets the status. - * - * @return the status - */ - public Map getStatus() { - return status; - } - - /** - * Sets the status. - * - * @param status the status to set - */ - public void setStatus(Map status) { - this.status = status; - } - - } - -} diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionStub.java deleted file mode 100644 index 377220a..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitionStub.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -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 VmDefinitionStub - extends K8sDynamicStubBase { - - private static DynamicTypeAdapterFactory taf = new VmDefintionModelTypeAdapterFactory(); - - /** - * Instantiates a new stub for VM defintions. - * - * @param client the client - * @param context the context - * @param namespace the namespace - * @param name the name - */ - public VmDefinitionStub(K8sClient client, APIResource context, - String namespace, String name) { - super(VmDefinition.class, VmDefinitions.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 VmDefinitionStub get(K8sClient client, - GroupVersionKind gvk, String namespace, String name) - throws ApiException { - return new VmDefinitionStub(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 VmDefinitionStub get(K8sClient client, - APIResource context, String namespace, String name) { - return new VmDefinitionStub(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 VmDefinitionStub createFromYaml(K8sClient client, - APIResource context, Reader yaml) throws ApiException { - var model = new VmDefinition(client.getJSON().getGson(), - K8s.yamlToJson(client, yaml)); - return K8sGenericStub.create(VmDefinition.class, - VmDefinitions.class, client, context, model, - (c, ns, n) -> new VmDefinitionStub(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(VmDefinition.class, - VmDefinitions.class, client, context, namespace, options, - (c, ns, n) -> new VmDefinitionStub(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 VmDefinitionModel(s) objects. - */ - public static class VmDefintionModelTypeAdapterFactory extends - DynamicTypeAdapterFactory { - - /** - * Instantiates a new dynamic model type adapter factory. - */ - public VmDefintionModelTypeAdapterFactory() { - super(VmDefinition.class, VmDefinitions.class); - } - } - -} \ No newline at end of file diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitions.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitions.java deleted file mode 100644 index c79654e..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinitions.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.common; - -import com.google.gson.Gson; -import com.google.gson.JsonObject; - -/** - * Represents a list of {@link VmDefinition}s. - */ -public class VmDefinitions - 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 VmDefinitions(Gson delegate, JsonObject data) { - super(VmDefinition.class, delegate, data); - } -} diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmExtraData.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmExtraData.java deleted file mode 100644 index e1565c5..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmExtraData.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * 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 io.kubernetes.client.util.Strings; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Represents internally used dynamic data associated with a - * {@link VmDefinition}. - */ -public class VmExtraData { - - private static final Logger logger - = Logger.getLogger(VmExtraData.class.getName()); - - private final VmDefinition vmDef; - private String nodeName = ""; - private List nodeAddresses = Collections.emptyList(); - private long resetCount; - - /** - * Initializes a new instance. - * - * @param vmDef the VM definition - */ - public VmExtraData(VmDefinition vmDef) { - this.vmDef = vmDef; - vmDef.extra(this); - } - - /** - * Sets the node info. - * - * @param name the name - * @param addresses the addresses - * @return the VM extra data - */ - public VmExtraData nodeInfo(String name, List addresses) { - nodeName = name; - nodeAddresses = addresses; - return this; - } - - /** - * Return the node name. - * - * @return the string - */ - public String nodeName() { - return nodeName; - } - - /** - * Gets the node addresses. - * - * @return the nodeAddresses - */ - public List nodeAddresses() { - return nodeAddresses; - } - - /** - * Sets the reset count. - * - * @param resetCount the reset count - * @return the vm extra data - */ - public VmExtraData resetCount(long resetCount) { - this.resetCount = resetCount; - return this; - } - - /** - * Returns the reset count. - * - * @return the long - */ - public long resetCount() { - return resetCount; - } - - /** - * Create a connection file. - * - * @param password the password - * @param preferredIpVersion the preferred IP version - * @param deleteConnectionFile the delete connection file - * @return the string - */ - public Optional connectionFile(String password, - Class preferredIpVersion, boolean deleteConnectionFile) { - var addr = displayIp(preferredIpVersion); - if (addr.isEmpty()) { - logger - .severe(() -> "Failed to find display IP for " + vmDef.name()); - return Optional.empty(); - } - var port = vmDef. fromVm("display", "spice", "port") - .map(Number::longValue); - if (port.isEmpty()) { - logger - .severe(() -> "No port defined for display of " + vmDef.name()); - return Optional.empty(); - } - StringBuffer data = new StringBuffer(100) - .append("[virt-viewer]\ntype=spice\nhost=") - .append(addr.get().getHostAddress()).append("\nport=") - .append(port.get().toString()) - .append('\n'); - if (password != null) { - data.append("password=").append(password).append('\n'); - } - vmDef. fromVm("display", "spice", "proxyUrl") - .ifPresent(u -> { - if (!Strings.isNullOrEmpty(u)) { - data.append("proxy=").append(u).append('\n'); - } - }); - if (deleteConnectionFile) { - data.append("delete-this-file=1\n"); - } - return Optional.of(data.toString()); - } - - private Optional displayIp(Class preferredIpVersion) { - Optional server = vmDef.fromVm("display", "spice", "server"); - if (server.isPresent()) { - var srv = server.get(); - try { - var addr = InetAddress.getByName(srv); - logger.fine(() -> "Using IP address from CRD for " - + vmDef.metadata().getName() + ": " + addr); - return Optional.of(addr); - } catch (UnknownHostException e) { - logger.log(Level.SEVERE, e, () -> "Invalid server address " - + srv + ": " + e.getMessage()); - return Optional.empty(); - } - } - var addrs = nodeAddresses.stream().map(a -> { - try { - return InetAddress.getByName(a); - } catch (UnknownHostException e) { - logger.warning(() -> "Invalid IP address: " + a); - return null; - } - }).filter(Objects::nonNull).toList(); - logger.fine( - () -> "Known IP addresses for " + vmDef.name() + ": " + addrs); - return addrs.stream() - .filter(a -> preferredIpVersion.isAssignableFrom(a.getClass())) - .findFirst().or(() -> addrs.stream().findFirst()); - } - -} diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmPool.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmPool.java deleted file mode 100644 index f7aaa67..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmPool.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.common; - -import java.time.Duration; -import java.time.Instant; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; -import org.jdrupes.vmoperator.common.VmDefinition.Assignment; -import org.jdrupes.vmoperator.common.VmDefinition.Grant; -import org.jdrupes.vmoperator.common.VmDefinition.Permission; -import org.jdrupes.vmoperator.util.DataPath; - -/** - * Represents a VM pool. - */ -public class VmPool { - - private final String name; - private String retention; - private boolean loginOnAssignment; - private boolean defined; - private List permissions = Collections.emptyList(); - private final Set vms - = Collections.synchronizedSet(new HashSet<>()); - - /** - * Instantiates a new vm pool. - * - * @param name the name - */ - public VmPool(String name) { - this.name = name; - } - - /** - * Fill the properties of a provisionally created pool from - * the definition. - * - * @param definition the definition - */ - public void defineFrom(VmPool definition) { - retention = definition.retention(); - permissions = definition.permissions(); - loginOnAssignment = definition.loginOnAssignment(); - defined = true; - } - - /** - * Returns the name. - * - * @return the name - */ - public String name() { - return name; - } - - /** - * Checks if is login on assignment. - * - * @return the loginOnAssignment - */ - public boolean loginOnAssignment() { - return loginOnAssignment; - } - - /** - * Checks if is defined. - * - * @return the result - */ - public boolean isDefined() { - return defined; - } - - /** - * Marks the pool as undefined. - */ - public void setUndefined() { - defined = false; - } - - /** - * Gets the retention. - * - * @return the retention - */ - public String retention() { - return retention; - } - - /** - * Permissions granted for a VM from the pool. - * - * @return the permissions - */ - public List permissions() { - return permissions; - } - - /** - * Returns the VM names. - * - * @return the vms - */ - public Set vms() { - return vms; - } - - /** - * Collect all permissions for the given user with the given roles. - * - * @param user the user - * @param roles the roles - * @return the sets the - */ - public Set permissionsFor(String user, - Collection roles) { - return permissions.stream() - .filter(g -> DataPath.get(g, "user").map(u -> u.equals(user)) - .orElse(false) - || DataPath.get(g, "role").map(roles::contains).orElse(false)) - .map(g -> DataPath.> get(g, "may") - .orElse(Collections.emptySet()).stream()) - .flatMap(Function.identity()).collect(Collectors.toSet()); - } - - /** - * Checks if the given VM belongs to the pool and is not in use. - * - * @param vmDef the vm def - * @return true, if is assignable - */ - @SuppressWarnings("PMD.SimplifyBooleanReturns") - public boolean isAssignable(VmDefinition vmDef) { - // Check if the VM is in the pool - if (!vmDef.pools().contains(name)) { - return false; - } - - // Check if the VM is not in use - if (vmDef.consoleConnected()) { - return false; - } - - // If not assigned, it's usable - if (vmDef.assignment().isEmpty()) { - return true; - } - - // Check if it is to be retained - if (vmDef.assignment().map(Assignment::lastUsed).map(this::retainUntil) - .map(ru -> Instant.now().isBefore(ru)).orElse(false)) { - return false; - } - - // Additional check in case lastUsed has not been updated - // by PoolMonitor#onVmResourceChanged() yet ("race condition") - if (vmDef.condition("ConsoleConnected") - .map(cc -> cc.getLastTransitionTime().toInstant()) - .map(this::retainUntil) - .map(ru -> Instant.now().isBefore(ru)).orElse(false)) { - return false; - } - return true; - } - - /** - * Return the instant until which an assignment should be retained. - * - * @param lastUsed the last used - * @return the instant - */ - public Instant retainUntil(Instant lastUsed) { - if (retention.startsWith("P")) { - return lastUsed.plus(Duration.parse(retention)); - } - return Instant.parse(retention); - } - - /** - * To string. - * - * @return the string - */ - @Override - @SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition", - "PMD.AvoidSynchronizedStatement" }) - public String toString() { - StringBuilder builder = new StringBuilder(50); - builder.append("VmPool [name=").append(name).append(", permissions=") - .append(permissions).append(", vms="); - if (vms.size() <= 3) { - builder.append(vms); - } else { - synchronized (vms) { - builder.append('[').append(vms.stream().limit(3) - .map(s -> s + ",").collect(Collectors.joining())) - .append("...]"); - } - } - builder.append(']'); - return builder.toString(); - } -} diff --git a/org.jdrupes.vmoperator.manager.events/.eclipse-pmd b/org.jdrupes.vmoperator.manager.events/.eclipse-pmd index 5d69caa..8b394f8 100644 --- a/org.jdrupes.vmoperator.manager.events/.eclipse-pmd +++ b/org.jdrupes.vmoperator.manager.events/.eclipse-pmd @@ -2,6 +2,6 @@ - + diff --git a/org.jdrupes.vmoperator.manager.events/build.gradle b/org.jdrupes.vmoperator.manager.events/build.gradle index bb4b8d8..56c364f 100644 --- a/org.jdrupes.vmoperator.manager.events/build.gradle +++ b/org.jdrupes.vmoperator.manager.events/build.gradle @@ -9,5 +9,6 @@ plugins { } dependencies { + api 'org.jgrapes:org.jgrapes.core:[1.19.0,2)' api project(':org.jdrupes.vmoperator.common') } diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/AssignVm.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/AssignVm.java deleted file mode 100644 index 7252c6a..0000000 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/AssignVm.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager.events; - -import org.jdrupes.vmoperator.manager.events.GetVms.VmData; -import org.jgrapes.core.Event; - -/** - * Assign a VM from a pool to a user. - */ -public class AssignVm extends Event { - - private final String fromPool; - private final String toUser; - - /** - * Instantiates a new event. - * - * @param fromPool the from pool - * @param toUser the to user - */ - public AssignVm(String fromPool, String toUser) { - this.fromPool = fromPool; - this.toUser = toUser; - } - - /** - * Gets the pool to assign from. - * - * @return the pool - */ - public String fromPool() { - return fromPool; - } - - /** - * Gets the user to assign to. - * - * @return the to user - */ - public String toUser() { - return toUser; - } -} diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ChannelDictionary.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ChannelDictionary.java deleted file mode 100644 index 2b23532..0000000 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ChannelDictionary.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager.events; - -import java.util.Collection; -import java.util.Optional; -import java.util.Set; -import org.jgrapes.core.Channel; - -/** - * Supports the lookup of a channel by a name (an id). As a convenience, - * it is possible to additionally associate arbitrary data with the entry - * (and thus with the channel). Note that this interface defines a - * read-only view of the dictionary. - * - * @param the key type - * @param the channel type - * @param the type of the associated data - */ -public interface ChannelDictionary { - - /** - * Combines the channel and the associated data. - * - * @param the channel type - * @param the type of the associated data - * @param channel the channel - * @param associated the associated - */ - public record Value(C channel, A associated) { - } - - /** - * Returns all known keys. - * - * @return the keys - */ - Set keys(); - - /** - * Return all known values. - * - * @return the collection - */ - Collection> values(); - - /** - * Returns the channel and associates data registered for the key - * or an empty optional if no entry exists. - * - * @param key the key - * @return the result - */ - Optional> value(K key); - - /** - * Return all known channels. - * - * @return the collection - */ - default Collection channels() { - return values().stream().map(v -> v.channel).toList(); - } - - /** - * Returns the channel registered for the key or an empty optional - * if no mapping exists. - * - * @param key the key - * @return the optional - */ - default Optional channel(K key) { - return value(key).map(b -> b.channel); - } - - /** - * Returns all known associated data. - * - * @return the collection - */ - default Collection associated() { - return values().stream() - .filter(v -> v.associated() != null) - .map(v -> v.associated).toList(); - } - - /** - * Return the data associated with the entry for the channel. - * - * @param key the key - * @return the data - */ - default Optional associated(K key) { - return value(key).map(b -> b.associated); - } -} diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ChannelManager.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ChannelManager.java deleted file mode 100644 index da36123..0000000 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ChannelManager.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager.events; - -import java.util.Collection; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Function; -import org.jgrapes.core.Channel; - -/** - * Provides an actively managed implementation of the {@link ChannelDictionary}. - * - * The {@link ChannelManager} can be used for housekeeping by any component - * that creates channels. It can be shared between this component and - * some other component, preferably passing it as {@link ChannelDictionary} - * (the read-only view) to the second component. Alternatively, the other - * component can use a {@link ChannelTracker} to track the mappings using - * events. - * - * @param the key type - * @param the channel type - * @param the type of the associated data - */ -public class ChannelManager - implements ChannelDictionary { - - private final Map> entries = new ConcurrentHashMap<>(); - private final Function supplier; - - /** - * Instantiates a new channel manager. - * - * @param supplier the supplier that creates new channels - */ - public ChannelManager(Function supplier) { - this.supplier = supplier; - } - - /** - * Instantiates a new channel manager without a default supplier. - */ - public ChannelManager() { - this(k -> null); - } - - /** - * Return all keys. - * - * @return the keys. - */ - @Override - public Set keys() { - return entries.keySet(); - } - - /** - * Return all known values. - * - * @return the collection - */ - @Override - public Collection> values() { - return entries.values(); - } - - /** - * Returns the channel and associates data registered for the key - * or an empty optional if no mapping exists. - * - * @param key the key - * @return the result - */ - public Optional> value(K key) { - return Optional.ofNullable(entries.get(key)); - } - - /** - * Store the given data. - * - * @param key the key - * @param channel the channel - * @param associated the associated - * @return the channel manager - */ - public ChannelManager put(K key, C channel, A associated) { - entries.put(key, new Value<>(channel, associated)); - return this; - } - - /** - * Store the given data. - * - * @param key the key - * @param channel the channel - * @return the channel manager - */ - public ChannelManager put(K key, C channel) { - put(key, channel, null); - return this; - } - - /** - * Creates a new channel without adding it to the channel manager. - * After fully initializing the channel, it should be added to the - * manager using {@link #put(K, C)}. - * - * @param key the key - * @return the c - */ - public C createChannel(K key) { - return supplier.apply(key); - } - - /** - * Returns the {@link Channel} for the given name, creating it using - * the supplier passed to the constructor if it doesn't exist yet. - * - * @param key the key - * @return the channel - */ - public C channelGet(K key) { - return computeIfAbsent(key, supplier); - } - - /** - * Returns the {@link Channel} for the given name, creating it using - * the given supplier if it doesn't exist yet. - * - * @param key the key - * @param supplier the supplier - * @return the channel - */ - public C computeIfAbsent(K key, Function supplier) { - return entries.computeIfAbsent(key, - k -> new Value<>(supplier.apply(k), null)).channel(); - } - - /** - * Associate the entry for the channel with the given data. The entry - * for the channel must already exist. - * - * @param key the key - * @param data the data - * @return the channel manager - */ - public ChannelManager associate(K key, A data) { - Optional.ofNullable(entries.computeIfPresent(key, - (k, existing) -> new Value<>(existing.channel(), data))); - return this; - } - - /** - * Removes the channel with the given name. - * - * @param name the name - */ - public void remove(String name) { - entries.remove(name); - } -} diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ChannelTracker.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ChannelTracker.java deleted file mode 100644 index 8a41908..0000000 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ChannelTracker.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager.events; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import org.jgrapes.core.Channel; - -/** - * Used to track mapping from a key to a channel. Entries must - * be maintained by handlers for "add/remove" (or "open/close") - * events delivered on the channels that are to be - * made available by the tracker. - * - * The channels are stored in the dictionary using {@link WeakReference}s. - * Removing entries is therefore best practice but not an absolute necessity - * as entries for cleared references are removed when one of the methods - * {@link #values()}, {@link #channels()} or {@link #associated()} is called. - * - * @param the key type - * @param the channel type - * @param the type of the associated data - */ -public class ChannelTracker - implements ChannelDictionary { - - private final Map> entries = new ConcurrentHashMap<>(); - - /** - * Combines the channel and associated data. - * - * @param the generic type - * @param the generic type - */ - @SuppressWarnings("PMD.ShortClassName") - private static class Data { - public final WeakReference channel; - public A associated; - - /** - * Instantiates a new value. - * - * @param channel the channel - */ - public Data(C channel) { - this.channel = new WeakReference<>(channel); - } - } - - @Override - public Set keys() { - return entries.keySet(); - } - - @Override - public Collection> values() { - var result = new ArrayList>(); - for (var itr = entries.entrySet().iterator(); itr.hasNext();) { - var value = itr.next().getValue(); - var channel = value.channel.get(); - if (channel == null) { - itr.remove(); - continue; - } - result.add(new Value<>(channel, value.associated)); - } - return result; - } - - /** - * Returns the channel and associates data registered for the key - * or an empty optional if no mapping exists. - * - * @param key the key - * @return the result - */ - public Optional> value(K key) { - var value = entries.get(key); - if (value == null) { - return Optional.empty(); - } - var channel = value.channel.get(); - if (channel == null) { - // Cleanup old reference - entries.remove(key); - return Optional.empty(); - } - return Optional.of(new Value<>(channel, value.associated)); - } - - /** - * Store the given data. - * - * @param key the key - * @param channel the channel - * @param associated the associated - * @return the channel manager - */ - public ChannelTracker put(K key, C channel, A associated) { - Data data = new Data<>(channel); - data.associated = associated; - entries.put(key, data); - return this; - } - - /** - * Store the given data. - * - * @param key the key - * @param channel the channel - * @return the channel manager - */ - public ChannelTracker put(K key, C channel) { - put(key, channel, null); - return this; - } - - /** - * Associate the entry for the channel with the given data. The entry - * for the channel must already exist. - * - * @param key the key - * @param data the data - * @return the channel manager - */ - public ChannelTracker associate(K key, A data) { - Optional.ofNullable(entries.get(key)) - .ifPresent(v -> v.associated = data); - return this; - } - - /** - * Removes the channel with the given name. - * - * @param name the name - */ - public void remove(String name) { - entries.remove(name); - } -} diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/Exit.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/Exit.java deleted file mode 100644 index 1c11a4e..0000000 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/Exit.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager.events; - -import org.jgrapes.core.events.Stop; - -/** - * Like {@link Stop}, but sets an exit status. - */ -@SuppressWarnings("PMD.ShortClassName") -public class Exit extends Stop { - - private final int exitStatus; - - /** - * Instantiates a new exit. - * - * @param exitStatus the exit status - */ - public Exit(int exitStatus) { - this.exitStatus = exitStatus; - } - - public int exitStatus() { - return exitStatus; - } -} diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetDisplaySecret.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetDisplaySecret.java deleted file mode 100644 index dc47b4a..0000000 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetDisplaySecret.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager.events; - -import org.jdrupes.vmoperator.common.VmDefinition; -import org.jgrapes.core.Event; - -/** - * Gets the current display secret and optionally updates it. - */ -public class GetDisplaySecret extends Event { - - private final VmDefinition vmDef; - private final String user; - - /** - * Instantiates a new request for the display secret. - * After handling the event, a result of `null` means that - * no secret is needed. No result means that the console - * is not accessible. - * - * @param vmDef the vm name - * @param user the requesting user - */ - public GetDisplaySecret(VmDefinition vmDef, String user) { - this.vmDef = vmDef; - this.user = user; - } - - /** - * Gets the VM definition. - * - * @return the VM definition - */ - public VmDefinition vmDefinition() { - return vmDef; - } - - /** - * Return the id of the user who has requested the password. - * - * @return the string - */ - public String user() { - return user; - } - - /** - * Returns `true` if a password is available. May only be called - * when the event is completed. Note that the password returned - * by {@link #secret()} may be `null`, indicating that no password - * is needed. - * - * @return true, if successful - */ - public boolean secretAvailable() { - if (!isDone()) { - throw new IllegalStateException("Event is not done."); - } - return !currentResults().isEmpty(); - } - - /** - * Return the secret. May only be called when the event has been - * completed with a valid result (see {@link #secretAvailable()}). - * - * @return the password. A value of `null` means that no password - * is required. - */ - public String secret() { - if (!isDone() || currentResults().isEmpty()) { - throw new IllegalStateException("Event is not done."); - } - return currentResults().get(0); - } -} diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetPools.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetPools.java deleted file mode 100644 index b563c9c..0000000 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetPools.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager.events; - -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import org.jdrupes.vmoperator.common.VmPool; -import org.jgrapes.core.Event; - -/** - * Gets the known pools' definitions. - */ -public class GetPools extends Event> { - - private String name; - private String user; - private List roles = Collections.emptyList(); - - /** - * Return only the pool with the given name. - * - * @param name the name - * @return the returns the vms - */ - public GetPools withName(String name) { - this.name = name; - return this; - } - - /** - * Return only {@link VmPool}s that are accessible by - * the given user or roles. - * - * @param user the user - * @param roles the roles - * @return the event - */ - public GetPools accessibleFor(String user, List roles) { - this.user = user; - this.roles = roles; - return this; - } - - /** - * Returns the name filter criterion, if set. - * - * @return the optional - */ - public Optional name() { - return Optional.ofNullable(name); - } - - /** - * Returns the user filter criterion, if set. - * - * @return the optional - */ - public Optional forUser() { - return Optional.ofNullable(user); - } - - /** - * Returns the roles criterion. - * - * @return the list - */ - public List forRoles() { - return roles; - } -} diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetVms.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetVms.java deleted file mode 100644 index 0e24013..0000000 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetVms.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager.events; - -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import org.jdrupes.vmoperator.common.VmDefinition; -import org.jgrapes.core.Event; - -/** - * Gets the known VMs' definitions and channels. - */ -public class GetVms extends Event> { - - private String name; - private String user; - private List roles = Collections.emptyList(); - private String fromPool; - private String toUser; - - /** - * Return only the VMs with the given name. - * - * @param name the name - * @return the returns the vms - */ - public GetVms withName(String name) { - this.name = name; - return this; - } - - /** - * Return only {@link VmDefinition}s that are accessible by - * the given user or roles. - * - * @param user the user - * @param roles the roles - * @return the event - */ - public GetVms accessibleFor(String user, List roles) { - this.user = user; - this.roles = roles; - return this; - } - - /** - * Return only {@link VmDefinition}s that are assigned from the given pool. - * - * @param pool the pool - * @return the returns the vms - */ - public GetVms assignedFrom(String pool) { - this.fromPool = pool; - return this; - } - - /** - * Return only {@link VmDefinition}s that are assigned to the given user. - * - * @param user the user - * @return the returns the vms - */ - public GetVms assignedTo(String user) { - this.toUser = user; - return this; - } - - /** - * Returns the name filter criterion, if set. - * - * @return the optional - */ - public Optional name() { - return Optional.ofNullable(name); - } - - /** - * Returns the user filter criterion, if set. - * - * @return the optional - */ - public Optional user() { - return Optional.ofNullable(user); - } - - /** - * Returns the roles criterion. - * - * @return the list - */ - public List roles() { - return roles; - } - - /** - * Returns the pool filter criterion, if set. - * - * @return the optional - */ - public Optional fromPool() { - return Optional.ofNullable(fromPool); - } - - /** - * Returns the user filter criterion, if set. - * - * @return the optional - */ - public Optional toUser() { - return Optional.ofNullable(toUser); - } - - /** - * Return tuple. - * - * @param definition the definition - * @param channel the channel - */ - public record VmData(VmDefinition definition, VmChannel channel) { - } -} diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/PodChanged.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/PodChanged.java deleted file mode 100644 index 8bbcfe8..0000000 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/PodChanged.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager.events; - -import io.kubernetes.client.openapi.models.V1Pod; -import org.jdrupes.vmoperator.common.K8sObserver; -import org.jgrapes.core.Channel; -import org.jgrapes.core.Components; -import org.jgrapes.core.Event; - -/** - * Indicates a change in a pod that runs a VM. - */ -public class PodChanged extends Event { - - private final V1Pod pod; - private final K8sObserver.ResponseType type; - - /** - * Instantiates a new VM changed event. - * - * @param pod the pod - * @param type the type - */ - public PodChanged(V1Pod pod, K8sObserver.ResponseType type) { - this.pod = pod; - this.type = type; - } - - /** - * Gets the pod. - * - * @return the pod - */ - public V1Pod pod() { - return pod; - } - - /** - * Returns the type. - * - * @return the type - */ - public K8sObserver.ResponseType type() { - return type; - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append(Components.objectName(this)).append(" [") - .append(pod.getMetadata().getName()).append(' ').append(type); - if (channels() != null) { - builder.append(", channels=").append(Channel.toString(channels())); - } - builder.append(']'); - return builder.toString(); - } -} diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ModifyVm.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/StartVm.java similarity index 67% rename from org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ModifyVm.java rename to org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/StartVm.java index 9e19255..fb28f0a 100644 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ModifyVm.java +++ b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/StartVm.java @@ -22,26 +22,22 @@ import org.jgrapes.core.Channel; import org.jgrapes.core.Event; /** - * Modifies a VM. + * Starts a VM. */ -public class ModifyVm extends Event { +@SuppressWarnings("PMD.DataClass") +public class StartVm extends Event { private final String name; - private final String path; - private final Object value; /** - * Instantiates a new modify vm event. + * Instantiates a new start vm event. * * @param channels the channels * @param name the name */ - public ModifyVm(String name, String path, Object value, - Channel... channels) { + public StartVm(String name, Channel... channels) { super(channels); this.name = name; - this.path = path; - this.value = value; } /** @@ -53,22 +49,4 @@ public class ModifyVm extends Event { return name; } - /** - * Gets the path. - * - * @return the path - */ - public String path() { - return path; - } - - /** - * Gets the value. - * - * @return the value - */ - public Object value() { - return value; - } - } diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ResetVm.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/StopVm.java similarity index 64% rename from org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ResetVm.java rename to org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/StopVm.java index 778820e..a6d6281 100644 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ResetVm.java +++ b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/StopVm.java @@ -1,6 +1,6 @@ /* * VM-Operator - * Copyright (C) 2024 Michael N. Lipp + * 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 @@ -18,30 +18,35 @@ package org.jdrupes.vmoperator.manager.events; +import org.jgrapes.core.Channel; import org.jgrapes.core.Event; /** - * Triggers a reset of the VM. + * Stops a VM. */ -public class ResetVm extends Event { +@SuppressWarnings("PMD.DataClass") +public class StopVm extends Event { - private final String vmName; + private final String name; /** - * Instantiates a new event. + * Instantiates a new start vm event. * - * @param vmName the vm name + * @param channels the channels + * @param name the name */ - public ResetVm(String vmName) { - this.vmName = vmName; + public StopVm(String name, Channel... channels) { + super(channels); + this.name = name; } /** - * Gets the vm name. + * Gets the name. * - * @return the vm name + * @return the name */ - public String vmName() { - return vmName; + public String name() { + return name; } + } diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/UpdateAssignment.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/UpdateAssignment.java deleted file mode 100644 index b4fcf56..0000000 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/UpdateAssignment.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager.events; - -import org.jdrupes.vmoperator.common.VmPool; -import org.jgrapes.core.Event; - -/** - * Note the assignment to a user in the VM status. - */ -public class UpdateAssignment extends Event { - - private final VmPool fromPool; - private final String toUser; - - /** - * Instantiates a new event. - * - * @param fromPool the pool from which the VM was assigned - * @param toUser the to user - */ - public UpdateAssignment(VmPool fromPool, String toUser) { - this.fromPool = fromPool; - this.toUser = toUser; - } - - /** - * Gets the pool from which the VM was assigned. - * - * @return the pool - */ - public VmPool fromPool() { - return fromPool; - } - - /** - * Gets the user to whom the VM was assigned. - * - * @return the to user - */ - public String toUser() { - return toUser; - } -} diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmChannel.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmChannel.java index 73507ae..bc06e68 100644 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmChannel.java +++ b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmChannel.java @@ -18,21 +18,21 @@ package org.jdrupes.vmoperator.manager.events; -import org.jdrupes.vmoperator.common.K8sClient; -import org.jdrupes.vmoperator.common.VmDefinition; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; import org.jgrapes.core.Channel; -import org.jgrapes.core.Event; import org.jgrapes.core.EventPipeline; import org.jgrapes.core.Subchannel.DefaultSubchannel; /** * A subchannel used to send the events related to a specific VM. */ +@SuppressWarnings("PMD.DataClass") public class VmChannel extends DefaultSubchannel { private final EventPipeline pipeline; - private final K8sClient client; - private VmDefinition definition; + private final ApiClient client; + private DynamicKubernetesObject vmDefinition; private long generation = -1; /** @@ -43,7 +43,7 @@ public class VmChannel extends DefaultSubchannel { * @param client the client */ public VmChannel(Channel mainChannel, EventPipeline pipeline, - K8sClient client) { + ApiClient client) { super(mainChannel); this.pipeline = pipeline; this.client = client; @@ -55,18 +55,19 @@ public class VmChannel extends DefaultSubchannel { * @param definition the definition * @return the watch channel */ - public VmChannel setVmDefinition(VmDefinition definition) { - this.definition = definition; + @SuppressWarnings("PMD.LinguisticNaming") + public VmChannel setVmDefinition(DynamicKubernetesObject definition) { + this.vmDefinition = definition; return this; } /** * Returns the last known definition of the VM. * - * @return the defintion + * @return the json object */ - public VmDefinition vmDefinition() { - return definition; + public DynamicKubernetesObject vmDefinition() { + return vmDefinition; } /** @@ -85,6 +86,7 @@ public class VmChannel extends DefaultSubchannel { * @param generation the generation to set * @return true if value has changed */ + @SuppressWarnings("PMD.LinguisticNaming") public boolean setGeneration(long generation) { if (this.generation == generation) { return false; @@ -102,25 +104,12 @@ public class VmChannel extends DefaultSubchannel { return pipeline; } - /** - * Fire the given event on this channel, using the associated - * {@link #pipeline()}. - * - * @param the generic type - * @param event the event - * @return the t - */ - public > T fire(T event) { - pipeline.fire(event, this); - return event; - } - /** * Returns the API client. * * @return the API client */ - public K8sClient client() { + public ApiClient client() { return client; } } diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmResourceChanged.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmDefChanged.java similarity index 56% rename from org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmResourceChanged.java rename to org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmDefChanged.java index eac30fb..fd5d43c 100644 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmResourceChanged.java +++ b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmDefChanged.java @@ -18,42 +18,48 @@ package org.jdrupes.vmoperator.manager.events; -import org.jdrupes.vmoperator.common.K8sObserver; -import org.jdrupes.vmoperator.common.VmDefinition; +import io.kubernetes.client.openapi.models.V1APIResource; +import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; import org.jgrapes.core.Channel; import org.jgrapes.core.Components; import org.jgrapes.core.Event; /** - * Indicates a change in a VM "resource". Note that the resource - * combines the VM CR's metadata (mostly immutable), the VM CR's - * "spec" part, the VM CR's "status" subresource and state information - * from the pod. Consumers that are only interested in "spec" changes - * should check {@link #specChanged()} before processing the event any - * further. + * Indicates a change in a VM definition. Note that the definition + * consists of the metadata (mostly immutable), the "spec" and the + * "status" parts. Consumers that are only interested in "spec" + * changes should check {@link #specChanged()} before processing + * the event any further. */ @SuppressWarnings("PMD.DataClass") -public class VmResourceChanged extends Event { +public class VmDefChanged extends Event { - private final K8sObserver.ResponseType type; - private final VmDefinition vmDefinition; + /** + * The type of change. + */ + public enum Type { + ADDED, MODIFIED, DELETED + } + + private final Type type; private final boolean specChanged; - private final boolean podChanged; + private final V1APIResource crd; + private final DynamicKubernetesObject vmDef; /** * Instantiates a new VM changed event. * * @param type the type - * @param vmDefinition the VM definition * @param specChanged the spec part changed + * @param crd the crd + * @param vmDefinition the VM definition */ - public VmResourceChanged(K8sObserver.ResponseType type, - VmDefinition vmDefinition, boolean specChanged, - boolean podChanged) { + public VmDefChanged(Type type, boolean specChanged, V1APIResource crd, + DynamicKubernetesObject vmDefinition) { this.type = type; - this.vmDefinition = vmDefinition; this.specChanged = specChanged; - this.podChanged = podChanged; + this.crd = crd; + this.vmDef = vmDefinition; } /** @@ -61,19 +67,10 @@ public class VmResourceChanged extends Event { * * @return the type */ - public K8sObserver.ResponseType type() { + public Type type() { return type; } - /** - * Return the VM definition. - * - * @return the VM definition - */ - public VmDefinition vmDefinition() { - return vmDefinition; - } - /** * Indicates if the "spec" part changed. */ @@ -82,19 +79,31 @@ public class VmResourceChanged extends Event { } /** - * Indicates if the pod status changed. + * Returns the Crd. + * + * @return the v 1 API resource */ - public boolean podChanged() { - return podChanged; + public V1APIResource crd() { + return crd; + } + + /** + * Returns the object. + * + * @return the object. + */ + public DynamicKubernetesObject vmDefinition() { + return vmDef; } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append(Components.objectName(this)).append(" [") - .append(vmDefinition.name()).append(' ').append(type); + .append(vmDef.getMetadata().getName()).append(' ').append(type); if (channels() != null) { - builder.append(", channels=").append(Channel.toString(channels())); + builder.append(", channels="); + builder.append(Channel.toString(channels())); } builder.append(']'); return builder.toString(); diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmPoolChanged.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmPoolChanged.java deleted file mode 100644 index 0c3f3a1..0000000 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmPoolChanged.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager.events; - -import org.jdrupes.vmoperator.common.VmPool; -import org.jgrapes.core.Channel; -import org.jgrapes.core.Components; -import org.jgrapes.core.Event; - -/** - * Indicates a change in a pool configuration. - */ -public class VmPoolChanged extends Event { - - private final VmPool vmPool; - private final boolean deleted; - - /** - * Instantiates a new VM changed event. - * - * @param pool the pool - * @param deleted true, if the pool was deleted - */ - public VmPoolChanged(VmPool pool, boolean deleted) { - vmPool = pool; - this.deleted = deleted; - } - - /** - * Instantiates a new VM changed event for an existing pool. - * - * @param pool the pool - */ - public VmPoolChanged(VmPool pool) { - this(pool, false); - } - - /** - * Returns the VM pool. - * - * @return the vm pool - */ - public VmPool vmPool() { - return vmPool; - } - - /** - * Pool has been deleted. - * - * @return true, if successful - */ - public boolean deleted() { - return deleted; - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(30); - builder.append(Components.objectName(this)) - .append(" ["); - if (deleted) { - builder.append("Deleted: "); - } - builder.append(vmPool); - if (channels() != null) { - builder.append(", channels=").append(Channel.toString(channels())); - } - builder.append(']'); - return builder.toString(); - } -} diff --git a/org.jdrupes.vmoperator.manager/.eclipse-pmd b/org.jdrupes.vmoperator.manager/.eclipse-pmd index 5d69caa..8b394f8 100644 --- a/org.jdrupes.vmoperator.manager/.eclipse-pmd +++ b/org.jdrupes.vmoperator.manager/.eclipse-pmd @@ -2,6 +2,6 @@ - + diff --git a/org.jdrupes.vmoperator.manager/.gitignore b/org.jdrupes.vmoperator.manager/.gitignore deleted file mode 100644 index 50a6b62..0000000 --- a/org.jdrupes.vmoperator.manager/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/logging.properties diff --git a/org.jdrupes.vmoperator.manager/.settings/net.sf.jautodoc.prefs b/org.jdrupes.vmoperator.manager/.settings/net.sf.jautodoc.prefs index 03e8200..6f3b6d4 100644 --- a/org.jdrupes.vmoperator.manager/.settings/net.sf.jautodoc.prefs +++ b/org.jdrupes.vmoperator.manager/.settings/net.sf.jautodoc.prefs @@ -1,6 +1,6 @@ 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 */ +header_text=/*\n * VM-Operator\n * Copyright (C) 2023 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 replacements=\n\n\nReturns the\nSets the\nAdds the\nEdits the\nRemoves the\nInits the\nParses the\nCreates the\nBuilds the\nChecks if is\nPrints the\nChecks for\n\n\n visibility_package=false diff --git a/org.jdrupes.vmoperator.manager/build.gradle b/org.jdrupes.vmoperator.manager/build.gradle index 4ce4ed0..c112737 100644 --- a/org.jdrupes.vmoperator.manager/build.gradle +++ b/org.jdrupes.vmoperator.manager/build.gradle @@ -13,100 +13,101 @@ dependencies { implementation 'commons-cli:commons-cli:1.5.0' - implementation 'org.jgrapes:org.jgrapes.util:[1.38.1,2)' - implementation 'org.jgrapes:org.jgrapes.io:[2.12.1,3)' - implementation 'org.jgrapes:org.jgrapes.http:[3.5.0,4)' + implementation 'org.jgrapes:org.jgrapes.core:[1.19.0,2)' + implementation 'org.jgrapes:org.jgrapes.io:[2.7.0,3)' + implementation 'org.jgrapes:org.jgrapes.http:[3.1.0,4)' + implementation 'org.jgrapes:org.jgrapes.util:[1.31.0,2)' - implementation 'org.jgrapes:org.jgrapes.webconsole.base:[2.3.0,3)' - implementation 'org.jgrapes:org.jgrapes.webconsole.vuejs:[1.8.0,2)' - implementation 'org.jgrapes:org.jgrapes.webconsole.rbac:[1.4.0,2)' - implementation 'org.jgrapes:org.jgrapes.webconlet.oidclogin:[1.7.0,2)' - implementation 'org.jgrapes:org.jgrapes.webconlet.markdowndisplay:[1.2.0,2)' + implementation 'org.jgrapes:org.jgrapes.webconsole.base:[1.2.0,2)' + implementation 'org.jgrapes:org.jgrapes.webconsole.vuejs:[1.3.0,2)' + implementation 'org.jgrapes:org.jgrapes.webconsole.rbac:[1.0.0,2)' + implementation 'org.jgrapes:org.jgrapes.webconlet.locallogin:[1.0.0,2)' - runtimeOnly 'org.jgrapes:org.jgrapes.webconlet.sysinfo:[1.4.0,2)' - runtimeOnly 'org.jgrapes:org.jgrapes.webconlet.logviewer:[0.2.0,2)' + runtimeOnly 'org.jgrapes:org.jgrapes.webconlet.locallogin:[0.1.0,2)' + runtimeOnly 'org.jgrapes:org.jgrapes.webconlet.sysinfo:[1.2.0,2)' + runtimeOnly 'org.jgrapes:org.jgrapes.webconlet.logviewer:[0.1.0-SNAPSHOT,2)' runtimeOnly 'com.electronwill.night-config:yaml:[3.6.7,3.7)' runtimeOnly 'org.eclipse.angus:angus-activation:[1.0.0,2.0.0)' runtimeOnly 'org.slf4j:slf4j-jdk14:[2.0.7,3)' runtimeOnly 'org.apache.logging.log4j:log4j-to-jul:2.20.0' - runtimeOnly project(':org.jdrupes.vmoperator.vmmgmt') - runtimeOnly project(':org.jdrupes.vmoperator.vmaccess') + runtimeOnly project(':org.jdrupes.vmoperator.vmconlet') + + testImplementation 'io.fabric8:kubernetes-client:[6.8.1,6.9)' } application { applicationName = 'vm-manager' - applicationDefaultJvmArgs = ['-Xmx128m', '-XX:+UseParallelGC', + applicationDefaultJvmArgs = ['-Xmx50m', '-XX:+UseParallelGC', '-Djava.util.logging.manager=org.jdrupes.vmoperator.util.LongLoggingManager' ] // Define the main class for the application. mainClass = 'org.jdrupes.vmoperator.manager.Manager' } -project.ext.gitBranch = grgit.branch.current.name.replace('/', '-') -def registry = "${project.rootProject.properties['docker.registry']}" -def rootVersion = rootProject.version - task buildImage(type: Exec) { dependsOn installDist inputs.files 'src/org/jdrupes/vmoperator/manager/Containerfile' - commandLine 'podman', 'build', '--pull', - '-t', "${project.name}:${project.gitBranch}",\ + commandLine 'podman', 'build', '-t', "${project.name}:${project.version}",\ '-f', 'src/org/jdrupes/vmoperator/manager/Containerfile', '.' } +task tagLatestImage(type: Exec) { + dependsOn buildImage + + enabled = !project.version.contains("SNAPSHOT") + && !project.version.contains("alpha") \ + && !project.version.contains("beta") \ + + commandLine 'podman', 'tag', "${project.name}:${project.version}",\ + "${project.name}:latest" +} + +task buildLatestImage { + dependsOn buildImage + dependsOn tagLatestImage +} + task pushImage(type: Exec) { dependsOn buildImage - // Don't push without testing first - dependsOn test commandLine 'podman', 'push', '--tls-verify=false', \ - "${project.name}:${project.gitBranch}", \ - "${registry}/${project.name}:${project.gitBranch}" + "localhost/${project.name}:${project.version}", \ + "${project.rootProject.properties['docker.registry']}" \ + + "/${project.name}:${project.version}" } -task tagWithVersion(type: Exec) { - dependsOn pushImage +task pushLatestImage(type: Exec) { + dependsOn buildLatestImage - enabled = !rootVersion.contains("SNAPSHOT") - - commandLine 'podman', 'push', \ - "${project.name}:${project.gitBranch}",\ - "${registry}/${project.name}:${project.version}" -} + enabled = !project.version.contains("SNAPSHOT") + && !project.version.contains("alpha") \ + && !project.version.contains("beta") \ -task tagAsLatest(type: Exec) { - dependsOn tagWithVersion - - enabled = !rootVersion.contains("SNAPSHOT") - && !rootVersion.contains("alpha") \ - && !rootVersion.contains("beta") \ - || project.rootProject.properties['docker.testRegistry'] \ - && project.rootProject.properties['docker.registry'] \ - == project.rootProject.properties['docker.testRegistry'] - - commandLine 'podman', 'push', \ - "${project.name}:${project.gitBranch}",\ - "${registry}/${project.name}:latest" -} - -task publishImage { - dependsOn pushImage - dependsOn tagWithVersion - dependsOn tagAsLatest + commandLine 'podman', 'push', '--tls-verify=false', \ + "localhost/${project.name}:${project.version}", \ + "${project.rootProject.properties['docker.registry']}" \ + + "/${project.name}:latest" } task pushForTest(type: Exec) { dependsOn buildImage commandLine 'podman', 'push', '--tls-verify=false', \ - "${project.name}:${project.gitBranch}", \ - "${project.rootProject.properties['docker.testRegistry']}" \ + "localhost/${project.name}:${project.version}", \ + "${project.rootProject.properties['docker.registry']}" \ + "/${project.name}:test" } +task pushImages { + // Don't push without testing first + dependsOn test + dependsOn pushImage + dependsOn pushLatestImage +} + test { enabled = project.hasProperty("k8s.testCluster") diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/logging.properties b/org.jdrupes.vmoperator.manager/logging.properties similarity index 87% rename from org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/logging.properties rename to org.jdrupes.vmoperator.manager/logging.properties index 2a16af6..9e6d0f5 100644 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/logging.properties +++ b/org.jdrupes.vmoperator.manager/logging.properties @@ -1,6 +1,6 @@ # # VM-Operator -# Copyright (C) 2025 Michael N. Lipp +# 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 General Public License as published by @@ -19,7 +19,10 @@ handlers=java.util.logging.ConsoleHandler, \ org.jgrapes.webconlet.logviewer.LogViewerHandler -org.jdrupes.vmoperator.level=FINE +org.jgrapes.level=FINE +org.jgrapes.core.handlerTracking.level=FINER + +org.jdrupes.vmoperator.manager.level=FINE java.util.logging.ConsoleHandler.level=ALL java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/ManagerIntro-Preview.md b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/ManagerIntro-Preview.md deleted file mode 100644 index b6b9efa..0000000 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/ManagerIntro-Preview.md +++ /dev/null @@ -1,5 +0,0 @@ -You can use the "puzzle piece" icon on the top right corner of the -page to add display widgets (conlets) to the overview tab. - -Use the "full screen" icon on the top right corner of any -conlet (if available) to get a detailed view. diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/ManagerIntro-Preview_de.md b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/ManagerIntro-Preview_de.md deleted file mode 100644 index bec5f3e..0000000 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/ManagerIntro-Preview_de.md +++ /dev/null @@ -1,6 +0,0 @@ -Verwenden Sie das "Puzzle"-Icon auf der rechten oberen Ecke -der Seite, um Anzeige-Widgets (Conlets) hinzuzufügen. - -Wenn sich in der rechten oberen Ecke eines Conlets ein Vollbild-Icon -befindet, können Sie es verwenden, um eine Detailansicht in einem neuen -Register anzufordern. diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/console-brand.ftl.html b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/console-brand.ftl.html index 9c9de88..a81ee0a 100644 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/console-brand.ftl.html +++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/console-brand.ftl.html @@ -1,5 +1,2 @@ ${_("consoleTitle")}  - (<#if clusterName()??>${clusterName() + "/"}${ namespace() }) \ No newline at end of file + src="${renderSupport.consoleResource('VM-Operator.svg')}">${_("consoleTitle")} \ No newline at end of file diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/console-footer.ftl.html b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/console-footer.ftl.html deleted file mode 100644 index 72596d5..0000000 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/console-footer.ftl.html +++ /dev/null @@ -1,3 +0,0 @@ -
-Copyright © Michael N. Lipp 2023, 2025 -
diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/l10n.properties b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/l10n.properties index 6bcc3a2..ec22a06 100644 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/l10n.properties +++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/l10n.properties @@ -17,4 +17,3 @@ # consoleTitle = VM-Operator -introTitle = Usage \ No newline at end of file diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/l10n_de.properties b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/l10n_de.properties deleted file mode 100644 index dcbba93..0000000 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/l10n_de.properties +++ /dev/null @@ -1,19 +0,0 @@ -# -# VM-Operator -# Copyright (C) 2024 Michael N. Lipp -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY -# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License -# for more details. -# -# You should have received a copy of the GNU Affero General Public License along -# with this program; if not, see . -# - -introTitle = Benutzung diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml index 0200021..d49e705 100644 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml +++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml @@ -1,138 +1,114 @@ apiVersion: v1 kind: ConfigMap metadata: - namespace: ${ cr.namespace() } - name: ${ cr.name() } + namespace: ${ cr.metadata.namespace.asString } + name: ${ cr.metadata.name.asString } labels: app.kubernetes.io/name: ${ constants.APP_NAME } - app.kubernetes.io/instance: ${ cr.name() } + app.kubernetes.io/instance: ${ cr.metadata.name.asString } app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME } annotations: vmoperator.jdrupes.org/version: ${ managerVersion } ownerReferences: - - apiVersion: ${ cr.apiVersion() } - kind: ${ constants.Crd.KIND_VM } - name: ${ cr.name() } - uid: ${ cr.metadata().getUid() } + - apiVersion: ${ cr.apiVersion.asString } + kind: ${ constants.VM_OP_KIND_VM } + name: ${ cr.metadata.name.asString } + uid: ${ cr.metadata.uid.asString } controller: false - + data: config.yaml: | "/Runner": # The directory used to store data files. Defaults to (depending on # values available): - # * $XDG_DATA_HOME/vmrunner/${ cr.name() } - # * $HOME/.local/share/vmrunner/${ cr.name() } - # * ./${ cr.name() } + # * $XDG_DATA_HOME/vmrunner/${ cr.metadata.name.asString } + # * $HOME/.local/share/vmrunner/${ cr.metadata.name.asString } + # * ./${ cr.metadata.name.asString } dataDir: /var/local/vm-data # The directory used to store runtime files. Defaults to (depending on # values available): - # * $XDG_RUNTIME_DIR/vmrunner/${ cr.name() } - # * /tmp/$USER/vmrunner/${ cr.name() } - # * /tmp/vmrunner/${ cr.name() } - # runtimeDir: "$XDG_RUNTIME_DIR/vmrunner/${ cr.name() }" + # * $XDG_RUNTIME_DIR/vmrunner/${ cr.metadata.name.asString } + # * /tmp/$USER/vmrunner/${ cr.metadata.name.asString } + # * /tmp/vmrunner/${ cr.metadata.name.asString } + # runtimeDir: "$XDG_RUNTIME_DIR/vmrunner/${ cr.metadata.name.asString }" - <#assign spec = cr.spec() /> # The template to use. Resolved relative to /usr/share/vmrunner/templates. # template: "Standard-VM-latest.ftl.yaml" - <#if spec.runnerTemplate?? && spec.runnerTemplate.source?? > - template: ${ spec.runnerTemplate.source } + <#if cr.spec.runnerTemplate?? && cr.spec.runnerTemplate.source?? > + template: ${ cr.spec.runnerTemplate.source.asString } # The template is copied to the data diretory when the VM starts for # the first time. Subsequent starts use the copy unless this option is set. - <#if spec.runnerTemplate?? && spec.runnerTemplate.update?? > - updateTemplate: ${ spec.runnerTemplate.update?c } + <#if cr.spec.runnerTemplate?? && cr.spec.runnerTemplate.update?? > + updateTemplate: ${ cr.spec.runnerTemplate.update.asBoolean?c } - - # Whether a shutdown initiated by the guest stops the pod deployment - guestShutdownStops: ${ (spec.guestShutdownStops!false)?c } - - # When incremented, the VM is reset. The value has no default value, - # i.e. if you start the VM without a value for this property, and - # decide to trigger a reset later, you have to first set the value - # and then inrement it. - resetCounter: ${ cr.extra().resetCount()?c } - - # Forward the cloud-init data if provided - <#if spec.cloudInit??> - cloudInit: - metaData: ${ toJson(adjustCloudInitMeta(spec.cloudInit.metaData!{}, cr.metadata())) } - <#if spec.cloudInit.userData??> - userData: ${ toJson(spec.cloudInit.userData) } - <#else> - userData: {} - - <#if spec.cloudInit.networkConfig??> - networkConfig: ${ toJson(spec.cloudInit.networkConfig) } - - - + # Define the VM (required) vm: # The VM's name (required) - name: ${ cr.name() } + name: ${ cr.metadata.name.asString } # The machine's uuid. If none is specified, a uuid is generated # and stored in the data directory. If the uuid is important # (e.g. because licenses depend on it) it is recommaned to specify # it here explicitly or to carefully backup the data directory. # uuid: "generated uuid" - <#if spec.vm.machineUuid??> - uuid: "${ spec.vm.machineUuid }" + <#if cr.spec.vm.machineUuid??> + uuid: "${ cr.spec.vm.machineUuid.asString }" # Whether to provide a software TPM (defaults to false) # useTpm: false - useTpm: ${ spec.vm.useTpm?c } + useTpm: ${ cr.spec.vm.useTpm.asBoolean?c } # How to boot (see https://github.com/mnlipp/VM-Operator/blob/main/org.jdrupes.vmoperator.runner.qemu/resources/org/jdrupes/vmoperator/runner/qemu/defaults.yaml): # * bios # * uefi[-4m] # * secure[-4m] - firmware: ${ spec.vm.firmware } + firmware: ${ cr.spec.vm.firmware.asString } # Whether to show a boot menu. # bootMenu: false - bootMenu: ${ spec.vm.bootMenu?c } + bootMenu: ${ cr.spec.vm.bootMenu.asBoolean?c } # When terminating, a graceful powerdown is attempted. If it # doesn't succeed within the given timeout (seconds) SIGTERM # is sent to Qemu. # powerdownTimeout: 900 - powerdownTimeout: ${ spec.vm.powerdownTimeout?c } + powerdownTimeout: ${ cr.spec.vm.powerdownTimeout.asLong?c } # CPU settings - cpuModel: ${ spec.vm.cpuModel } + cpuModel: ${ cr.spec.vm.cpuModel.asString } # Setting maximumCpus to 1 omits the "-smp" options. The defaults (0) # cause the corresponding property to be omitted from the "-smp" option. # If currentCpus is greater than maximumCpus, the latter is adjusted. - <#if spec.vm.maximumCpus?? > - maximumCpus: ${ parseQuantity(spec.vm.maximumCpus)?c } + <#if cr.spec.vm.maximumCpus?? > + maximumCpus: ${ parseQuantity(cr.spec.vm.maximumCpus.asString)?c } - <#if spec.vm.cpuTopology?? > - sockets: ${ spec.vm.cpuTopology.sockets?c } - diesPerSocket: ${ spec.vm.cpuTopology.diesPerSocket?c } - coresPerDie: ${ spec.vm.cpuTopology.coresPerDie?c } - threadsPerCore: ${ spec.vm.cpuTopology.threadsPerCore?c } + <#if cr.spec.vm.cpuTopology?? > + sockets: ${ cr.spec.vm.cpuTopology.sockets.asInt?c } + diesPerSocket: ${ cr.spec.vm.cpuTopology.diesPerSocket.asInt?c } + coresPerDie: ${ cr.spec.vm.cpuTopology.coresPerDie.asInt?c } + threadsPerCore: ${ cr.spec.vm.cpuTopology.threadsPerCore.asInt?c } - <#if spec.vm.currentCpus?? > - currentCpus: ${ parseQuantity(spec.vm.currentCpus)?c } + <#if cr.spec.vm.currentCpus?? > + currentCpus: ${ parseQuantity(cr.spec.vm.currentCpus.asString)?c } # RAM settings # Maximum defaults to 1G - maximumRam: "${ formatMemory(parseQuantity(spec.vm.maximumRam)) }" - <#if spec.vm.currentRam?? > - currentRam: "${ formatMemory(parseQuantity(spec.vm.currentRam)) }" + maximumRam: "${ formatMemory(parseQuantity(cr.spec.vm.maximumRam.asString)) }" + <#if cr.spec.vm.currentRam?? > + currentRam: "${ formatMemory(parseQuantity(cr.spec.vm.currentRam.asString)) }" # RTC settings. # rtcBase: utc # rtcClock: rt - rtcBase: ${ spec.vm.rtcBase } - rtcClock: ${ spec.vm.rtcClock } + rtcBase: ${ cr.spec.vm.rtcBase.asString } + rtcClock: ${ cr.spec.vm.rtcClock.asString } # Network settings # Supported types are "tap" and "user" (for debugging). Type "user" @@ -144,19 +120,19 @@ data: # mac: (undefined) network: <#assign nwCounter = 0/> - <#list spec.vm.networks as itf> + <#list cr.spec.vm.networks.asList() as itf> <#if itf.tap??> - type: tap - device: ${ itf.tap.device } - bridge: ${ itf.tap.bridge } + device: ${ itf.tap.device.asString } + bridge: ${ itf.tap.bridge.asString } <#if itf.tap.mac??> - mac: "${ itf.tap.mac }" + mac: "${ itf.tap.mac.asString }" <#elseif itf.user??> - type: user - device: ${ itf.user.device } + device: ${ itf.user.device.asString } <#if itf.user.net??> - net: "${ itf.user.net }" + net: "${ itf.user.net.asString }" <#assign nwCounter += 1/> @@ -172,11 +148,11 @@ data: # file: (undefined) drives: <#assign drvCounter = 0/> - <#list spec.vm.disks as disk> + <#list cr.spec.vm.disks.asList() as disk> <#if disk.volumeClaimTemplate?? && disk.volumeClaimTemplate.metadata?? && disk.volumeClaimTemplate.metadata.name??> - <#assign diskName = disk.volumeClaimTemplate.metadata.name + "-disk"> + <#assign diskName = disk.volumeClaimTemplate.metadata.name.asString + "-disk"> <#else> <#assign diskName = "disk-" + drvCounter> @@ -184,36 +160,30 @@ data: - type: raw resource: /dev/${ diskName } <#if disk.bootindex??> - bootindex: ${ disk.bootindex?c } + bootindex: ${ disk.bootindex.asInt?c } <#assign drvCounter = drvCounter + 1/> <#if disk.cdrom??> - type: ide-cd - file: "${ imageLocation(disk.cdrom.image) }" + file: "${ disk.cdrom.image.asString }" <#if disk.bootindex??> - bootindex: ${ disk.bootindex?c } + bootindex: ${ disk.bootindex.asInt?c } display: - <#if spec.vm.display.outputs?? > - outputs: ${ spec.vm.display.outputs?c } - - <#if loginRequestedFor?? > - loggedInUser: "${ loginRequestedFor }" - - <#if spec.vm.display.spice??> + <#if cr.spec.vm.display.spice??> spice: - port: ${ spec.vm.display.spice.port?c } - <#if spec.vm.display.spice.ticket??> - ticket: "${ spec.vm.display.spice.ticket }" + port: ${ cr.spec.vm.display.spice.port.asInt?c } + <#if cr.spec.vm.display.spice.ticket??> + ticket: "${ cr.spec.vm.display.spice.ticket.asString }" - <#if spec.vm.display.spice.streamingVideo??> - streaming-video: "${ spec.vm.display.spice.streamingVideo }" + <#if cr.spec.vm.display.spice.streamingVideo??> + ticket: "${ cr.spec.vm.display.spice.streamingVideo.asString }" - usbRedirects: ${ spec.vm.display.spice.usbRedirects?c } + usbRedirects: ${ cr.spec.vm.display.spice.usbRedirects.asInt?c } logging.properties: | diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerDataPvc.ftl.yaml b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerDataPvc.ftl.yaml deleted file mode 100644 index ddb638c..0000000 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerDataPvc.ftl.yaml +++ /dev/null @@ -1,18 +0,0 @@ -kind: PersistentVolumeClaim -apiVersion: v1 -metadata: - namespace: ${ cr.namespace() } - name: ${ runnerDataPvcName } - labels: - app.kubernetes.io/name: ${ constants.APP_NAME } - app.kubernetes.io/instance: ${ cr.name() } - app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME } -spec: - accessModes: - - ReadWriteOnce - <#if reconciler.runnerDataPvc?? && reconciler.runnerDataPvc.storageClassName??> - storageClassName: ${ reconciler.runnerDataPvc.storageClassName } - - resources: - requests: - storage: 1Mi diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerDiskPvc.ftl.yaml b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerDiskPvc.ftl.yaml deleted file mode 100644 index 8258d55..0000000 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerDiskPvc.ftl.yaml +++ /dev/null @@ -1,16 +0,0 @@ -kind: PersistentVolumeClaim -apiVersion: v1 -metadata: - namespace: ${ cr.namespace() } - name: ${ disk.generatedPvcName } - labels: - app.kubernetes.io/name: ${ constants.APP_NAME } - app.kubernetes.io/instance: ${ cr.name() } - app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME } - <#if disk.volumeClaimTemplate.metadata?? - && disk.volumeClaimTemplate.metadata.annotations??> - annotations: - ${ toJson(disk.volumeClaimTemplate.metadata.annotations) } - -spec: - ${ toJson(disk.volumeClaimTemplate.spec) } diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerLoadBalancer.ftl.yaml b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerLoadBalancer.ftl.yaml index b7215a5..2c32aa6 100644 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerLoadBalancer.ftl.yaml +++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerLoadBalancer.ftl.yaml @@ -1,26 +1,26 @@ apiVersion: v1 kind: Service metadata: - namespace: ${ cr.namespace() } - name: ${ cr.name() } + namespace: ${ cr.metadata.namespace.asString } + name: ${ cr.metadata.name.asString } labels: app.kubernetes.io/name: ${ constants.APP_NAME } - app.kubernetes.io/instance: ${ cr.name() } + app.kubernetes.io/instance: ${ cr.metadata.name.asString } app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME } annotations: vmoperator.jdrupes.org/version: ${ managerVersion } ownerReferences: - - apiVersion: ${ cr.apiVersion() } - kind: ${ constants.Crd.KIND_VM } - name: ${ cr.name() } - uid: ${ cr.metadata().getUid() } + - apiVersion: ${ cr.apiVersion.asString } + kind: ${ constants.VM_OP_KIND_VM } + name: ${ cr.metadata.name.asString } + uid: ${ cr.metadata.uid.asString } controller: false spec: type: LoadBalancer ports: - name: spice - port: ${ cr.spec().vm.display.spice.port?c } + port: ${ cr.spec.vm.display.spice.port.asInt?c } selector: app.kubernetes.io/name: ${ constants.APP_NAME } - app.kubernetes.io/instance: ${ cr.name() } + app.kubernetes.io/instance: ${ cr.metadata.name.asString } diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerPod.ftl.yaml b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerPod.ftl.yaml deleted file mode 100644 index 7518ad3..0000000 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerPod.ftl.yaml +++ /dev/null @@ -1,135 +0,0 @@ -kind: Pod -apiVersion: v1 -metadata: - namespace: ${ cr.namespace() } - name: ${ cr.name() } - labels: - app.kubernetes.io/name: ${ constants.APP_NAME } - app.kubernetes.io/instance: ${ cr.name() } - app.kubernetes.io/component: ${ constants.APP_NAME } - app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME } - annotations: - # Triggers update of config map mounted in pod - # See https://ahmet.im/blog/kubernetes-secret-volumes-delay/ - vmrunner.jdrupes.org/cmVersion: "${ configMapResourceVersion }" - vmoperator.jdrupes.org/version: ${ managerVersion } - ownerReferences: - - apiVersion: ${ cr.apiVersion() } - kind: ${ constants.Crd.KIND_VM } - name: ${ cr.name() } - uid: ${ cr.metadata().getUid() } - blockOwnerDeletion: true - controller: false -<#assign spec = cr.spec() /> -spec: - containers: - - name: ${ cr.name() } - <#assign image = spec.image> - <#if image.source??> - image: ${ image.source } - <#else> - image: ${ image.repository }/${ image.path }<#if image.version??>:${ image.version } - - <#if image.pullPolicy??> - imagePullPolicy: ${ image.pullPolicy } - - <#if spec.vm.display.spice??> - ports: - <#if spec.vm.display.spice??> - - name: spice - containerPort: ${ spec.vm.display.spice.port?c } - protocol: TCP - - - volumeMounts: - # Not needed because pod is priviledged: - # - mountPath: /dev/kvm - # name: dev-kvm - # - mountPath: /dev/net/tun - # name: dev-tun - # - mountPath: /sys/fs/cgroup - # name: cgroup - - name: config - mountPath: /etc/opt/vmrunner - - name: runner-data - mountPath: /var/local/vm-data - - name: vmop-image-repository - mountPath: ${ constants.IMAGE_REPO_PATH } - volumeDevices: - <#list spec.vm.disks as disk> - <#if disk.volumeClaimTemplate??> - - name: ${ disk.generatedDiskName } - devicePath: /dev/${ disk.generatedDiskName } - - - securityContext: - privileged: true - <#if spec.resources??> - resources: ${ toJson(spec.resources) } - <#else> - <#if spec.vm.currentCpus?? || spec.vm.currentRam?? > - resources: - requests: - <#if spec.vm.currentCpus?? > - <#assign factor = 2.0 /> - <#if reconciler.cpuOvercommit??> - <#assign factor = reconciler.cpuOvercommit * 1.0 /> - - cpu: ${ (parseQuantity(spec.vm.currentCpus) / factor)?c } - - <#if spec.vm.currentRam?? > - <#assign factor = 1.25 /> - <#if reconciler.ramOvercommit??> - <#assign factor = reconciler.ramOvercommit * 1.0 /> - - memory: ${ (parseQuantity(spec.vm.currentRam) / factor)?floor?c } - - - - volumes: - # Not needed because pod is priviledged: - # - name: dev-kvm - # hostPath: - # path: /dev/kvm - # type: CharDevice - # - hostPath: - # path: /dev/net/tun - # type: CharDevice - # name: dev-tun - # - name: cgroup - # hostPath: - # path: /sys/fs/cgroup - - name: config - projected: - sources: - - configMap: - name: ${ cr.name() } - <#if displaySecret??> - - secret: - name: ${ displaySecret } - - - name: vmop-image-repository - persistentVolumeClaim: - claimName: vmop-image-repository - - name: runner-data - persistentVolumeClaim: - claimName: ${ runnerDataPvcName } - <#list spec.vm.disks as disk> - <#if disk.volumeClaimTemplate??> - - name: ${ disk.generatedDiskName } - persistentVolumeClaim: - claimName: ${ disk.generatedPvcName } - - - hostNetwork: true - terminationGracePeriodSeconds: ${ (spec.vm.powerdownTimeout + 5)?c } - <#if spec.nodeName??> - nodeName: ${ spec.nodeName } - - <#if spec.nodeSelector??> - nodeSelector: ${ toJson(spec.nodeSelector) } - - <#if spec.affinity??> - affinity: ${ toJson(spec.affinity) } - - serviceAccountName: vm-runner diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerSts.ftl.yaml b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerSts.ftl.yaml new file mode 100644 index 0000000..ac1178a --- /dev/null +++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerSts.ftl.yaml @@ -0,0 +1,186 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + namespace: ${ cr.metadata.namespace.asString } + name: ${ cr.metadata.name.asString } + labels: + app.kubernetes.io/name: ${ constants.APP_NAME } + app.kubernetes.io/instance: ${ cr.metadata.name.asString } + app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME } + annotations: + vmoperator.jdrupes.org/version: ${ managerVersion } + ownerReferences: + - apiVersion: ${ cr.apiVersion.asString } + kind: ${ constants.VM_OP_KIND_VM } + name: ${ cr.metadata.name.asString } + uid: ${ cr.metadata.uid.asString } + blockOwnerDeletion: true + controller: false + +spec: + selector: + matchLabels: + app.kubernetes.io/name: ${ constants.APP_NAME } + app.kubernetes.io/instance: ${ cr.metadata.name.asString } + replicas: ${ (cr.spec.vm.state.asString == "Running")?then(1, 0) } + template: + metadata: + namespace: ${ cr.metadata.namespace.asString } + name: ${ cr.metadata.name.asString } + labels: + app.kubernetes.io/name: ${ constants.APP_NAME } + app.kubernetes.io/instance: ${ cr.metadata.name.asString } + app.kubernetes.io/component: ${ constants.APP_NAME } + app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME } + annotations: + # Triggers update of config map mounted in pod + # See https://ahmet.im/blog/kubernetes-secret-volumes-delay/ + vmrunner.jdrupes.org/cmVersion: "${ cm.metadata.resourceVersion.asString }" + vmoperator.jdrupes.org/version: ${ managerVersion } + spec: + containers: + - name: ${ cr.metadata.name.asString } + <#assign image = cr.spec.image> + <#if image.source??> + image: ${ image.source.asString } + <#else> + image: ${ image.repository.asString }/${ image.path.asString }<#if image.version??>:${ image.version.asString } + + <#if image.pullPolicy??> + imagePullPolicy: ${ image.pullPolicy.asString } + + <#if cr.spec.vm.display.spice??> + ports: + <#if cr.spec.vm.display.spice??> + - name: spice + containerPort: ${ cr.spec.vm.display.spice.port.asInt?c } + protocol: TCP + + + volumeMounts: + # Not needed because pod is priviledged: + # - mountPath: /dev/kvm + # name: dev-kvm + # - mountPath: /dev/net/tun + # name: dev-tun + # - mountPath: /sys/fs/cgroup + # name: cgroup + - name: config + mountPath: /etc/opt/vmrunner + - name: runner-data + mountPath: /var/local/vm-data + - name: vmop-image-repository + mountPath: ${ constants.IMAGE_REPO_PATH } + volumeDevices: + <#assign diskCounter = 0/> + <#list cr.spec.vm.disks.asList() as disk> + <#if disk.volumeClaimTemplate??> + <#if disk.volumeClaimTemplate.metadata?? + && disk.volumeClaimTemplate.metadata.name??> + <#assign diskName = disk.volumeClaimTemplate.metadata.name.asString + "-disk"> + <#else> + <#assign diskName = "disk-" + diskCounter> + + - name: ${ diskName } + devicePath: /dev/${ diskName } + <#assign diskCounter = diskCounter + 1/> + + + securityContext: + privileged: true + <#if cr.spec.resources??> + resources: ${ cr.spec.resources.toString() } + <#else> + <#if cr.spec.vm.currentCpus?? || cr.spec.vm.currentRam?? > + resources: + requests: + <#if cr.spec.vm.currentCpus?? > + <#assign factor = 2.0 /> + <#if reconciler.cpuOvercommit??> + <#assign factor = reconciler.cpuOvercommit * 1.0 /> + + cpu: ${ (parseQuantity(cr.spec.vm.currentCpus.asString) / factor)?c } + + <#if cr.spec.vm.currentRam?? > + <#assign factor = 1.25 /> + <#if reconciler.ramOvercommit??> + <#assign factor = reconciler.ramOvercommit * 1.0 /> + + memory: ${ (parseQuantity(cr.spec.vm.currentRam.asString) / factor)?floor?c } + + + + volumes: + # Not needed because pod is priviledged: + # - name: dev-kvm + # hostPath: + # path: /dev/kvm + # type: CharDevice + # - hostPath: + # path: /dev/net/tun + # type: CharDevice + # name: dev-tun + # - name: cgroup + # hostPath: + # path: /sys/fs/cgroup + - name: config + configMap: + name: ${ cr.metadata.name.asString } + - name: vmop-image-repository + persistentVolumeClaim: + claimName: vmop-image-repository + hostNetwork: true + terminationGracePeriodSeconds: ${ (cr.spec.vm.powerdownTimeout.asInt + 5)?c } + <#if cr.spec.nodeName??> + nodeName: ${ cr.spec.nodeName.asString } + + <#if cr.spec.nodeSelector??> + nodeSelector: ${ cr.spec.nodeSelector.toString() } + + <#if cr.spec.affinity??> + affinity: ${ cr.spec.affinity.toString() } + + serviceAccountName: vm-runner + volumeClaimTemplates: + - metadata: + namespace: ${ cr.metadata.namespace.asString } + name: runner-data + labels: + app.kubernetes.io/name: ${ constants.APP_NAME } + app.kubernetes.io/instance: ${ cr.metadata.name.asString } + app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME } + spec: + accessModes: + - ReadWriteOnce + <#if reconciler.runnerDataPvc?? && reconciler.runnerDataPvc.storageClassName??> + storageClassName: ${ reconciler.runnerDataPvc.storageClassName } + + resources: + requests: + storage: 1Mi + <#assign diskCounter = 0/> + <#list cr.spec.vm.disks.asList() as disk> + <#if disk.volumeClaimTemplate??> + <#if disk.volumeClaimTemplate.metadata?? + && disk.volumeClaimTemplate.metadata.name??> + <#assign diskName = disk.volumeClaimTemplate.metadata.name.asString + "-disk"> + <#else> + <#assign diskName = "disk-" + diskCounter> + + - metadata: + namespace: ${ cr.metadata.namespace.asString } + name: ${ diskName } + labels: + app.kubernetes.io/name: ${ constants.APP_NAME } + app.kubernetes.io/instance: ${ cr.metadata.name.asString } + app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME } + <#if disk.volumeClaimTemplate.metadata?? + && disk.volumeClaimTemplate.metadata.annotations??> + annotations: + ${ disk.volumeClaimTemplate.metadata.annotations.toString() } + + spec: + ${ disk.volumeClaimTemplate.spec.toString() } + <#assign diskCounter = diskCounter + 1/> + + diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/AbstractMonitor.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/AbstractMonitor.java deleted file mode 100644 index c10752e..0000000 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/AbstractMonitor.java +++ /dev/null @@ -1,251 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager; - -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.options.ListOptions; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.logging.Level; -import org.jdrupes.vmoperator.common.K8s; -import org.jdrupes.vmoperator.common.K8sClient; -import org.jdrupes.vmoperator.common.K8sObserver; -import org.jdrupes.vmoperator.manager.events.Exit; -import org.jgrapes.core.Channel; -import org.jgrapes.core.Component; -import org.jgrapes.core.Components; -import org.jgrapes.core.annotation.Handler; -import org.jgrapes.core.events.Start; -import org.jgrapes.core.events.Stop; -import org.jgrapes.util.events.ConfigurationUpdate; - -/** - * A base class for monitoring VM related resources. When started, - * it creates observers for all versions of the the {@link APIResource} - * configured by {@link #context(APIResource)}. The APIResource is not - * passed to the constructor because in some cases it has to be - * evaluated lazily. - * - * @param the object type for the context - * @param the object list type for the context - */ -public abstract class AbstractMonitor extends Component { - - private final Class objectClass; - private final Class objectListClass; - private K8sClient client; - private APIResource context; - private String namespace; - private ListOptions options = new ListOptions(); - private final AtomicInteger observerCounter = new AtomicInteger(0); - - /** - * Initializes the instance. - * - * @param componentChannel the component channel - * @param objectClass the class of the Kubernetes object to watch - * @param objectListClass the class of the list of Kubernetes objects - * to watch - */ - protected AbstractMonitor(Channel componentChannel, - Class objectClass, Class objectListClass) { - super(componentChannel); - this.objectClass = objectClass; - this.objectListClass = objectListClass; - } - - /** - * Return the client. - * - * @return the client - */ - public K8sClient client() { - return client; - } - - /** - * Sets the client to be used. - * - * @param client the client - * @return the abstract monitor - */ - public AbstractMonitor client(K8sClient client) { - this.client = client; - return this; - } - - /** - * Return the observed namespace. - * - * @return the namespace - */ - public String namespace() { - return namespace; - } - - /** - * Sets the namespace to be observed. - * - * @param namespace the namespaceToWatch to set - * @return the abstract monitor - */ - public AbstractMonitor namespace(String namespace) { - this.namespace = namespace; - return this; - } - - /** - * Returns the options for selecting the objects to observe. - * - * @return the options - */ - public ListOptions options() { - return options; - } - - /** - * Sets the options for selecting the objects to observe. - * - * @param options the options to set - * @return the abstract monitor - */ - public AbstractMonitor options(ListOptions options) { - this.options = options; - return this; - } - - /** - * Returns the observed context. - * - * @return the context - */ - public APIResource context() { - return context; - } - - /** - * Sets the context to observe. - * - * @param context the context - * @return the abstract monitor - */ - public AbstractMonitor context(APIResource context) { - this.context = context; - return this; - } - - /** - * Looks for a key "namespace" in the configuration and, if found, - * sets the namespace to its value. - * - * @param event the event - */ - @Handler - public void onConfigurationUpdate(ConfigurationUpdate event) { - event.structured(Components.manager(parent()).componentPath()) - .ifPresent(c -> { - if (c.containsKey("namespace")) { - namespace = (String) c.get("namespace"); - } - }); - } - - /** - * Handle the start event. Configures the namespace, invokes - * {@link #prepareMonitoring()} and starts the observers. - * - * @param event the event - */ - @Handler(priority = 10) - public void onStart(Start event) { - try { - // Get namespace - if (namespace == null) { - var path = Path - .of("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); - if (Files.isReadable(path)) { - namespace - = Files.lines(path).findFirst().orElse(null); - } - } - - // Additional preparations by derived class - prepareMonitoring(); - assert client != null; - assert context != null; - assert namespace != null; - - // Monitor all versions - for (var version : context.getVersions()) { - createObserver(version); - } - registerAsGenerator(); - } catch (IOException | ApiException e) { - logger.log(Level.SEVERE, e, - () -> "Cannot watch VMs, terminating."); - event.cancel(true); - fire(new Exit(1)); - } - } - - private void createObserver(String version) { - observerCounter.incrementAndGet(); - new K8sObserver<>(objectClass, objectListClass, client, - K8s.preferred(context, version), namespace, options) - .handler(this::handleChange).onTerminated((o, t) -> { - if (observerCounter.decrementAndGet() == 0) { - unregisterAsGenerator(); - } - // Exception has been logged already - if (t != null) { - fire(new Stop()); - } - }).start(); - } - - /** - * Invoked by {@link #onStart(Start)} after the namespace has - * been configured and before starting the observer. This is - * the last opportunity to invoke {@link #context(APIResource)}. - * - * @throws IOException Signals that an I/O exception has occurred. - * @throws ApiException the api exception - */ - @SuppressWarnings("PMD.EmptyMethodInAbstractClassShouldBeAbstract") - protected void prepareMonitoring() throws IOException, ApiException { - // To be overridden by derived class. - } - - /** - * Handle an observed change. The method is invoked by the observer - * thread(s). It is the responsibility of the implementing class to - * fire derived events on the appropriate event pipeline. - * - * @param client the client - * @param change the change - */ - protected abstract void handleChange(K8sClient client, Response change); -} diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/AvoidEmptyPolicy.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/AvoidEmptyPolicy.java deleted file mode 100644 index 912b623..0000000 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/AvoidEmptyPolicy.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.util.Collections; -import java.util.ResourceBundle; -import java.util.stream.Collectors; -import org.jgrapes.core.Channel; -import org.jgrapes.core.Component; -import org.jgrapes.core.annotation.Handler; -import org.jgrapes.webconlet.markdowndisplay.MarkdownDisplayConlet; -import org.jgrapes.webconsole.base.Conlet.RenderMode; -import org.jgrapes.webconsole.base.ConsoleConnection; -import org.jgrapes.webconsole.base.events.AddConletRequest; -import org.jgrapes.webconsole.base.events.ConsoleConfigured; -import org.jgrapes.webconsole.base.events.ConsoleReady; -import org.jgrapes.webconsole.base.events.RenderConlet; - -/** - * - */ -public class AvoidEmptyPolicy extends Component { - - private final String renderedFlagName = getClass().getName() + ".rendered"; - - /** - * Creates a new component with its channel set to the given channel. - * - * @param componentChannel - */ - public AvoidEmptyPolicy(Channel componentChannel) { - super(componentChannel); - } - - /** - * On console ready. - * - * @param event the event - * @param connection the connection - */ - @Handler - public void onConsoleReady(ConsoleReady event, - ConsoleConnection connection) { - connection.session().put(renderedFlagName, false); - } - - /** - * On render conlet. - * - * @param event the event - * @param connection the connection - */ - @Handler(priority = 100) - public void onRenderConlet(RenderConlet event, - ConsoleConnection connection) { - if (event.renderAs().contains(RenderMode.Preview) - || event.renderAs().contains(RenderMode.View)) { - connection.session().put(renderedFlagName, true); - } - } - - /** - * On console configured. - * - * @param event the event - * @param connection the console connection - * @throws InterruptedException the interrupted exception - */ - @Handler(priority = -100) - public void onConsoleConfigured(ConsoleConfigured event, - ConsoleConnection connection) throws InterruptedException, - IOException { - if ((Boolean) connection.session().getOrDefault(renderedFlagName, - false)) { - return; - } - var resourceBundle = ResourceBundle.getBundle( - getClass().getPackage().getName() + ".l10n", connection.locale(), - getClass().getClassLoader(), - ResourceBundle.Control.getNoFallbackControl( - ResourceBundle.Control.FORMAT_DEFAULT)); - var locale = resourceBundle.getLocale().toString(); - String shortDesc; - try (BufferedReader shortDescReader - = new BufferedReader(new InputStreamReader( - AvoidEmptyPolicy.class.getResourceAsStream( - "ManagerIntro-Preview" + (locale.isEmpty() ? "" - : "_" + locale) + ".md"), - "utf-8"))) { - shortDesc - = shortDescReader.lines().collect(Collectors.joining("\n")); - } - fire(new AddConletRequest(event.event().event().renderSupport(), - MarkdownDisplayConlet.class.getName(), - RenderMode.asSet(RenderMode.Preview)) - .addProperty(MarkdownDisplayConlet.CONLET_ID, - getClass().getName()) - .addProperty(MarkdownDisplayConlet.TITLE, - resourceBundle.getString("consoleTitle")) - .addProperty(MarkdownDisplayConlet.PREVIEW_SOURCE, - shortDesc) - .addProperty(MarkdownDisplayConlet.DELETABLE, true) - .addProperty(MarkdownDisplayConlet.EDITABLE_BY, - Collections.EMPTY_SET), - connection); - } - -} diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ConfigMapReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ConfigMapReconciler.java index 0ca6312..0fbb3a7 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ConfigMapReconciler.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ConfigMapReconciler.java @@ -18,18 +18,11 @@ package org.jdrupes.vmoperator.manager; -import com.google.gson.JsonObject; -import freemarker.template.AdapterTemplateModel; import freemarker.template.Configuration; import freemarker.template.TemplateException; -import freemarker.template.TemplateMethodModelEx; -import freemarker.template.TemplateModel; -import freemarker.template.TemplateModelException; -import freemarker.template.utility.DeepUnwrap; import io.kubernetes.client.custom.V1Patch; import io.kubernetes.client.openapi.ApiClient; import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.openapi.models.V1ObjectMeta; import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi; import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; import io.kubernetes.client.util.generic.dynamic.Dynamics; @@ -37,18 +30,13 @@ import io.kubernetes.client.util.generic.options.ListOptions; import io.kubernetes.client.util.generic.options.PatchOptions; import java.io.IOException; import java.io.StringWriter; -import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.Optional; import java.util.logging.Logger; import org.jdrupes.vmoperator.common.K8s; import static org.jdrupes.vmoperator.manager.Constants.APP_NAME; import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME; import org.jdrupes.vmoperator.manager.events.VmChannel; -import org.jdrupes.vmoperator.util.DataPath; -import org.jdrupes.vmoperator.util.GsonPtr; +import org.jdrupes.vmoperator.manager.events.VmDefChanged; import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.SafeConstructor; @@ -56,6 +44,7 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; /** * Delegee for reconciling the config map */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") /* default */ class ConfigMapReconciler { protected final Logger logger = Logger.getLogger(getClass().getName()); @@ -73,72 +62,34 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; /** * Reconcile. * + * @param event the event * @param model the model * @param channel the channel - * @param modelChanged the model has changed + * @return the dynamic kubernetes object * @throws IOException Signals that an I/O exception has occurred. * @throws TemplateException the template exception - * @throws ApiException the API exception + * @throws ApiException the api exception */ - public void reconcile(Map model, VmChannel channel, - boolean modelChanged) + public DynamicKubernetesObject reconcile(VmDefChanged event, + Map model, VmChannel channel) throws IOException, TemplateException, ApiException { - // Check if an update is needed - var prevData = channel.associated(PrevData.class) - .orElseGet(() -> new PrevData(null, new HashMap<>())); - Object newInputs = model.get("loginRequestedFor"); - if (!modelChanged && Objects.equals(prevData.inputs, newInputs)) { - // Make added data available in new model - model.putAll(prevData.added); - return; - } - prevData = new PrevData(newInputs, prevData.added); - channel.setAssociated(PrevData.class, prevData); + // Get API + DynamicKubernetesApi cmApi = new DynamicKubernetesApi("", "v1", + "configmaps", channel.client()); // Combine template and data and parse result - logger.fine(() -> "Create/update configmap " - + DataPath. get(model, "cr", "name").orElse("unknown")); - model.put("adjustCloudInitMeta", adjustCloudInitMetaModel); - prevData.added.put("adjustCloudInitMeta", adjustCloudInitMetaModel); var fmTemplate = fmConfig.getTemplate("runnerConfig.ftl.yaml"); StringWriter out = new StringWriter(); fmTemplate.process(model, out); // Avoid Yaml.load due to // https://github.com/kubernetes-client/java/issues/2741 - var newCm = Dynamics.newFromYaml( + var mapDef = Dynamics.newFromYaml( new Yaml(new SafeConstructor(new LoaderOptions())), out.toString()); - // Maybe override logging.properties from reconciler configuration. - DataPath. get(model, "reconciler", "loggingProperties") - .ifPresent(props -> { - GsonPtr.to(newCm.getRaw()).getAs(JsonObject.class, "data") - .get().addProperty("logging.properties", props); - }); - - // Maybe override logging.properties from VM definition. - DataPath. get(model, "cr", "spec", "loggingProperties") - .ifPresent(props -> { - GsonPtr.to(newCm.getRaw()).getAs(JsonObject.class, "data") - .get().addProperty("logging.properties", props); - }); - - // Get API and update - DynamicKubernetesApi cmApi = new DynamicKubernetesApi("", "v1", - "configmaps", channel.client()); - // Apply and maybe force pod update - var updatedCm = K8s.apply(cmApi, newCm, newCm.getRaw().toString()); - maybeForceUpdate(channel.client(), updatedCm); - model.put("configMapResourceVersion", - updatedCm.getMetadata().getResourceVersion()); - prevData.added.put("configMapResourceVersion", - updatedCm.getMetadata().getResourceVersion()); - } - - /** - * Key for association. - */ - private record PrevData(Object inputs, Map added) { + var newState = K8s.apply(cmApi, mapDef, out.toString()); + maybeForceUpdate(channel.client(), newState); + return newState; } /** @@ -153,16 +104,14 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; ListOptions listOpts = new ListOptions(); listOpts.setLabelSelector( "app.kubernetes.io/managed-by=" + VM_OP_NAME + "," - + "app.kubernetes.io/name=" + APP_NAME + "," - + "app.kubernetes.io/instance=" + newCm.getMetadata() - .getLabels().get("app.kubernetes.io/instance")); + + "app.kubernetes.io/name=" + APP_NAME); // Get pod, selected by label var podApi = new DynamicKubernetesApi("", "v1", "pods", client); var pods = podApi .list(newCm.getMetadata().getNamespace(), listOpts).getObject(); // If the VM is being created, the pod may not exist yet. - if (pods == null || pods.getItems().isEmpty()) { + if (pods == null || pods.getItems().size() == 0) { return; } var pod = pods.getItems().get(0); @@ -184,27 +133,4 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; } } - private final TemplateMethodModelEx adjustCloudInitMetaModel - = new TemplateMethodModelEx() { - @Override - public Object exec(@SuppressWarnings("rawtypes") List arguments) - throws TemplateModelException { - @SuppressWarnings("unchecked") - var res = new HashMap<>((Map) DeepUnwrap - .unwrap((TemplateModel) arguments.get(0))); - var metadata - = (V1ObjectMeta) ((AdapterTemplateModel) arguments.get(1)) - .getAdaptedObject(Object.class); - if (!res.containsKey("instance-id")) { - res.put("instance-id", - Optional.ofNullable(metadata.getGeneration()) - .map(s -> "v" + s).orElse("v1")); - } - if (!res.containsKey("local-hostname")) { - res.put("local-hostname", metadata.getName()); - } - return res; - } - }; - } diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Constants.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Constants.java index 2ef4199..bf51a59 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Constants.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Constants.java @@ -23,6 +23,9 @@ package org.jdrupes.vmoperator.manager; */ public class Constants extends org.jdrupes.vmoperator.common.Constants { + /** The Constant APP_NAME. */ + public static final String APP_NAME = "vm-runner"; + /** The Constant STATE_RUNNING. */ public static final String STATE_RUNNING = "Running"; diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Containerfile b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Containerfile index 08c4bff..c212945 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Containerfile +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Containerfile @@ -1,4 +1,4 @@ -FROM docker.io/eclipse-temurin:21-jre-alpine +FROM docker.io/eclipse-temurin:17-jre-alpine COPY build/install/vm-manager /opt/vmmanager diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java index ce14488..10bb1b2 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java @@ -1,6 +1,6 @@ /* * VM-Operator - * Copyright (C) 2023, 2025 Michael N. Lipp + * 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 @@ -18,42 +18,27 @@ package org.jdrupes.vmoperator.manager; -import com.google.gson.JsonObject; -import io.kubernetes.client.apimachinery.GroupVersionKind; +import io.kubernetes.client.custom.V1Patch; import io.kubernetes.client.openapi.ApiException; import io.kubernetes.client.openapi.Configuration; +import io.kubernetes.client.util.Config; +import io.kubernetes.client.util.generic.options.PatchOptions; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.time.Instant; -import java.util.Comparator; -import java.util.Optional; import java.util.logging.Level; -import org.jdrupes.vmoperator.common.Constants.Crd; -import org.jdrupes.vmoperator.common.Constants.Status; -import org.jdrupes.vmoperator.common.K8sClient; -import org.jdrupes.vmoperator.common.K8sObserver.ResponseType; -import org.jdrupes.vmoperator.common.VmDefinition.Assignment; -import org.jdrupes.vmoperator.common.VmDefinitionStub; -import org.jdrupes.vmoperator.common.VmPool; -import org.jdrupes.vmoperator.manager.events.AssignVm; -import org.jdrupes.vmoperator.manager.events.ChannelManager; -import org.jdrupes.vmoperator.manager.events.Exit; -import org.jdrupes.vmoperator.manager.events.GetPools; -import org.jdrupes.vmoperator.manager.events.GetVms; -import org.jdrupes.vmoperator.manager.events.GetVms.VmData; -import org.jdrupes.vmoperator.manager.events.ModifyVm; -import org.jdrupes.vmoperator.manager.events.PodChanged; -import org.jdrupes.vmoperator.manager.events.UpdateAssignment; -import org.jdrupes.vmoperator.manager.events.VmChannel; -import org.jdrupes.vmoperator.manager.events.VmPoolChanged; -import org.jdrupes.vmoperator.manager.events.VmResourceChanged; +import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP; +import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM; +import org.jdrupes.vmoperator.common.K8s; +import org.jdrupes.vmoperator.manager.events.StartVm; +import org.jdrupes.vmoperator.manager.events.StopVm; +import org.jdrupes.vmoperator.manager.events.VmDefChanged; import org.jgrapes.core.Channel; import org.jgrapes.core.Component; -import org.jgrapes.core.EventPipeline; import org.jgrapes.core.annotation.Handler; import org.jgrapes.core.events.HandlingError; import org.jgrapes.core.events.Start; +import org.jgrapes.core.events.Stop; import org.jgrapes.util.events.ConfigurationUpdate; /** @@ -61,8 +46,8 @@ import org.jgrapes.util.events.ConfigurationUpdate; * [Operator Whitepaper](https://github.com/cncf/tag-app-delivery/blob/eece8f7307f2970f46f100f51932db106db46968/operator-wg/whitepaper/Operator-WhitePaper_v1-0.md#operator-components-in-kubernetes). * * The implementation splits the controller in two components. The - * {@link VmMonitor} and the {@link Reconciler}. The former watches - * the VM definitions (CRs) and generates {@link VmResourceChanged} events + * {@link VmWatcher} and the {@link Reconciler}. The former watches + * the VM definitions (CRs) and generates {@link VmDefChanged} events * when they change. The latter handles the changes and reconciles the * resources in the cluster. * @@ -95,33 +80,15 @@ import org.jgrapes.util.events.ConfigurationUpdate; public class Controller extends Component { private String namespace; - private final ChannelManager chanMgr; /** * Creates a new instance. */ - @SuppressWarnings("PMD.ConstructorCallsOverridableMethod") public Controller(Channel componentChannel) { super(componentChannel); // Prepare component tree - chanMgr = new ChannelManager<>(name -> { - try { - return new VmChannel(channel(), newEventPipeline(), - new K8sClient()); - } catch (IOException e) { - logger.log(Level.SEVERE, e, () -> "Failed to create client" - + " for handling changes: " + e.getMessage()); - return null; - } - }); - attach(new VmMonitor(channel(), chanMgr)); - attach(new DisplaySecretMonitor(channel(), chanMgr)); - // Currently, we don't use the IP assigned by the load balancer - // to access the VM's console. Might change in the future. - // attach(new ServiceMonitor(channel()).channelManager(chanMgr)); + attach(new VmWatcher(channel())); attach(new Reconciler(channel())); - attach(new PoolMonitor(channel())); - attach(new PodMonitor(channel(), chanMgr)); } /** @@ -172,157 +139,66 @@ public class Controller extends Component { .of("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); if (Files.isReadable(path)) { namespace = Files.lines(path).findFirst().orElse(null); - fire(new ConfigurationUpdate().add(componentPath(), "namespace", - namespace)); } } if (namespace == null) { logger.severe(() -> "Namespace to control not configured and" + " no file in kubernetes directory."); event.cancel(true); - fire(new Exit(2)); + fire(new Stop()); return; } - logger.config(() -> "Controlling namespace \"" + namespace + "\"."); + logger.fine(() -> "Controlling namespace \"" + namespace + "\"."); } /** - * Returns the VM data. - * - * @param event the event - */ - @Handler - public void onGetVms(GetVms event) { - event.setResult(chanMgr.channels().stream() - .filter(c -> event.name().isEmpty() - || c.vmDefinition().name().equals(event.name().get())) - .filter(c -> event.user().isEmpty() && event.roles().isEmpty() - || !c.vmDefinition().permissionsFor(event.user().orElse(null), - event.roles()).isEmpty()) - .filter(c -> event.fromPool().isEmpty() - || c.vmDefinition().assignment().map(Assignment::pool) - .map(p -> p.equals(event.fromPool().get())).orElse(false)) - .filter(c -> event.toUser().isEmpty() - || c.vmDefinition().assignment().map(Assignment::user) - .map(u -> u.equals(event.toUser().get())).orElse(false)) - .map(c -> new VmData(c.vmDefinition(), c)) - .toList()); - } - - /** - * Assign a VM if not already assigned. + * On start vm. * * @param event the event * @throws ApiException the api exception - * @throws InterruptedException + * @throws IOException Signals that an I/O exception has occurred. */ @Handler - @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") - public void onAssignVm(AssignVm event) - throws ApiException, InterruptedException { - while (true) { - // Search for existing assignment. - var vmQuery = chanMgr.channels().stream() - .filter(c -> c.vmDefinition().assignment().map(Assignment::pool) - .map(p -> p.equals(event.fromPool())).orElse(false)) - .filter(c -> c.vmDefinition().assignment().map(Assignment::user) - .map(u -> u.equals(event.toUser())).orElse(false)) - .findFirst(); - if (vmQuery.isPresent()) { - var vmDef = vmQuery.get().vmDefinition(); - event.setResult(new VmData(vmDef, vmQuery.get())); - return; - } - - // Get the pool definition for checking possible assignment - VmPool vmPool = newEventPipeline().fire(new GetPools() - .withName(event.fromPool())).get().stream().findFirst() - .orElse(null); - if (vmPool == null) { - return; - } - - // Find available VM. - vmQuery = chanMgr.channels().stream() - .filter(c -> vmPool.isAssignable(c.vmDefinition())) - .sorted(Comparator.comparing((VmChannel c) -> c.vmDefinition() - .assignment().map(Assignment::lastUsed) - .orElse(Instant.ofEpochSecond(0))) - .thenComparing(preferRunning)) - .findFirst(); - - // None found - if (vmQuery.isEmpty()) { - return; - } - - // Assign to user - var chosenVm = vmQuery.get(); - if (Optional.ofNullable(chosenVm.fire(new UpdateAssignment( - vmPool, event.toUser())).get()).orElse(false)) { - var vmDef = chosenVm.vmDefinition(); - event.setResult(new VmData(vmDef, chosenVm)); - - // Make sure that a newly assigned VM is running. - chosenVm.fire(new ModifyVm(vmDef.name(), "state", "Running")); - return; - } - } + public void onStartVm(StartVm event) throws ApiException, IOException { + patchRunning(event.name(), true); } - private static Comparator preferRunning - = new Comparator<>() { - @Override - public int compare(VmChannel ch1, VmChannel ch2) { - if (ch1.vmDefinition().conditionStatus("Running").orElse(false) - && !ch2.vmDefinition().conditionStatus("Running") - .orElse(false)) { - return -1; - } - return 0; - } - }; - /** - * When s pool is deleted, remove all related assignments. + * On stop vm. * * @param event the event - * @throws InterruptedException + * @throws ApiException the api exception + * @throws IOException Signals that an I/O exception has occurred. */ @Handler - @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") - public void onPoolChanged(VmPoolChanged event) throws InterruptedException { - if (!event.deleted()) { + public void onStopVm(StopVm event) throws ApiException, IOException { + patchRunning(event.name(), false); + } + + private void patchRunning(String name, boolean running) + throws ApiException, IOException { + var crApi = K8s.crApi(Config.defaultClient(), VM_OP_GROUP, + VM_OP_KIND_VM, namespace, name); + if (crApi.isEmpty()) { + logger.warning(() -> "Trying to patch " + namespace + "/" + name + + " which does not exist."); return; } - var vms = newEventPipeline() - .fire(new GetVms().assignedFrom(event.vmPool().name())).get(); - for (var vm : vms) { - vm.channel().fire(new UpdateAssignment(event.vmPool(), null)); - } - } - /** - * Remove runner version from status when pod is deleted - * - * @param event the event - * @param channel the channel - * @throws ApiException the api exception - */ - @Handler - public void onPodChange(PodChanged event, VmChannel channel) - throws ApiException { - if (event.type() == ResponseType.DELETED) { - // Remove runner info from status - var vmDef = channel.vmDefinition(); - var vmStub = VmDefinitionStub.get(channel.client(), - new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM), - vmDef.namespace(), vmDef.name()); - vmStub.updateStatus(from -> { - JsonObject status = from.statusJson(); - status.remove(Status.RUNNER_VERSION); - return status; - }); + // Patch running + PatchOptions patchOpts = new PatchOptions(); + patchOpts.setFieldManager("kubernetes-java-kubectl-apply"); + var res = crApi.get().patch(namespace, name, + V1Patch.PATCH_FORMAT_JSON_PATCH, + new V1Patch("[{\"op\": \"replace\", \"path\": " + + "\"/spec/vm/state\", " + + "\"value\": \"" + (running ? "Running" : "Stopped") + + "\"}]"), + patchOpts); + if (!res.isSuccess()) { + logger.warning( + () -> "Cannot patch pod annotations: " + res.getStatus()); } + } } diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretMonitor.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretMonitor.java deleted file mode 100644 index b094b79..0000000 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretMonitor.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * 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.manager; - -import io.kubernetes.client.custom.V1Patch; -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.Watch.Response; -import io.kubernetes.client.util.generic.options.ListOptions; -import io.kubernetes.client.util.generic.options.PatchOptions; -import java.io.IOException; -import java.util.logging.Level; -import static org.jdrupes.vmoperator.common.Constants.APP_NAME; -import org.jdrupes.vmoperator.common.Constants.DisplaySecret; -import static org.jdrupes.vmoperator.common.Constants.VM_OP_NAME; -import org.jdrupes.vmoperator.common.K8sClient; -import org.jdrupes.vmoperator.common.K8sV1PodStub; -import org.jdrupes.vmoperator.common.K8sV1SecretStub; -import org.jdrupes.vmoperator.manager.events.ChannelDictionary; -import org.jdrupes.vmoperator.manager.events.VmChannel; -import org.jgrapes.core.Channel; - -/** - * Watches for changes of display secrets. Updates an artifical attribute - * of the pod running the VM in response to force an update of the files - * in the pod that reflect the information from the secret. - */ -public class DisplaySecretMonitor - extends AbstractMonitor { - - private final ChannelDictionary channelDictionary; - - /** - * Instantiates a new display secrets monitor. - * - * @param componentChannel the component channel - * @param channelDictionary the channel dictionary - */ - public DisplaySecretMonitor(Channel componentChannel, - ChannelDictionary channelDictionary) { - super(componentChannel, V1Secret.class, V1SecretList.class); - this.channelDictionary = channelDictionary; - context(K8sV1SecretStub.CONTEXT); - ListOptions options = new ListOptions(); - options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + "," - + "app.kubernetes.io/component=" + DisplaySecret.NAME); - options(options); - } - - @Override - protected void prepareMonitoring() throws IOException, ApiException { - client(new K8sClient()); - } - - @Override - protected void handleChange(K8sClient client, Response change) { - String vmName = change.object.getMetadata().getLabels() - .get("app.kubernetes.io/instance"); - if (vmName == null) { - return; - } - var channel = channelDictionary.channel(vmName).orElse(null); - if (channel == null || channel.vmDefinition() == null) { - return; - } - - try { - patchPod(client, change); - } catch (ApiException e) { - logger.log(Level.WARNING, e, - () -> "Cannot patch pod annotations: " + e.getMessage()); - } - } - - private void patchPod(K8sClient client, Response change) - throws ApiException { - // Force update for pod - ListOptions listOpts = new ListOptions(); - listOpts.setLabelSelector( - "app.kubernetes.io/managed-by=" + VM_OP_NAME + "," - + "app.kubernetes.io/name=" + APP_NAME + "," - + "app.kubernetes.io/instance=" + change.object.getMetadata() - .getLabels().get("app.kubernetes.io/instance")); - // Get pod, selected by label - var pods = K8sV1PodStub.list(client, namespace(), listOpts); - - // If the VM is being created, the pod may not exist yet. - if (pods.isEmpty()) { - return; - } - var pod = pods.iterator().next(); - - // Patch pod annotation - PatchOptions patchOpts = new PatchOptions(); - patchOpts.setFieldManager("kubernetes-java-kubectl-apply"); - pod.patch(V1Patch.PATCH_FORMAT_JSON_PATCH, - new V1Patch("[{\"op\": \"replace\", \"path\": " - + "\"/metadata/annotations/vmrunner.jdrupes.org~1dpVersion\", " - + "\"value\": \"" - + change.object.getMetadata().getResourceVersion() - + "\"}]"), - patchOpts); - } -} diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretReconciler.java deleted file mode 100644 index 1e3eb0f..0000000 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretReconciler.java +++ /dev/null @@ -1,342 +0,0 @@ -/* - * 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.manager; - -import com.google.gson.JsonObject; -import freemarker.template.TemplateException; -import io.kubernetes.client.apimachinery.GroupVersionKind; -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.openapi.models.V1ObjectMeta; -import io.kubernetes.client.openapi.models.V1Secret; -import io.kubernetes.client.util.generic.options.ListOptions; -import java.io.IOException; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.time.Instant; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Scanner; -import java.util.logging.Logger; -import static org.jdrupes.vmoperator.common.Constants.APP_NAME; -import org.jdrupes.vmoperator.common.Constants.Crd; -import org.jdrupes.vmoperator.common.Constants.DisplaySecret; -import org.jdrupes.vmoperator.common.Constants.Status; -import org.jdrupes.vmoperator.common.K8sV1SecretStub; -import org.jdrupes.vmoperator.common.VmDefinition; -import org.jdrupes.vmoperator.common.VmDefinitionStub; -import org.jdrupes.vmoperator.manager.events.GetDisplaySecret; -import org.jdrupes.vmoperator.manager.events.VmChannel; -import org.jdrupes.vmoperator.manager.events.VmResourceChanged; -import org.jdrupes.vmoperator.util.DataPath; -import org.jgrapes.core.Channel; -import org.jgrapes.core.CompletionLock; -import org.jgrapes.core.Component; -import org.jgrapes.core.Event; -import org.jgrapes.core.annotation.Handler; -import org.jgrapes.util.events.ConfigurationUpdate; -import org.jose4j.base64url.Base64; - -/** - * The properties of the display secret do not only depend on the - * VM definition, but also on events that occur during runtime. - * The reconciler for the display secret is therefore a separate - * component. - * - * The reconciler supports the following configuration properties: - * - * * `passwordValidity`: the validity of the random password in seconds. - * Used to calculate the password expiry time in the generated secret. - */ -public class DisplaySecretReconciler extends Component { - - protected final Logger logger = Logger.getLogger(getClass().getName()); - private int passwordValidity = 10; - private final List pendingPrepares - = Collections.synchronizedList(new LinkedList<>()); - - /** - * Instantiates a new display secret reconciler. - * - * @param componentChannel the component channel - */ - public DisplaySecretReconciler(Channel componentChannel) { - super(componentChannel); - } - - /** - * On configuration update. - * - * @param event the event - */ - @Handler - public void onConfigurationUpdate(ConfigurationUpdate event) { - event.structured(componentPath()) - // for backward compatibility - .or(() -> { - var oldConfig = event - .structured("/Manager/Controller/DisplaySecretMonitor"); - if (oldConfig.isPresent()) { - logger.warning(() -> "Using configuration with old " - + "path '/Manager/Controller/DisplaySecretMonitor' " - + "for `passwordValidity`, please update " - + "the configuration."); - } - return oldConfig; - }).ifPresent(c -> { - try { - Optional.ofNullable(c.get("passwordValidity")) - .map(p -> p instanceof Integer ? (Integer) p - : Integer.valueOf((String) p)) - .ifPresent(p -> { - passwordValidity = p; - }); - } catch (NumberFormatException e) { - logger.warning( - () -> "Malformed configuration: " + e.getMessage()); - } - }); - } - - /** - * Reconcile. If the configuration prevents generating a secret - * or the secret already exists, do nothing. Else generate a new - * secret with a random password and immediate expiration, thus - * preventing access to the display. - * - * @param vmDef the VM definition - * @param model the model - * @param channel the channel - * @param specChanged the spec changed - * @throws IOException Signals that an I/O exception has occurred. - * @throws TemplateException the template exception - * @throws ApiException the api exception - */ - public void reconcile(VmDefinition vmDef, Map model, - VmChannel channel, boolean specChanged) - throws IOException, TemplateException, ApiException { - // Nothing to do unless spec changed - if (!specChanged) { - return; - } - - // Secret needed at all? - var display = vmDef.fromVm("display").get(); - if (!DataPath. get(display, "spice", "generateSecret") - .orElse(true)) { - return; - } - - // Check if exists - ListOptions options = new ListOptions(); - options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + "," - + "app.kubernetes.io/component=" + DisplaySecret.NAME + "," - + "app.kubernetes.io/instance=" + vmDef.name()); - var stubs = K8sV1SecretStub.list(channel.client(), vmDef.namespace(), - options); - if (!stubs.isEmpty()) { - return; - } - - // Create secret - var secretName = vmDef.name() + "-" + DisplaySecret.NAME; - logger.fine(() -> "Create/update secret " + secretName); - var secret = new V1Secret(); - secret.setMetadata(new V1ObjectMeta().namespace(vmDef.namespace()) - .name(secretName) - .putLabelsItem("app.kubernetes.io/name", APP_NAME) - .putLabelsItem("app.kubernetes.io/component", DisplaySecret.NAME) - .putLabelsItem("app.kubernetes.io/instance", vmDef.name())); - secret.setType("Opaque"); - SecureRandom random = null; - try { - random = SecureRandom.getInstanceStrong(); - } catch (NoSuchAlgorithmException e) { // NOPMD - // "Every implementation of the Java platform is required - // to support at least one strong SecureRandom implementation." - } - byte[] bytes = new byte[16]; - random.nextBytes(bytes); - var password = Base64.encode(bytes); - secret.setStringData(Map.of(DisplaySecret.PASSWORD, password, - DisplaySecret.EXPIRY, "now")); - K8sV1SecretStub.create(channel.client(), secret); - } - - /** - * Prepares access to the console for the user from the event. - * Generates a new password and sends it to the runner. - * Requests the VM (via the runner) to login the user if specified - * in the event. - * - * @param event the event - * @param channel the channel - * @throws ApiException the api exception - */ - @Handler - public void onGetDisplaySecret(GetDisplaySecret event, VmChannel channel) - throws ApiException { - // Get VM definition and check if running - var vmStub = VmDefinitionStub.get(channel.client(), - new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM), - event.vmDefinition().namespace(), event.vmDefinition().name()); - var vmDef = vmStub.model().orElse(null); - if (vmDef == null || !vmDef.conditionStatus("Running").orElse(false)) { - return; - } - - // Update console user in status - vmDef = vmStub.updateStatus(from -> { - JsonObject status = from.statusJson(); - status.addProperty(Status.CONSOLE_USER, event.user()); - return status; - }).get(); - - // Get secret and update password in secret - var stub = getSecretStub(event, channel, vmDef); - if (stub == null) { - return; - } - var secret = stub.model().get(); - if (!updatePassword(secret, event)) { - return; - } - - // Register wait for confirmation (by VM status change, - // after secret update) - var pending = new PendingRequest(event, - event.vmDefinition().displayPasswordSerial().orElse(0L) + 1, - new CompletionLock(event, 1500)); - pendingPrepares.add(pending); - Event.onCompletion(event, e -> { - pendingPrepares.remove(pending); - }); - - // Update, will (eventually) trigger confirmation - stub.update(secret).getObject(); - } - - private K8sV1SecretStub getSecretStub(GetDisplaySecret event, - VmChannel channel, VmDefinition vmDef) throws ApiException { - // Look for secret - ListOptions options = new ListOptions(); - options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + "," - + "app.kubernetes.io/component=" + DisplaySecret.NAME + "," - + "app.kubernetes.io/instance=" + vmDef.name()); - var stubs = K8sV1SecretStub.list(channel.client(), vmDef.namespace(), - options); - if (stubs.isEmpty()) { - // No secret means no password for this VM wanted - event.setResult(null); - return null; - } - return stubs.iterator().next(); - } - - private boolean updatePassword(V1Secret secret, GetDisplaySecret event) { - var expiry = Optional.ofNullable(secret.getData() - .get(DisplaySecret.EXPIRY)).map(b -> new String(b)).orElse(null); - if (secret.getData().get(DisplaySecret.PASSWORD) != null - && stillValid(expiry)) { - // Fixed secret, don't touch - event.setResult( - new String(secret.getData().get(DisplaySecret.PASSWORD))); - return false; - } - - // Generate password and set expiry - SecureRandom random = null; - try { - random = SecureRandom.getInstanceStrong(); - } catch (NoSuchAlgorithmException e) { // NOPMD - // "Every implementation of the Java platform is required - // to support at least one strong SecureRandom implementation." - } - byte[] bytes = new byte[16]; - random.nextBytes(bytes); - var password = Base64.encode(bytes); - secret.setStringData(Map.of(DisplaySecret.PASSWORD, password, - DisplaySecret.EXPIRY, - Long.toString(Instant.now().getEpochSecond() + passwordValidity))); - event.setResult(password); - return true; - } - - private boolean stillValid(String expiry) { - if (expiry == null || "never".equals(expiry)) { - return true; - } - @SuppressWarnings({ "PMD.CloseResource", "resource" }) - var scanner = new Scanner(expiry); - if (!scanner.hasNextLong()) { - return false; - } - long expTime = scanner.nextLong(); - return expTime > Instant.now().getEpochSecond() + passwordValidity; - } - - /** - * On vm def changed. - * - * @param event the event - * @param channel the channel - */ - @Handler - @SuppressWarnings("PMD.AvoidSynchronizedStatement") - public void onVmResourceChanged(VmResourceChanged event, Channel channel) { - synchronized (pendingPrepares) { - String vmName = event.vmDefinition().name(); - for (var pending : pendingPrepares) { - if (pending.event.vmDefinition().name().equals(vmName) - && event.vmDefinition().displayPasswordSerial() - .map(s -> s >= pending.expectedSerial).orElse(false)) { - pending.lock.remove(); - // pending will be removed from pendingGest by - // waiting thread, see updatePassword - continue; - } - } - } - } - - /** - * The Class PendingGet. - */ - private static class PendingRequest { - public final GetDisplaySecret event; - public final long expectedSerial; - public final CompletionLock lock; - - /** - * Instantiates a new pending get. - * - * @param event the event - * @param expectedSerial the expected serial - */ - public PendingRequest(GetDisplaySecret event, long expectedSerial, - CompletionLock lock) { - super(); - this.event = event; - this.expectedSerial = expectedSerial; - this.lock = lock; - } - } -} diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/LoadBalancerReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/LoadBalancerReconciler.java index a66b432..efa95f4 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/LoadBalancerReconciler.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/LoadBalancerReconciler.java @@ -18,25 +18,24 @@ package org.jdrupes.vmoperator.manager; -import com.google.gson.Gson; +import com.google.gson.JsonObject; import freemarker.template.Configuration; import freemarker.template.TemplateException; import io.kubernetes.client.openapi.ApiException; import io.kubernetes.client.openapi.models.V1APIService; import io.kubernetes.client.openapi.models.V1ObjectMeta; +import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi; import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; import io.kubernetes.client.util.generic.dynamic.Dynamics; import java.io.IOException; import java.io.StringWriter; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; import java.util.logging.Logger; -import org.jdrupes.vmoperator.common.K8sV1ServiceStub; -import org.jdrupes.vmoperator.common.VmDefinition; +import org.jdrupes.vmoperator.common.K8s; import org.jdrupes.vmoperator.manager.events.VmChannel; -import org.jdrupes.vmoperator.util.DataPath; +import org.jdrupes.vmoperator.manager.events.VmDefChanged; import org.jdrupes.vmoperator.util.GsonPtr; import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; @@ -45,6 +44,7 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; /** * Delegee for reconciling the service */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") /* default */ class LoadBalancerReconciler { private static final String LOAD_BALANCER_SERVICE = "loadBalancerService"; @@ -68,47 +68,32 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; /** * Reconcile. * - * @param vmDef the VM definition + * @param event the event * @param model the model * @param channel the channel - * @param specChanged the spec changed * @throws IOException Signals that an I/O exception has occurred. * @throws TemplateException the template exception * @throws ApiException the api exception */ - public void reconcile(VmDefinition vmDef, Map model, - VmChannel channel, boolean specChanged) + public void reconcile(VmDefChanged event, + Map model, VmChannel channel) throws IOException, TemplateException, ApiException { - // Nothing to do unless spec changed - if (!specChanged) { - return; - } - // Check if to be generated - @SuppressWarnings({ "unchecked" }) - var lbsDef = Optional.of(model) + @SuppressWarnings({ "unchecked", "PMD.AvoidDuplicateLiterals" }) + var lbs = Optional.of(model) .map(m -> (Map) m.get("reconciler")) .map(c -> c.get(LOAD_BALANCER_SERVICE)).orElse(Boolean.FALSE); - if (!(lbsDef instanceof Map) && !(lbsDef instanceof Boolean)) { + if (lbs instanceof Boolean isOn && !isOn) { + return; + } + if (!(lbs instanceof Map)) { logger.warning(() -> "\"" + LOAD_BALANCER_SERVICE + "\" in configuration must be boolean or mapping but is " - + lbsDef.getClass() + "."); - return; - } - if (lbsDef instanceof Boolean isOn && !isOn) { - return; - } - - // Load balancer can also be turned off for VM - if (vmDef - .>> fromSpec(LOAD_BALANCER_SERVICE) - .map(m -> m.isEmpty()).orElse(false)) { + + lbs.getClass() + "."); return; } // Combine template and data and parse result - logger.fine(() -> "Create/update load balancer service for " - + DataPath. get(model, "cr", "name").orElse("unknown")); var fmTemplate = fmConfig.getTemplate("runnerLoadBalancer.ftl.yaml"); StringWriter out = new StringWriter(); fmTemplate.process(model, out); @@ -116,78 +101,63 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; // https://github.com/kubernetes-client/java/issues/2741 var svcDef = Dynamics.newFromYaml( new Yaml(new SafeConstructor(new LoaderOptions())), out.toString()); - @SuppressWarnings("unchecked") - var defaults = lbsDef instanceof Map - ? (Map>) lbsDef - : null; - var client = channel.client(); - mergeMetadata(client.getJSON().getGson(), svcDef, defaults, vmDef); + mergeMetadata(svcDef, lbs, channel); // Apply - var svcStub = K8sV1ServiceStub - .get(client, vmDef.namespace(), vmDef.name()); - if (svcStub.apply(svcDef).isEmpty()) { - logger.warning( - () -> "Could not patch service for " + svcStub.name()); - } + DynamicKubernetesApi svcApi = new DynamicKubernetesApi("", "v1", + "services", channel.client()); + K8s.apply(svcApi, svcDef, svcDef.getRaw().toString()); } - private void mergeMetadata(Gson gson, DynamicKubernetesObject svcDef, - Map> defaults, - VmDefinition vmDefinition) { - // Get specific load balancer metadata from VM definition - var vmLbMeta = vmDefinition - .>> fromSpec(LOAD_BALANCER_SERVICE) - .orElse(Collections.emptyMap()); + @SuppressWarnings("unchecked") + private void mergeMetadata(DynamicKubernetesObject svcDef, + Object lbsConfig, VmChannel channel) { + // Get metadata from config + Map asmData = Collections.emptyMap(); + if (lbsConfig instanceof Map config) { + asmData = (Map) config; + } + var json = channel.client().getJSON(); + JsonObject cfgMeta + = json.deserialize(json.serialize(asmData), JsonObject.class); - // Merge - var svcMeta = svcDef.getMetadata(); - var svcJsonMeta = GsonPtr.to(svcDef.getRaw()).to(METADATA); - Optional.ofNullable(mergeIfAbsent(svcMeta.getLabels(), - mergeReplace(defaults.get(LABELS), vmLbMeta.get(LABELS)))) - .ifPresent(lbls -> svcJsonMeta.set(LABELS, gson.toJsonTree(lbls))); - Optional.ofNullable(mergeIfAbsent(svcMeta.getAnnotations(), - mergeReplace(defaults.get(ANNOTATIONS), vmLbMeta.get(ANNOTATIONS)))) - .ifPresent(as -> svcJsonMeta.set(ANNOTATIONS, gson.toJsonTree(as))); + // Get metadata from VM definition + var vmMeta = GsonPtr.to(channel.vmDefinition().getRaw()).to("spec") + .get(JsonObject.class, LOAD_BALANCER_SERVICE) + .map(JsonObject::deepCopy).orElseGet(() -> new JsonObject()); + + // Merge Data from VM definition into config data + mergeReplace(GsonPtr.to(cfgMeta).to(LABELS).get(JsonObject.class), + GsonPtr.to(vmMeta).to(LABELS).get(JsonObject.class)); + mergeReplace( + GsonPtr.to(cfgMeta).to(ANNOTATIONS).get(JsonObject.class), + GsonPtr.to(vmMeta).to(ANNOTATIONS).get(JsonObject.class)); + + // Merge additional data into service definition + var svcMeta = GsonPtr.to(svcDef.getRaw()).to(METADATA); + mergeIfAbsent(svcMeta.to(LABELS).get(JsonObject.class), + GsonPtr.to(cfgMeta).to(LABELS).get(JsonObject.class)); + mergeIfAbsent(svcMeta.to(ANNOTATIONS).get(JsonObject.class), + GsonPtr.to(cfgMeta).to(ANNOTATIONS).get(JsonObject.class)); } - private Map mergeReplace(Map dest, - Map src) { - if (src == null) { - return dest; - } - if (dest == null) { - dest = new LinkedHashMap<>(); - } else { - dest = new LinkedHashMap<>(dest); - } + private void mergeReplace(JsonObject dest, JsonObject src) { for (var e : src.entrySet()) { - if (e.getValue() == null) { + if (e.getValue().isJsonNull()) { dest.remove(e.getKey()); continue; } - dest.put(e.getKey(), e.getValue()); + dest.add(e.getKey(), e.getValue()); } - return dest; } - private Map mergeIfAbsent(Map dest, - Map src) { - if (src == null) { - return dest; - } - if (dest == null) { - dest = new LinkedHashMap<>(); - } else { - dest = new LinkedHashMap<>(dest); - } + private void mergeIfAbsent(JsonObject dest, JsonObject src) { for (var e : src.entrySet()) { - if (dest.containsKey(e.getKey())) { + if (dest.has(e.getKey())) { continue; } - dest.put(e.getKey(), e.getValue()); + dest.add(e.getKey(), e.getValue()); } - return dest; } } diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Manager.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Manager.java index f431c9d..f668c21 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Manager.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Manager.java @@ -18,8 +18,6 @@ package org.jdrupes.vmoperator.manager; -import freemarker.template.TemplateMethodModelEx; -import freemarker.template.TemplateModelException; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -29,9 +27,6 @@ import java.net.URISyntaxException; import java.nio.file.Files; import java.util.Arrays; import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -41,7 +36,6 @@ import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME; -import org.jdrupes.vmoperator.manager.events.Exit; import org.jdrupes.vmoperator.util.FsdUtils; import org.jgrapes.core.Channel; import org.jgrapes.core.Component; @@ -50,23 +44,18 @@ import org.jgrapes.core.NamedChannel; import org.jgrapes.core.annotation.Handler; import org.jgrapes.core.events.HandlingError; import org.jgrapes.core.events.Stop; -import org.jgrapes.http.HttpConnector; import org.jgrapes.http.HttpServer; import org.jgrapes.http.InMemorySessionManager; import org.jgrapes.http.LanguageSelector; import org.jgrapes.http.events.Request; import org.jgrapes.io.NioDispatcher; import org.jgrapes.io.util.PermitsPool; -import org.jgrapes.net.SocketConnector; import org.jgrapes.net.SocketServer; -import org.jgrapes.net.SslCodec; import org.jgrapes.util.ComponentCollector; import org.jgrapes.util.FileSystemWatcher; import org.jgrapes.util.YamlConfigurationStore; -import org.jgrapes.util.events.ConfigurationUpdate; import org.jgrapes.util.events.WatchFile; -import org.jgrapes.webconlet.oidclogin.LoginConlet; -import org.jgrapes.webconlet.oidclogin.OidcClient; +import org.jgrapes.webconlet.locallogin.LoginConlet; import org.jgrapes.webconsole.base.BrowserLocalBackedKVStore; import org.jgrapes.webconsole.base.ConletComponentFactory; import org.jgrapes.webconsole.base.ConsoleWeblet; @@ -75,48 +64,41 @@ import org.jgrapes.webconsole.base.PageResourceProviderFactory; import org.jgrapes.webconsole.base.WebConsole; import org.jgrapes.webconsole.rbac.RoleConfigurator; import org.jgrapes.webconsole.rbac.RoleConletFilter; -import org.jgrapes.webconsole.rbac.UserLogger; import org.jgrapes.webconsole.vuejs.VueJsConsoleWeblet; /** * The application class. */ -@SuppressWarnings({ "PMD.ExcessiveImports" }) public class Manager extends Component { - private static String version; private static Manager app; - private String clusterName; - private String namespace = "unknown"; - private static int exitStatus; /** * Instantiates a new manager. * @param cmdLine * * @throws IOException Signals that an I/O exception has occurred. - * @throws URISyntaxException */ - @SuppressWarnings({ "PMD.NcssCount", - "PMD.ConstructorCallsOverridableMethod" }) - public Manager(CommandLine cmdLine) throws IOException, URISyntaxException { - super(new NamedChannel("manager")); + @SuppressWarnings("PMD.TooFewBranchesForASwitchStatement") + public Manager(CommandLine cmdLine) throws IOException { // Prepare component tree attach(new NioDispatcher()); - attach(new FileSystemWatcher(channel())); - attach(new Controller(channel())); + Channel mgrChannel = new NamedChannel("manager"); + attach(new FileSystemWatcher(mgrChannel)); + attach(new Controller(mgrChannel)); // Configuration store with file in /etc/opt (default) File cfgFile = new File(cmdLine.getOptionValue('c', - "/etc/opt/" + VM_OP_NAME.replace("-", "") + "/config.yaml")); + "/etc/opt/" + VM_OP_NAME.replace("-", "") + "/config.yaml")) + .getCanonicalFile(); logger.config(() -> "Using configuration from: " + cfgFile.getPath()); // Don't rely on night config to produce a good exception // for this simple case if (!Files.isReadable(cfgFile.toPath())) { throw new IOException("Cannot read configuration file " + cfgFile); } - attach(new YamlConfigurationStore(channel(), cfgFile, false)); - fire(new WatchFile(cfgFile.toPath()), channel()); + attach(new YamlConfigurationStore(mgrChannel, cfgFile, false)); + fire(new WatchFile(cfgFile.toPath())); // Prepare GUI Channel httpTransport = new NamedChannel("guiTransport"); @@ -126,19 +108,9 @@ public class Manager extends Component { .setServerAddress(new InetSocketAddress(8080)) .setName("GuiSocketServer")); - // Channel for HTTP application layer - Channel httpChannel = new NamedChannel("guiHttp"); - - // Create network channels for client requests. - Channel requestChannel = attach(new SocketConnector(SELF)); - Channel secReqChannel - = attach(new SslCodec(SELF, requestChannel, true)); - // Support for making HTTP requests - attach(new HttpConnector(httpChannel, requestChannel, - secReqChannel)); - // Create an HTTP server as converter between transport and application // layer. + Channel httpChannel = new NamedChannel("guiHttp"); HttpServer guiHttpServer = attach(new HttpServer(httpChannel, httpTransport, Request.In.Get.class, Request.In.Post.class)); guiHttpServer.setName("GuiHttpServer"); @@ -154,12 +126,7 @@ public class Manager extends Component { return; } ConsoleWeblet consoleWeblet = guiHttpServer - .attach(new VueJsConsoleWeblet(httpChannel, SELF, rootUri) { - @Override - protected Map createConsoleBaseModel() { - return augmentBaseModel(super.createConsoleBaseModel()); - } - }) + .attach(new VueJsConsoleWeblet(httpChannel, Channel.SELF, rootUri)) .prependClassTemplateLoader(getClass()) .prependResourceBundleProvider(getClass()) .prependConsoleResourceProvider(getClass()); @@ -168,18 +135,12 @@ public class Manager extends Component { console.attach(new BrowserLocalBackedKVStore( console.channel(), consoleWeblet.prefix().getPath())); console.attach(new KVStoreBasedConsolePolicy(console.channel())); - console.attach(new AvoidEmptyPolicy(console.channel())); console.attach(new RoleConfigurator(console.channel())); console.attach(new RoleConletFilter(console.channel())); console.attach(new LoginConlet(console.channel())); - console.attach(new OidcClient(console.channel(), httpChannel, - httpChannel, new URI("/oauth/callback"), 1500)); - console.attach(new UserLogger(console.channel())); - // Add all available page resource providers console.attach(new ComponentCollector<>( PageResourceProviderFactory.class, console.channel())); - // Add all available conlets console.attach(new ComponentCollector<>( ConletComponentFactory.class, console.channel(), type -> { @@ -192,46 +153,6 @@ public class Manager extends Component { })); } - private Map augmentBaseModel(Map base) { - base.put("version", version); - base.put("clusterName", new TemplateMethodModelEx() { - @Override - public Object exec(@SuppressWarnings("rawtypes") List arguments) - throws TemplateModelException { - return clusterName; - } - }); - base.put("namespace", new TemplateMethodModelEx() { - @Override - public Object exec(@SuppressWarnings("rawtypes") List arguments) - throws TemplateModelException { - return namespace; - } - }); - return base; - } - - /** - * Configure the component. - * - * @param event the event - */ - @Handler - public void onConfigurationUpdate(ConfigurationUpdate event) { - event.structured(componentPath()).ifPresent(c -> { - if (c.containsKey("clusterName")) { - clusterName = (String) c.get("clusterName"); - } else { - clusterName = null; - } - }); - event.structured(componentPath() + "/Controller").ifPresent(c -> { - if (c.containsKey("namespace")) { - namespace = (String) c.get("namespace"); - } - }); - } - /** * Log the exception when a handling error is reported. * @@ -246,16 +167,6 @@ public class Manager extends Component { event.stop(); } - /** - * On exit. - * - * @param event the event - */ - @Handler - public void onExit(Exit event) { - exitStatus = event.exitStatus(); - } - /** * On stop. * @@ -263,7 +174,7 @@ public class Manager extends Component { */ @Handler(priority = -1000) public void onStop(Stop event) { - logger.info(() -> "Application stopped."); + logger.fine(() -> "Application stopped."); } static { @@ -290,14 +201,13 @@ public class Manager extends Component { * @param args the arguments * @throws Exception the exception */ + @SuppressWarnings("PMD.SignatureDeclareThrowsException") public static void main(String[] args) { try { // Instance logger is not available yet. var logger = Logger.getLogger(Manager.class.getName()); - version = Optional.ofNullable( - Manager.class.getPackage().getImplementationVersion()) - .orElse("unknown"); - logger.config(() -> "Version: " + version); + logger.config(() -> "Version: " + + Manager.class.getPackage().getImplementationVersion()); logger.config(() -> "running on " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.version") + ")" @@ -318,18 +228,14 @@ public class Manager extends Component { try { app.fire(new Stop()); Components.awaitExhaustion(); - } catch (InterruptedException e) { // NOPMD + } catch (InterruptedException e) { // Cannot do anything about this. } })); // Start the application Components.start(app); - - // Wait for (regular) termination - Components.awaitExhaustion(); - System.exit(exitStatus); - } catch (IOException | InterruptedException | URISyntaxException + } catch (IOException | InterruptedException | org.apache.commons.cli.ParseException e) { Logger.getLogger(Manager.class.getName()).log(Level.SEVERE, e, () -> "Failed to start manager: " + e.getMessage()); diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PodMonitor.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PodMonitor.java deleted file mode 100644 index cfb49e5..0000000 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PodMonitor.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * 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.manager; - -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.openapi.models.V1Pod; -import io.kubernetes.client.openapi.models.V1PodList; -import io.kubernetes.client.util.Watch.Response; -import io.kubernetes.client.util.generic.options.ListOptions; -import java.io.IOException; -import java.time.Duration; -import java.time.Instant; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.logging.Level; -import static org.jdrupes.vmoperator.common.Constants.APP_NAME; -import static org.jdrupes.vmoperator.common.Constants.VM_OP_NAME; -import org.jdrupes.vmoperator.common.K8sClient; -import org.jdrupes.vmoperator.common.K8sObserver.ResponseType; -import org.jdrupes.vmoperator.common.K8sV1PodStub; -import org.jdrupes.vmoperator.manager.events.ChannelDictionary; -import org.jdrupes.vmoperator.manager.events.PodChanged; -import org.jdrupes.vmoperator.manager.events.VmChannel; -import org.jdrupes.vmoperator.manager.events.VmResourceChanged; -import org.jgrapes.core.Channel; -import org.jgrapes.core.annotation.Handler; - -/** - * Watches for changes of pods that run VMs. - */ -public class PodMonitor extends AbstractMonitor { - - private final ChannelDictionary channelDictionary; - - private final Map pendingChanges - = new ConcurrentHashMap<>(); - - /** - * Instantiates a new pod monitor. - * - * @param componentChannel the component channel - * @param channelDictionary the channel dictionary - */ - public PodMonitor(Channel componentChannel, - ChannelDictionary channelDictionary) { - super(componentChannel, V1Pod.class, V1PodList.class); - this.channelDictionary = channelDictionary; - context(K8sV1PodStub.CONTEXT); - ListOptions options = new ListOptions(); - options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + "," - + "app.kubernetes.io/component=" + APP_NAME + "," - + "app.kubernetes.io/managed-by=" + VM_OP_NAME); - options(options); - } - - @Override - protected void prepareMonitoring() throws IOException, ApiException { - client(new K8sClient()); - } - - @Override - protected void handleChange(K8sClient client, Response change) { - String vmName = change.object.getMetadata().getLabels() - .get("app.kubernetes.io/instance"); - if (vmName == null) { - return; - } - var channel = channelDictionary.channel(vmName).orElse(null); - var responseType = ResponseType.valueOf(change.type); - if (channel != null && channel.vmDefinition() != null) { - pendingChanges.remove(vmName); - channel.fire(new PodChanged(change.object, responseType)); - return; - } - - // VM definition not available yet, may happen during startup - if (responseType == ResponseType.DELETED) { - return; - } - purgePendingChanges(); - logger.finer(() -> "Add pending pod change for " + vmName); - pendingChanges.put(vmName, new PendingChange(Instant.now(), change)); - } - - private void purgePendingChanges() { - Instant tooOld = Instant.now().minus(Duration.ofMinutes(15)); - for (var itr = pendingChanges.entrySet().iterator(); itr.hasNext();) { - var change = itr.next(); - if (change.getValue().from().isBefore(tooOld)) { - itr.remove(); - logger.finer( - () -> "Cleaned pending pod change for " + change.getKey()); - } - } - } - - /** - * Check for pending changes. - * - * @param event the event - * @param channel the channel - */ - @Handler - public void onVmResourceChanged(VmResourceChanged event, - VmChannel channel) { - Optional.ofNullable(pendingChanges.remove(event.vmDefinition().name())) - .map(PendingChange::change).ifPresent(change -> { - logger.finer(() -> "Firing pending pod change for " - + event.vmDefinition().name()); - channel.fire(new PodChanged(change.object, - ResponseType.valueOf(change.type))); - if (logger.isLoggable(Level.FINER) - && pendingChanges.isEmpty()) { - logger.finer("No pending pod changes left."); - } - }); - } - - private record PendingChange(Instant from, Response change) { - } - -} diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PodReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PodReconciler.java deleted file mode 100644 index 4733e73..0000000 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PodReconciler.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager; - -import freemarker.template.Configuration; -import freemarker.template.TemplateException; -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.util.generic.dynamic.Dynamics; -import io.kubernetes.client.util.generic.options.ListOptions; -import io.kubernetes.client.util.generic.options.PatchOptions; -import java.io.IOException; -import java.io.StringWriter; -import java.util.Map; -import java.util.logging.Logger; -import static org.jdrupes.vmoperator.common.Constants.APP_NAME; -import org.jdrupes.vmoperator.common.Constants.DisplaySecret; -import org.jdrupes.vmoperator.common.K8sClient; -import org.jdrupes.vmoperator.common.K8sV1PodStub; -import org.jdrupes.vmoperator.common.K8sV1SecretStub; -import org.jdrupes.vmoperator.common.VmDefinition; -import org.jdrupes.vmoperator.common.VmDefinition.RequestedVmState; -import org.jdrupes.vmoperator.manager.events.VmChannel; -import org.yaml.snakeyaml.LoaderOptions; -import org.yaml.snakeyaml.Yaml; -import org.yaml.snakeyaml.constructor.SafeConstructor; - -/** - * Delegee for reconciling the pod. - */ -/* default */ class PodReconciler { - - protected final Logger logger = Logger.getLogger(getClass().getName()); - private final Configuration fmConfig; - - /** - * Instantiates a new pod reconciler. - * - * @param fmConfig the fm config - */ - public PodReconciler(Configuration fmConfig) { - this.fmConfig = fmConfig; - } - - /** - * Reconcile the pod. - * - * @param vmDef the vm def - * @param model the model - * @param channel the channel - * @param specChanged the spec changed - * @throws IOException Signals that an I/O exception has occurred. - * @throws TemplateException the template exception - * @throws ApiException the api exception - */ - public void reconcile(VmDefinition vmDef, Map model, - VmChannel channel, boolean specChanged) - throws IOException, TemplateException, ApiException { - // Get pod stub. - var podStub = K8sV1PodStub.get(channel.client(), vmDef.namespace(), - vmDef.name()); - - // Nothing to do if exists and should be running - if (vmDef.vmState() == RequestedVmState.RUNNING - && podStub.model().isPresent()) { - return; - } - - // Delete if running but should be stopped - if (vmDef.vmState() == RequestedVmState.STOPPED) { - if (podStub.model().isPresent()) { - podStub.delete(); - } - return; - } - - // Create pod. First combine template and data and parse result - logger.fine(() -> "Create/update pod " + podStub.name()); - addDisplaySecret(channel.client(), model, vmDef); - var fmTemplate = fmConfig.getTemplate("runnerPod.ftl.yaml"); - StringWriter out = new StringWriter(); - fmTemplate.process(model, out); - // Avoid Yaml.load due to - // https://github.com/kubernetes-client/java/issues/2741 - var podDef = Dynamics.newFromYaml( - new Yaml(new SafeConstructor(new LoaderOptions())), out.toString()); - - // Do apply changes - PatchOptions opts = new PatchOptions(); - opts.setForce(true); - opts.setFieldManager("kubernetes-java-kubectl-apply"); - if (podStub.apply(podDef).isEmpty()) { - logger.warning( - () -> "Could not patch pod for " + podStub.name()); - } - } - - private void addDisplaySecret(K8sClient client, Map model, - VmDefinition vmDef) throws ApiException { - ListOptions options = new ListOptions(); - options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + "," - + "app.kubernetes.io/component=" + DisplaySecret.NAME + "," - + "app.kubernetes.io/instance=" + vmDef.name()); - var dsStub = K8sV1SecretStub - .list(client, vmDef.namespace(), options).stream().findFirst(); - if (dsStub.isPresent()) { - dsStub.get().model().ifPresent(m -> { - model.put("displaySecret", m.getMetadata().getName()); - }); - } - } - -} diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PoolMonitor.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PoolMonitor.java deleted file mode 100644 index e554d5a..0000000 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PoolMonitor.java +++ /dev/null @@ -1,210 +0,0 @@ -/* - * 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.manager; - -import com.google.gson.JsonObject; -import io.kubernetes.client.apimachinery.GroupVersionKind; -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.util.Watch; -import java.io.IOException; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import org.jdrupes.vmoperator.common.Constants.Crd; -import org.jdrupes.vmoperator.common.Constants.Status; -import org.jdrupes.vmoperator.common.K8s; -import org.jdrupes.vmoperator.common.K8sClient; -import org.jdrupes.vmoperator.common.K8sDynamicModel; -import org.jdrupes.vmoperator.common.K8sDynamicModels; -import org.jdrupes.vmoperator.common.K8sDynamicStub; -import org.jdrupes.vmoperator.common.K8sObserver.ResponseType; -import org.jdrupes.vmoperator.common.VmDefinition.Assignment; -import org.jdrupes.vmoperator.common.VmDefinitionStub; -import org.jdrupes.vmoperator.common.VmPool; -import org.jdrupes.vmoperator.manager.events.GetPools; -import org.jdrupes.vmoperator.manager.events.VmPoolChanged; -import org.jdrupes.vmoperator.manager.events.VmResourceChanged; -import org.jdrupes.vmoperator.util.GsonPtr; -import org.jgrapes.core.Channel; -import org.jgrapes.core.EventPipeline; -import org.jgrapes.core.annotation.Handler; -import org.jgrapes.core.events.Attached; - -/** - * Watches for changes of VM pools. Reports the changes using - * {@link VmPoolChanged} events fired on a special pipeline to - * avoid concurrent change informations. - */ -public class PoolMonitor extends - AbstractMonitor { - - private final Map pools = new ConcurrentHashMap<>(); - private EventPipeline poolPipeline; - - /** - * Instantiates a new VM pool manager. - * - * @param componentChannel the component channel - */ - public PoolMonitor(Channel componentChannel) { - super(componentChannel, K8sDynamicModel.class, - K8sDynamicModels.class); - } - - /** - * On attached. - * - * @param event the event - */ - @Handler - @SuppressWarnings("PMD.CompareObjectsWithEquals") - public void onAttached(Attached event) { - if (event.node() == this) { - poolPipeline = newEventPipeline(); - } - } - - @Override - protected void prepareMonitoring() throws IOException, ApiException { - client(new K8sClient()); - - // Get all our API versions - var ctx = K8s.context(client(), Crd.GROUP, "", Crd.KIND_VM_POOL); - if (ctx.isEmpty()) { - logger.severe(() -> "Cannot get CRD context."); - return; - } - context(ctx.get()); - } - - @Override - protected void handleChange(K8sClient client, - Watch.Response response) { - - var type = ResponseType.valueOf(response.type); - var poolName = response.object.metadata().getName(); - - // When pool is deleted, save VMs in pending - if (type == ResponseType.DELETED) { - Optional.ofNullable(pools.get(poolName)).ifPresent(pool -> { - pool.setUndefined(); - if (pool.vms().isEmpty()) { - pools.remove(poolName); - } - poolPipeline.fire(new VmPoolChanged(pool, true)); - }); - return; - } - - // Get full definition - var poolModel = response.object; - if (poolModel.data() == null) { - // ADDED event does not provide data, see - // https://github.com/kubernetes-client/java/issues/3215 - try { - poolModel = K8sDynamicStub.get(client(), context(), namespace(), - poolModel.metadata().getName()).model().orElse(null); - } catch (ApiException e) { - return; - } - } - - // Get pool and merge changes - var vmPool = pools.computeIfAbsent(poolName, k -> new VmPool(poolName)); - vmPool.defineFrom(client().getJSON().getGson().fromJson( - GsonPtr.to(poolModel.data()).to("spec").get(), VmPool.class)); - poolPipeline.fire(new VmPoolChanged(vmPool)); - } - - /** - * Track VM definition changes. - * - * @param event the event - * @throws ApiException - */ - @Handler - public void onVmResourceChanged(VmResourceChanged event) - throws ApiException { - final var vmDef = event.vmDefinition(); - final String vmName = vmDef.name(); - switch (event.type()) { - case ADDED: - vmDef.> fromSpec("pools") - .orElse(Collections.emptyList()).stream().forEach(p -> { - pools.computeIfAbsent(p, k -> new VmPool(p)) - .vms().add(vmName); - poolPipeline.fire(new VmPoolChanged(pools.get(p))); - }); - break; - case DELETED: - pools.values().stream().forEach(p -> { - if (p.vms().remove(vmName)) { - poolPipeline.fire(new VmPoolChanged(p)); - } - }); - return; - default: - break; - } - - // Sync last usage to console state change if user matches - if (vmDef.assignment().map(Assignment::user) - .map(at -> at.equals(vmDef.consoleUser().orElse(null))) - .orElse(true)) { - return; - } - - var ccChange = vmDef.condition("ConsoleConnected") - .map(cc -> cc.getLastTransitionTime().toInstant()); - if (ccChange - .map(tt -> vmDef.assignment().map(Assignment::lastUsed) - .map(alu -> alu.isAfter(tt)).orElse(true)) - .orElse(true)) { - return; - } - var vmStub = VmDefinitionStub.get(client(), - new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM), - vmDef.namespace(), vmDef.name()); - vmStub.updateStatus(from -> { - // TODO - JsonObject status = from.statusJson(); - var assignment = GsonPtr.to(status).to(Status.ASSIGNMENT); - assignment.set("lastUsed", ccChange.get().toString()); - return status; - }); - } - - /** - * Return the requested pools. - * - * @param event the event - */ - @Handler - public void onGetPools(GetPools event) { - event.setResult(pools.values().stream().filter(VmPool::isDefined) - .filter(p -> event.name().isEmpty() - || p.name().equals(event.name().get())) - .filter(p -> event.forUser().isEmpty() && event.forRoles().isEmpty() - || !p.permissionsFor(event.forUser().orElse(null), - event.forRoles()).isEmpty()) - .toList()); - } -} diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PvcReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PvcReconciler.java deleted file mode 100644 index 515bfc9..0000000 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PvcReconciler.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.manager; - -import freemarker.core.ParseException; -import freemarker.template.Configuration; -import freemarker.template.MalformedTemplateNameException; -import freemarker.template.TemplateException; -import freemarker.template.TemplateNotFoundException; -import io.kubernetes.client.custom.V1Patch; -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.util.generic.dynamic.Dynamics; -import io.kubernetes.client.util.generic.options.ListOptions; -import io.kubernetes.client.util.generic.options.PatchOptions; -import java.io.IOException; -import java.io.StringWriter; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.logging.Logger; -import java.util.stream.Collectors; -import static org.jdrupes.vmoperator.common.Constants.APP_NAME; -import static org.jdrupes.vmoperator.common.Constants.VM_OP_NAME; -import org.jdrupes.vmoperator.common.K8sV1PvcStub; -import org.jdrupes.vmoperator.common.VmDefinition; -import org.jdrupes.vmoperator.manager.events.VmChannel; -import org.jdrupes.vmoperator.util.DataPath; -import org.jdrupes.vmoperator.util.GsonPtr; -import org.yaml.snakeyaml.LoaderOptions; -import org.yaml.snakeyaml.Yaml; -import org.yaml.snakeyaml.constructor.SafeConstructor; - -/** - * Delegee for reconciling the stateful set (effectively the pod). - */ -/* default */ class PvcReconciler { - - protected final Logger logger = Logger.getLogger(getClass().getName()); - private final Configuration fmConfig; - - /** - * Instantiates a new pvc reconciler. - * - * @param fmConfig the fm config - */ - public PvcReconciler(Configuration fmConfig) { - this.fmConfig = fmConfig; - } - - /** - * Reconcile the PVCs. - * - * @param vmDef the VM definition - * @param model the model - * @param channel the channel - * @param specChanged the spec changed - * @throws IOException Signals that an I/O exception has occurred. - * @throws TemplateException the template exception - * @throws ApiException the api exception - */ - @SuppressWarnings({ "unchecked" }) - public void reconcile(VmDefinition vmDef, Map model, - VmChannel channel, boolean specChanged) - throws IOException, TemplateException, ApiException { - Set knownPvcs; - if (!specChanged && channel.associated(this, Set.class).isPresent()) { - knownPvcs = (Set) channel.associated(this, Set.class).get(); - } else { - ListOptions listOpts = new ListOptions(); - listOpts.setLabelSelector( - "app.kubernetes.io/managed-by=" + VM_OP_NAME + "," - + "app.kubernetes.io/name=" + APP_NAME + "," - + "app.kubernetes.io/instance=" + vmDef.name()); - knownPvcs = K8sV1PvcStub.list(channel.client(), - vmDef.namespace(), listOpts).stream().map(K8sV1PvcStub::name) - .collect(Collectors.toSet()); - channel.setAssociated(this, knownPvcs); - } - - // Reconcile runner data pvc - reconcileRunnerDataPvc(vmDef, model, channel, knownPvcs, specChanged); - - // Reconcile pvcs for defined disks - var diskDefs = vmDef.>> fromVm("disks") - .orElse(List.of()); - var diskCounter = 0; - for (var diskDef : diskDefs) { - if (!diskDef.containsKey("volumeClaimTemplate")) { - continue; - } - var diskName = DataPath.get(diskDef, "volumeClaimTemplate", - "metadata", "name").map(name -> name + "-disk") - .orElse("disk-" + diskCounter); - diskCounter += 1; - diskDef.put("generatedDiskName", diskName); - - // Don't do anything if pvc with old (sts generated) name exists. - var stsDiskPvcName = diskName + "-" + vmDef.name() + "-0"; - if (knownPvcs.contains(stsDiskPvcName)) { - diskDef.put("generatedPvcName", stsDiskPvcName); - continue; - } - - // Update PVC - reconcileRunnerDiskPvc(vmDef, model, channel, specChanged, diskDef); - } - } - - private void reconcileRunnerDataPvc(VmDefinition vmDef, - Map model, VmChannel channel, - Set knownPvcs, boolean specChanged) - throws TemplateNotFoundException, MalformedTemplateNameException, - ParseException, IOException, TemplateException, ApiException { - - // Look for old (sts generated) name. - var stsRunnerDataPvcName - = "runner-data" + "-" + vmDef.name() + "-0"; - if (knownPvcs.contains(stsRunnerDataPvcName)) { - model.put("runnerDataPvcName", stsRunnerDataPvcName); - return; - } - - // Generate PVC - var runnerDataPvcName = vmDef.name() + "-runner-data"; - logger.fine(() -> "Create/update pvc " + runnerDataPvcName); - model.put("runnerDataPvcName", runnerDataPvcName); - if (!specChanged) { - // Augmenting the model is all we have to do - return; - } - var fmTemplate = fmConfig.getTemplate("runnerDataPvc.ftl.yaml"); - StringWriter out = new StringWriter(); - fmTemplate.process(model, out); - // Avoid Yaml.load due to - // https://github.com/kubernetes-client/java/issues/2741 - var pvcDef = Dynamics.newFromYaml( - new Yaml(new SafeConstructor(new LoaderOptions())), out.toString()); - - // Do apply changes - var pvcStub = K8sV1PvcStub.get(channel.client(), - vmDef.namespace(), (String) model.get("runnerDataPvcName")); - PatchOptions opts = new PatchOptions(); - opts.setForce(true); - opts.setFieldManager("kubernetes-java-kubectl-apply"); - if (pvcStub.patch(V1Patch.PATCH_FORMAT_APPLY_YAML, - new V1Patch(channel.client().getJSON().serialize(pvcDef)), opts) - .isEmpty()) { - logger.warning( - () -> "Could not patch pvc for " + pvcStub.name()); - } - } - - private void reconcileRunnerDiskPvc(VmDefinition vmDef, - Map model, VmChannel channel, boolean specChanged, - Map diskDef) - throws TemplateNotFoundException, MalformedTemplateNameException, - ParseException, IOException, TemplateException, ApiException { - // Generate PVC - var pvcName = vmDef.name() + "-" + diskDef.get("generatedDiskName"); - diskDef.put("generatedPvcName", pvcName); - if (!specChanged) { - // Augmenting the model is all we have to do - return; - } - - // Generate PVC - logger.fine(() -> "Create/update pvc " + pvcName); - model.put("disk", diskDef); - var fmTemplate = fmConfig.getTemplate("runnerDiskPvc.ftl.yaml"); - StringWriter out = new StringWriter(); - fmTemplate.process(model, out); - model.remove("disk"); - // Avoid Yaml.load due to - // https://github.com/kubernetes-client/java/issues/2741 - var pvcDef = Dynamics.newFromYaml( - new Yaml(new SafeConstructor(new LoaderOptions())), out.toString()); - - // Apply changes - var pvcStub - = K8sV1PvcStub.get(channel.client(), vmDef.namespace(), pvcName); - var pvc = pvcStub.model(); - if (pvc.isEmpty() - || !"Bound".equals(pvc.get().getStatus().getPhase())) { - // Does not exist or isn't bound, use apply - PatchOptions opts = new PatchOptions(); - opts.setForce(true); - opts.setFieldManager("kubernetes-java-kubectl-apply"); - if (pvcStub.patch(V1Patch.PATCH_FORMAT_APPLY_YAML, - new V1Patch(channel.client().getJSON().serialize(pvcDef)), opts) - .isEmpty()) { - logger.warning( - () -> "Could not patch pvc for " + pvcStub.name()); - } - return; - } - - // If bound, use json merge, omitting immutable fields - var spec = GsonPtr.to(pvcDef.getRaw()).to("spec"); - spec.removeExcept("volumeAttributesClassName", "resources"); - spec.get("resources").ifPresent(p -> p.removeExcept("requests")); - PatchOptions opts = new PatchOptions(); - opts.setFieldManager("kubernetes-java-kubectl-apply"); - if (pvcStub.patch(V1Patch.PATCH_FORMAT_JSON_MERGE_PATCH, - new V1Patch(channel.client().getJSON().serialize(pvcDef)), opts) - .isEmpty()) { - logger.warning( - () -> "Could not patch pvc for " + pvcStub.name()); - } - } -} diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciler.java index e580c48..c3fee7d 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciler.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciler.java @@ -18,40 +18,37 @@ package org.jdrupes.vmoperator.manager; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import freemarker.template.AdapterTemplateModel; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import freemarker.core.ParseException; import freemarker.template.Configuration; +import freemarker.template.DefaultObjectWrapperBuilder; +import freemarker.template.MalformedTemplateNameException; import freemarker.template.SimpleNumber; -import freemarker.template.SimpleScalar; import freemarker.template.TemplateException; import freemarker.template.TemplateExceptionHandler; +import freemarker.template.TemplateHashModel; import freemarker.template.TemplateMethodModelEx; import freemarker.template.TemplateModelException; +import freemarker.template.TemplateNotFoundException; import io.kubernetes.client.custom.Quantity; import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; import java.io.IOException; -import java.lang.reflect.Modifier; import java.math.BigDecimal; import java.math.BigInteger; import java.net.URI; import java.net.URISyntaxException; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.logging.Level; import org.jdrupes.vmoperator.common.Convertions; -import org.jdrupes.vmoperator.common.K8sObserver; -import org.jdrupes.vmoperator.common.VmDefinition; -import org.jdrupes.vmoperator.common.VmDefinition.Assignment; -import org.jdrupes.vmoperator.common.VmPool; -import org.jdrupes.vmoperator.manager.events.GetPools; -import org.jdrupes.vmoperator.manager.events.ResetVm; import org.jdrupes.vmoperator.manager.events.VmChannel; -import org.jdrupes.vmoperator.manager.events.VmResourceChanged; +import org.jdrupes.vmoperator.manager.events.VmDefChanged; +import org.jdrupes.vmoperator.manager.events.VmDefChanged.Type; import org.jdrupes.vmoperator.util.ExtendedObjectWrapper; +import org.jdrupes.vmoperator.util.GsonPtr; import org.jgrapes.core.Channel; import org.jgrapes.core.Component; import org.jgrapes.core.annotation.Handler; @@ -68,25 +65,20 @@ import org.jgrapes.util.events.ConfigurationUpdate; * * * A [`ConfigMap`](https://kubernetes.io/docs/concepts/configuration/configmap/) * that defines the configuration file for the runner. - * - * * A [`PVC`](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) - * for 1 MiB of persistent storage used by the Runner (referred to as the - * "runnerDataPvc") - * - * * The PVCs for the VM's disks. - * - * * A [`Pod`](https://kubernetes.io/docs/concepts/workloads/pods/) with the - * runner instance[^oldSts]. - * + * + * * A [`StatefulSet`](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/) + * that creates + * * the [`Pod`](https://kubernetes.io/docs/concepts/workloads/pods/) + * with the Runner instance, + * * a PVC for 1 MiB of persistent storage used by the Runner + * (referred to as the "runnerDataPvc") and + * * the PVCs for the VM's disks. + * * * (Optional) A load balancer * [`Service`](https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/) * that allows the user to access a VM's console without knowing which * node it runs on. * - * [^oldSts]: Before version 3.4, the operator created a - * [`StatefulSet`](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/) - * that created the pod. - * * The reconciler is part of the {@link Controller} component. It's * configuration properties are therefore defined in * ```yaml @@ -129,26 +121,15 @@ import org.jgrapes.util.events.ConfigurationUpdate; * ``` * This makes all VM consoles available at IP address 192.168.168.1 * with the port numbers from the VM definitions. - * - * * `loggingProperties`: If defined, specifies the default logging - * properties to be used by the runners managed by the controller. - * This property is a string that holds the content of - * a logging.properties file. - * - * @see org.jdrupes.vmoperator.manager.DisplaySecretReconciler */ -@SuppressWarnings({ "PMD.AvoidDuplicateLiterals" }) +@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", + "PMD.AvoidDuplicateLiterals" }) public class Reconciler extends Component { - /** The Constant mapper. */ - @SuppressWarnings("PMD.FieldNamingConventions") - protected static final ObjectMapper mapper = new ObjectMapper(); - + @SuppressWarnings("PMD.SingularField") private final Configuration fmConfig; private final ConfigMapReconciler cmReconciler; - private final DisplaySecretReconciler dsReconciler; - private final PvcReconciler pvcReconciler; - private final PodReconciler podReconciler; + private final StatefulSetReconciler stsReconciler; private final LoadBalancerReconciler lbReconciler; @SuppressWarnings("PMD.UseConcurrentHashMap") private final Map config = new HashMap<>(); @@ -158,7 +139,6 @@ public class Reconciler extends Component { * * @param componentChannel the component channel */ - @SuppressWarnings("PMD.ConstructorCallsOverridableMethod") public Reconciler(Channel componentChannel) { super(componentChannel); @@ -173,9 +153,7 @@ public class Reconciler extends Component { fmConfig.setClassForTemplateLoading(Reconciler.class, ""); cmReconciler = new ConfigMapReconciler(fmConfig); - dsReconciler = attach(new DisplaySecretReconciler(componentChannel)); - pvcReconciler = new PvcReconciler(fmConfig); - podReconciler = new PodReconciler(fmConfig); + stsReconciler = new StatefulSetReconciler(fmConfig); lbReconciler = new LoadBalancerReconciler(fmConfig); } @@ -197,131 +175,92 @@ public class Reconciler extends Component { * @param event the event * @param channel the channel * @throws ApiException the api exception - * @throws TemplateException the template exception - * @throws IOException Signals that an I/O exception has occurred. - */ - @Handler - public void onVmResourceChanged(VmResourceChanged event, VmChannel channel) - throws ApiException, TemplateException, IOException { - // Ownership relationships takes care of deletions - if (event.type() == K8sObserver.ResponseType.DELETED) { - return; - } - - // Create model for processing templates - var vmDef = event.vmDefinition(); - Map model = prepareModel(vmDef); - cmReconciler.reconcile(model, channel, event.specChanged()); - - // The remaining reconcilers depend only on changes of the spec part - // or the pod state. - if (!event.specChanged() && !event.podChanged()) { - return; - } - dsReconciler.reconcile(vmDef, model, channel, event.specChanged()); - pvcReconciler.reconcile(vmDef, model, channel, event.specChanged()); - podReconciler.reconcile(vmDef, model, channel, event.specChanged()); - lbReconciler.reconcile(vmDef, model, channel, event.specChanged()); - } - - /** - * Reset the VM by incrementing the reset count and doing a - * partial reconcile (configmap only). - * - * @param event the event - * @param channel the channel * @throws IOException - * @throws ApiException + * @throws ParseException + * @throws MalformedTemplateNameException + * @throws TemplateNotFoundException * @throws TemplateException + * @throws KubectlException */ @Handler - public void onResetVm(ResetVm event, VmChannel channel) - throws ApiException, IOException, TemplateException { - var vmDef = channel.vmDefinition(); - var extra = vmDef.extra(); - extra.resetCount(extra.resetCount() + 1); - Map model - = prepareModel(channel.vmDefinition()); - cmReconciler.reconcile(model, channel, true); + @SuppressWarnings("PMD.ConfusingTernary") + public void onVmDefChanged(VmDefChanged event, VmChannel channel) + throws ApiException, TemplateException, IOException { + // We're only interested in "spec" changes. + if (!event.specChanged()) { + return; + } + + // Ownership relationships takes care of deletions + var defMeta = event.vmDefinition().getMetadata(); + if (event.type() == Type.DELETED) { + logger.fine(() -> "VM \"" + defMeta.getName() + "\" deleted"); + return; + } + + // Reconcile, use "augmented" vm definition for model + Map model = prepareModel(patchCr(event.vmDefinition())); + var configMap = cmReconciler.reconcile(event, model, channel); + model.put("cm", configMap.getRaw()); + stsReconciler.reconcile(event, model, channel); + lbReconciler.reconcile(event, model, channel); } - private Map prepareModel(VmDefinition vmDef) - throws TemplateModelException, ApiException { + private DynamicKubernetesObject patchCr(DynamicKubernetesObject vmDef) { + var json = vmDef.getRaw().deepCopy(); + // Adjust cdromImage path + var disks + = GsonPtr.to(json).to("spec", "vm", "disks").get(JsonArray.class); + for (var disk : disks) { + var cdrom = (JsonObject) ((JsonObject) disk).get("cdrom"); + if (cdrom == null) { + continue; + } + String image = cdrom.get("image").getAsString(); + if (image.isEmpty()) { + continue; + } + try { + @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") + var imageUri = new URI("file://" + Constants.IMAGE_REPO_PATH + + "/").resolve(image); + if ("file".equals(imageUri.getScheme())) { + cdrom.addProperty("image", imageUri.getPath()); + } else { + cdrom.addProperty("image", imageUri.toString()); + } + } catch (URISyntaxException e) { + logger.warning(() -> "Invalid CDROM image: " + image); + } + } + return new DynamicKubernetesObject(json); + } + + @SuppressWarnings("PMD.CognitiveComplexity") + private Map prepareModel(DynamicKubernetesObject vmDef) + throws TemplateModelException { @SuppressWarnings("PMD.UseConcurrentHashMap") Map model = new HashMap<>(); model.put("managerVersion", Optional.ofNullable(Reconciler.class.getPackage() .getImplementationVersion()).orElse("(Unknown)")); - model.put("cr", vmDef); + model.put("cr", vmDef.getRaw()); + model.put("constants", + (TemplateHashModel) new DefaultObjectWrapperBuilder( + Configuration.VERSION_2_3_32) + .build().getStaticModels() + .get(Constants.class.getName())); model.put("reconciler", config); - model.put("constants", constantsMap(Constants.class)); - addLoginRequestedFor(model, vmDef); // Methods - model.put("parseQuantity", parseQuantityModel); - model.put("formatMemory", formatMemoryModel); - model.put("imageLocation", imgageLocationModel); - model.put("toJson", toJsonModel); - return model; - } - - /** - * Creates a map with constants. Needed because freemarker doesn't support - * nested classes with its static models. - * - * @param clazz the clazz - * @return the map - */ - @SuppressWarnings("PMD.EmptyCatchBlock") - private Map constantsMap(Class clazz) { - @SuppressWarnings("PMD.UseConcurrentHashMap") - Map result = new HashMap<>(); - Arrays.stream(clazz.getFields()).filter(f -> { - var modifiers = f.getModifiers(); - return Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers) - && f.getType() == String.class; - }).forEach(f -> { - try { - result.put(f.getName(), f.get(null)); - } catch (IllegalArgumentException | IllegalAccessException e) { - // Should not happen, ignore - } - }); - Arrays.stream(clazz.getClasses()).filter(c -> { - var modifiers = c.getModifiers(); - return Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers); - }).forEach(c -> { - result.put(c.getSimpleName(), constantsMap(c)); - }); - return result; - } - - private void addLoginRequestedFor(Map model, - VmDefinition vmDef) { - vmDef.assignment().filter(a -> { - try { - return newEventPipeline() - .fire(new GetPools().withName(a.pool())).get() - .stream().findFirst().map(VmPool::loginOnAssignment) - .orElse(false); - } catch (InterruptedException e) { - logger.log(Level.WARNING, e, e::getMessage); - } - return false; - }).map(Assignment::user) - .or(() -> vmDef.fromSpec("vm", "display", "loggedInUser")) - .ifPresent(u -> model.put("loginRequestedFor", u)); - } - - private final TemplateMethodModelEx parseQuantityModel - = new TemplateMethodModelEx() { + model.put("parseQuantity", new TemplateMethodModelEx() { @Override @SuppressWarnings("PMD.PreserveStackTrace") public Object exec(@SuppressWarnings("rawtypes") List arguments) throws TemplateModelException { var arg = arguments.get(0); - if (arg instanceof SimpleNumber number) { - return number.getAsNumber(); + if (arg instanceof Number number) { + return number; } try { return Quantity.fromString(arg.toString()).getNumber(); @@ -330,11 +269,10 @@ public class Reconciler extends Component { + "specified as \"" + arg + "\": " + e.getMessage()); } } - }; - - private final TemplateMethodModelEx formatMemoryModel - = new TemplateMethodModelEx() { + }); + model.put("formatMemory", new TemplateMethodModelEx() { @Override + @SuppressWarnings("PMD.PreserveStackTrace") public Object exec(@SuppressWarnings("rawtypes") List arguments) throws TemplateModelException { var arg = arguments.get(0); @@ -359,45 +297,8 @@ public class Reconciler extends Component { } return Convertions.formatMemory(bigInt); } - }; + }); + return model; + } - private final TemplateMethodModelEx imgageLocationModel - = new TemplateMethodModelEx() { - @Override - @SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition" }) - public Object exec(@SuppressWarnings("rawtypes") List arguments) - throws TemplateModelException { - var image = ((SimpleScalar) arguments.get(0)).getAsString(); - if (image.isEmpty()) { - return ""; - } - try { - var imageUri - = new URI("file://" + Constants.IMAGE_REPO_PATH + "/") - .resolve(image); - if ("file".equals(imageUri.getScheme())) { - return imageUri.getPath(); - } - return imageUri.toString(); - } catch (URISyntaxException e) { - logger.warning(() -> "Invalid CDROM image: " + image); - } - return image; - } - }; - - private final TemplateMethodModelEx toJsonModel - = new TemplateMethodModelEx() { - @Override - public Object exec(@SuppressWarnings("rawtypes") List arguments) - throws TemplateModelException { - try { - return mapper.writeValueAsString( - ((AdapterTemplateModel) arguments.get(0)) - .getAdaptedObject(Object.class)); - } catch (JsonProcessingException e) { - return "{}"; - } - } - }; } diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/StatefulSetReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/StatefulSetReconciler.java new file mode 100644 index 0000000..3cd6a39 --- /dev/null +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/StatefulSetReconciler.java @@ -0,0 +1,107 @@ +/* + * 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.manager; + +import freemarker.template.Configuration; +import freemarker.template.TemplateException; +import io.kubernetes.client.custom.V1Patch; +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi; +import io.kubernetes.client.util.generic.dynamic.Dynamics; +import io.kubernetes.client.util.generic.options.PatchOptions; +import java.io.IOException; +import java.io.StringWriter; +import java.util.Map; +import java.util.logging.Logger; +import org.jdrupes.vmoperator.common.K8s; +import org.jdrupes.vmoperator.manager.events.VmChannel; +import org.jdrupes.vmoperator.manager.events.VmDefChanged; +import org.jdrupes.vmoperator.util.GsonPtr; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.SafeConstructor; + +/** + * Delegee for reconciling the stateful set (effectively the pod). + */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") +/* default */ class StatefulSetReconciler { + + protected final Logger logger = Logger.getLogger(getClass().getName()); + private final Configuration fmConfig; + + /** + * Instantiates a new config map reconciler. + * + * @param fmConfig the fm config + */ + public StatefulSetReconciler(Configuration fmConfig) { + this.fmConfig = fmConfig; + } + + /** + * Reconcile stateful set. + * + * @param event the event + * @param model the model + * @param channel the channel + * @throws IOException Signals that an I/O exception has occurred. + * @throws TemplateException the template exception + * @throws ApiException the api exception + */ + public void reconcile(VmDefChanged event, Map model, + VmChannel channel) + throws IOException, TemplateException, ApiException { + DynamicKubernetesApi stsApi = new DynamicKubernetesApi("apps", "v1", + "statefulsets", channel.client()); + var metadata = event.vmDefinition().getMetadata(); + + // Combine template and data and parse result + var fmTemplate = fmConfig.getTemplate("runnerSts.ftl.yaml"); + StringWriter out = new StringWriter(); + fmTemplate.process(model, out); + // Avoid Yaml.load due to + // https://github.com/kubernetes-client/java/issues/2741 + var stsDef = Dynamics.newFromYaml( + new Yaml(new SafeConstructor(new LoaderOptions())), out.toString()); + + // If exists apply changes only when transitioning state + // or not running. + var existing = K8s.get(stsApi, metadata); + if (existing.isPresent()) { + var current = GsonPtr.to(existing.get().getRaw()) + .to("spec").getAsInt("replicas").orElse(1); + var desired = GsonPtr.to(stsDef.getRaw()) + .to("spec").getAsInt("replicas").orElse(1); + if (current == 1 && desired == 1) { + return; + } + } + + // Do apply changes + PatchOptions opts = new PatchOptions(); + opts.setForce(true); + opts.setFieldManager("kubernetes-java-kubectl-apply"); + stsApi.patch(stsDef.getMetadata().getNamespace(), + stsDef.getMetadata().getName(), V1Patch.PATCH_FORMAT_APPLY_YAML, + new V1Patch(channel.client().getJSON().serialize(stsDef)), + opts).throwsApiException(); + } + +} diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmMonitor.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmMonitor.java deleted file mode 100644 index 22f083c..0000000 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmMonitor.java +++ /dev/null @@ -1,326 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023,2025 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * 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.manager; - -import com.google.gson.JsonObject; -import io.kubernetes.client.apimachinery.GroupVersionKind; -import io.kubernetes.client.custom.V1Patch; -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.util.Watch; -import io.kubernetes.client.util.generic.options.ListOptions; -import java.io.IOException; -import java.net.HttpURLConnection; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; -import org.jdrupes.vmoperator.common.Constants.Crd; -import org.jdrupes.vmoperator.common.Constants.Status; -import org.jdrupes.vmoperator.common.K8s; -import org.jdrupes.vmoperator.common.K8sClient; -import org.jdrupes.vmoperator.common.K8sDynamicStub; -import org.jdrupes.vmoperator.common.K8sObserver.ResponseType; -import org.jdrupes.vmoperator.common.K8sV1ConfigMapStub; -import org.jdrupes.vmoperator.common.K8sV1StatefulSetStub; -import org.jdrupes.vmoperator.common.VmDefinition; -import org.jdrupes.vmoperator.common.VmDefinitionStub; -import org.jdrupes.vmoperator.common.VmDefinitions; -import org.jdrupes.vmoperator.common.VmExtraData; -import static org.jdrupes.vmoperator.manager.Constants.APP_NAME; -import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME; -import org.jdrupes.vmoperator.manager.events.ChannelManager; -import org.jdrupes.vmoperator.manager.events.ModifyVm; -import org.jdrupes.vmoperator.manager.events.PodChanged; -import org.jdrupes.vmoperator.manager.events.UpdateAssignment; -import org.jdrupes.vmoperator.manager.events.VmChannel; -import org.jdrupes.vmoperator.manager.events.VmResourceChanged; -import org.jdrupes.vmoperator.util.GsonPtr; -import org.jgrapes.core.Channel; -import org.jgrapes.core.Event; -import org.jgrapes.core.EventPipeline; -import org.jgrapes.core.annotation.Handler; - -/** - * Watches for changes of VM definitions. When a VM definition (CR) - * becomes known, is is registered with a {@link ChannelManager} and thus - * gets an associated {@link VmChannel} and an associated - * {@link EventPipeline}. - * - * The {@link EventPipeline} is used for submitting an action that processes - * the change data from kubernetes, eventually transforming it to a - * {@link VmResourceChanged} event that is handled by another - * {@link EventPipeline} associated with the {@link VmChannel}. This - * event pipeline should be used for all events related to changes of - * a particular VM. - */ -public class VmMonitor extends - AbstractMonitor { - - private final ChannelManager channelManager; - - /** - * Instantiates a new VM definition watcher. - * - * @param componentChannel the component channel - * @param channelManager the channel manager - */ - public VmMonitor(Channel componentChannel, - ChannelManager channelManager) { - super(componentChannel, VmDefinition.class, - VmDefinitions.class); - this.channelManager = channelManager; - } - - @Override - protected void prepareMonitoring() throws IOException, ApiException { - client(new K8sClient()); - - // Get all our API versions - var ctx = K8s.context(client(), Crd.GROUP, "", Crd.KIND_VM); - if (ctx.isEmpty()) { - logger.severe(() -> "Cannot get CRD context."); - return; - } - context(ctx.get()); - - // Remove left over resources - purge(); - } - - private void purge() throws ApiException { - // Get existing CRs (VMs) - var known = K8sDynamicStub.list(client(), context(), namespace()) - .stream().map(stub -> stub.name()).collect(Collectors.toSet()); - ListOptions opts = new ListOptions(); - opts.setLabelSelector( - "app.kubernetes.io/managed-by=" + VM_OP_NAME + "," - + "app.kubernetes.io/name=" + APP_NAME); - for (var context : Set.of(K8sV1StatefulSetStub.CONTEXT, - K8sV1ConfigMapStub.CONTEXT)) { - for (var resStub : K8sDynamicStub.list(client(), context, - namespace(), opts)) { - String instance = resStub.model() - .map(m -> m.metadata().getName()).orElse("(unknown)"); - if (!known.contains(instance)) { - resStub.delete(); - } - } - } - } - - @Override - protected void handleChange(K8sClient client, - Watch.Response response) { - var name = response.object.getMetadata().getName(); - - // Process the response data on a VM specific pipeline to - // increase concurrency when e.g. starting many VMs. - var preparing = channelManager.associated(name) - .orElseGet(() -> newEventPipeline()); - preparing.submit("VmChange[" + name + "]", - () -> processChange(client, response, preparing)); - } - - private void processChange(K8sClient client, - Watch.Response response, EventPipeline preparing) { - // Get full definition and associate with channel as backup - var vmDef = response.object; - if (vmDef.data() == null) { - // ADDED event does not provide data, see - // https://github.com/kubernetes-client/java/issues/3215 - vmDef = getModel(client, vmDef); - } - var name = response.object.getMetadata().getName(); - var channel = channelManager.channel(name) - .orElseGet(() -> channelManager.createChannel(name)); - if (vmDef.data() != null) { - // New data, augment and save - addExtraData(vmDef, channel.vmDefinition()); - channel.setVmDefinition(vmDef); - } else { - // Reuse cached (e.g. if deleted) - vmDef = channel.vmDefinition(); - } - if (vmDef == null) { - logger.warning(() -> "Cannot get defintion for " - + response.object.getMetadata()); - return; - } - channelManager.put(name, channel, preparing); - - // Create and fire changed event. Remove channel from channel - // manager on completion. - VmResourceChanged chgEvt - = new VmResourceChanged(ResponseType.valueOf(response.type), vmDef, - channel.setGeneration(response.object.getMetadata() - .getGeneration()), - false); - if (ResponseType.valueOf(response.type) == ResponseType.DELETED) { - chgEvt = Event.onCompletion(chgEvt, - e -> channelManager.remove(e.vmDefinition().name())); - } - channel.fire(chgEvt); - } - - private VmDefinition getModel(K8sClient client, VmDefinition vmDef) { - try { - return VmDefinitionStub.get(client, context(), namespace(), - vmDef.metadata().getName()).model().orElse(null); - } catch (ApiException e) { - return null; - } - } - - private void addExtraData(VmDefinition vmDef, VmDefinition prevState) { - var extra = new VmExtraData(vmDef); - var prevExtra = Optional.ofNullable(prevState).map(VmDefinition::extra); - - // Maintain (or initialize) the resetCount - extra.resetCount(prevExtra.map(VmExtraData::resetCount).orElse(0L)); - - // Maintain node info - prevExtra - .ifPresent(e -> extra.nodeInfo(e.nodeName(), e.nodeAddresses())); - } - - /** - * On pod changed. - * - * @param event the event - * @param channel the channel - */ - @Handler - public void onPodChanged(PodChanged event, VmChannel channel) { - var vmDef = channel.vmDefinition(); - - // Make sure that this is properly sync'd with VM CR changes. - channelManager.associated(vmDef.name()) - .orElseGet(() -> activeEventPipeline()) - .submit("NodeInfo[" + vmDef.name() + "]", - () -> { - updateNodeInfo(event, vmDef); - channel.fire(new VmResourceChanged(ResponseType.MODIFIED, - vmDef, false, true)); - }); - } - - private void updateNodeInfo(PodChanged event, VmDefinition vmDef) { - var extra = vmDef.extra(); - if (event.type() == ResponseType.DELETED) { - // The status of a deleted pod is the status before deletion, - // i.e. the node info is still cached and must be removed. - extra.nodeInfo("", Collections.emptyList()); - return; - } - - // Get current node info from pod - var pod = event.pod(); - var nodeName = Optional - .ofNullable(pod.getSpec().getNodeName()).orElse(""); - logger.finer(() -> "Adding node name " + nodeName - + " to VM info for " + vmDef.name()); - var addrs = new ArrayList(); - Optional.ofNullable(pod.getStatus().getPodIPs()) - .orElse(Collections.emptyList()).stream() - .map(ip -> ip.getIp()).forEach(addrs::add); - logger.finer(() -> "Adding node addresses " + addrs - + " to VM info for " + vmDef.name()); - extra.nodeInfo(nodeName, addrs); - } - - /** - * On modify vm. - * - * @param event the event - * @throws ApiException the api exception - * @throws IOException Signals that an I/O exception has occurred. - */ - @Handler - public void onModifyVm(ModifyVm event, VmChannel channel) - throws ApiException, IOException { - patchVmDef(channel.client(), event.name(), "spec/vm/" + event.path(), - event.value()); - } - - private void patchVmDef(K8sClient client, String name, String path, - Object value) throws ApiException, IOException { - var vmStub = K8sDynamicStub.get(client, - new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM), namespace(), - name); - - // Patch running - String valueAsText = value instanceof String - ? "\"" + value + "\"" - : value.toString(); - var res = vmStub.patch(V1Patch.PATCH_FORMAT_JSON_PATCH, - new V1Patch("[{\"op\": \"replace\", \"path\": \"/" - + path + "\", \"value\": " + valueAsText + "}]"), - client.defaultPatchOptions()); - if (!res.isPresent()) { - logger.warning( - () -> "Cannot patch definition for Vm " + vmStub.name()); - } - } - - /** - * Attempt to Update the assignment information in the status of the - * VM CR. Returns true if successful. The handler does not attempt - * retries, because in case of failure it will be necessary to - * re-evaluate the chosen VM. - * - * @param event the event - * @param channel the channel - * @throws ApiException the api exception - */ - @Handler - public void onUpdatedAssignment(UpdateAssignment event, VmChannel channel) - throws ApiException { - try { - var vmDef = channel.vmDefinition(); - var vmStub = VmDefinitionStub.get(channel.client(), - new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM), - vmDef.namespace(), vmDef.name()); - if (vmStub.updateStatus(vmDef, from -> { - JsonObject status = from.statusJson(); - if (event.toUser() == null) { - ((JsonObject) GsonPtr.to(status).get()) - .remove(Status.ASSIGNMENT); - } else { - var assignment = GsonPtr.to(status).to(Status.ASSIGNMENT); - assignment.set("pool", event.fromPool().name()); - assignment.set("user", event.toUser()); - assignment.set("lastUsed", Instant.now().toString()); - } - return status; - }).isPresent()) { - event.setResult(true); - } - } catch (ApiException e) { - // Log exceptions except for conflict, which can be expected - if (HttpURLConnection.HTTP_CONFLICT != e.getCode()) { - throw e; - } - } - event.setResult(false); - } - -} diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmWatcher.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmWatcher.java new file mode 100644 index 0000000..492ad40 --- /dev/null +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmWatcher.java @@ -0,0 +1,300 @@ +/* + * 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.manager; + +import com.google.gson.reflect.TypeToken; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.apis.ApisApi; +import io.kubernetes.client.openapi.apis.CustomObjectsApi; +import io.kubernetes.client.openapi.models.V1APIGroup; +import io.kubernetes.client.openapi.models.V1APIResource; +import io.kubernetes.client.openapi.models.V1GroupVersionForDiscovery; +import io.kubernetes.client.openapi.models.V1Namespace; +import io.kubernetes.client.openapi.models.V1ObjectMeta; +import io.kubernetes.client.util.Config; +import io.kubernetes.client.util.Watch; +import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi; +import io.kubernetes.client.util.generic.options.ListOptions; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.time.Instant; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; +import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP; +import org.jdrupes.vmoperator.common.K8s; +import static org.jdrupes.vmoperator.manager.Constants.APP_NAME; +import static org.jdrupes.vmoperator.manager.Constants.VM_OP_KIND_VM; +import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME; +import org.jdrupes.vmoperator.manager.events.VmChannel; +import org.jdrupes.vmoperator.manager.events.VmDefChanged; +import org.jdrupes.vmoperator.manager.events.VmDefChanged.Type; +import org.jgrapes.core.Channel; +import org.jgrapes.core.Component; +import org.jgrapes.core.Components; +import org.jgrapes.core.annotation.Handler; +import org.jgrapes.core.events.Start; +import org.jgrapes.core.events.Stop; +import org.jgrapes.util.events.ConfigurationUpdate; + +/** + * Watches for changes of VM definitions. + */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") +public class VmWatcher extends Component { + + private String namespaceToWatch; + private final Map channels = new ConcurrentHashMap<>(); + + /** + * Instantiates a new VM definition watcher. + * + * @param componentChannel the component channel + */ + public VmWatcher(Channel componentChannel) { + super(componentChannel); + } + + /** + * Configure the component. + * + * @param event the event + */ + @Handler + public void onConfigurationUpdate(ConfigurationUpdate event) { + event.structured(Components.manager(parent()).componentPath()) + .ifPresent(c -> { + if (c.containsKey("namespace")) { + namespaceToWatch = (String) c.get("namespace"); + } + }); + } + + /** + * Handle the start event. + * + * @param event the event + * @throws IOException + * @throws ApiException + */ + @Handler(priority = 10) + public void onStart(Start event) throws IOException, ApiException { + // Get namespace + if (namespaceToWatch == null) { + var path = Path + .of("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); + if (Files.isReadable(path)) { + namespaceToWatch = Files.lines(path).findFirst().orElse(null); + } + } + // Availability already checked by Controller.onStart + logger.fine(() -> "Watching namespace \"" + namespaceToWatch + "\"."); + + // Get all our API versions + var client = Config.defaultClient(); + var apis = new ApisApi(client).getAPIVersions(); + var vmOpApiVersions = apis.getGroups().stream() + .filter(g -> g.getName().equals(VM_OP_GROUP)).findFirst() + .map(V1APIGroup::getVersions).stream().flatMap(l -> l.stream()) + .map(V1GroupVersionForDiscovery::getVersion).toList(); + + // Remove left overs + var coa = new CustomObjectsApi(client); + purge(client, coa, vmOpApiVersions); + + // Start a watcher thread for each existing CRD version. + // The watcher will send us an "ADDED" for each existing VM. + for (var version : vmOpApiVersions) { + coa.getAPIResources(VM_OP_GROUP, version) + .getResources().stream() + .filter(r -> VM_OP_KIND_VM.equals(r.getKind())) + .findFirst() + .ifPresent(crd -> watchVmDefs(crd, version)); + } + } + + @SuppressWarnings("PMD.CognitiveComplexity") + private void purge(ApiClient client, CustomObjectsApi coa, + List vmOpApiVersions) throws ApiException { + // Get existing CRs (VMs) + Set known = new HashSet<>(); + for (var version : vmOpApiVersions) { + // Get all known CR instances. + coa.getAPIResources(VM_OP_GROUP, version) + .getResources().stream() + .filter(r -> VM_OP_KIND_VM.equals(r.getKind())) + .findFirst() + .ifPresent(crd -> known.addAll(getKnown(client, crd, version))); + } + + ListOptions opts = new ListOptions(); + opts.setLabelSelector( + "app.kubernetes.io/managed-by=" + VM_OP_NAME + "," + + "app.kubernetes.io/name=" + APP_NAME); + for (String resource : List.of("apps/v1/statefulsets", + "v1/configmaps", "v1/secrets")) { + @SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops", + "PMD.AvoidDuplicateLiterals" }) + var resParts = new LinkedList<>(List.of(resource.split("/"))); + var group = resParts.size() == 3 ? resParts.poll() : ""; + var version = resParts.poll(); + var plural = resParts.poll(); + // Get resources, selected by label + @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") + var api = new DynamicKubernetesApi(group, version, plural, client); + var listObj = api.list(namespaceToWatch, opts).getObject(); + if (listObj == null) { + continue; + } + for (var obj : listObj.getItems()) { + String instance = obj.getMetadata().getLabels() + .get("app.kubernetes.io/instance"); + if (!known.contains(instance)) { + var resName = obj.getMetadata().getName(); + var result = api.delete(namespaceToWatch, resName); + if (!result.isSuccess()) { + logger.warning(() -> "Cannot cleanup resource \"" + + resName + "\": " + result.toString()); + } + } + } + } + } + + private Set getKnown(ApiClient client, V1APIResource crd, + String version) { + Set result = new HashSet<>(); + var api = new DynamicKubernetesApi(VM_OP_GROUP, version, + crd.getName(), client); + for (var item : api.list(namespaceToWatch).getObject().getItems()) { + if (!VM_OP_KIND_VM.equals(item.getKind())) { + continue; + } + result.add(item.getMetadata().getName()); + } + return result; + } + + private void watchVmDefs(V1APIResource crd, String version) { + @SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops", + "PMD.AvoidCatchingThrowable", "PMD.AvoidCatchingGenericException" }) + var watcher = new Thread(() -> { + try { + // Watch sometimes terminates without apparent reason. + while (true) { + Instant startedAt = Instant.now(); + var client = Config.defaultClient(); + var coa = new CustomObjectsApi(client); + var call = coa.listNamespacedCustomObjectCall(VM_OP_GROUP, + version, namespaceToWatch, crd.getName(), null, false, + null, null, null, null, null, null, null, true, null); + try (Watch watch + = Watch.createWatch(client, call, + new TypeToken>() { + }.getType())) { + for (Watch.Response item : watch) { + handleVmDefinitionChange(crd, item); + } + } catch (IOException | ApiException | RuntimeException e) { + logger.log(Level.FINE, e, () -> "Problem watching \"" + + crd.getName() + "\" (will retry): " + + e.getMessage()); + delayRestart(startedAt); + } + } + } catch (Throwable e) { + logger.log(Level.SEVERE, e, () -> "Probem watching: " + + e.getMessage()); + } + fire(new Stop()); + }); + watcher.setDaemon(true); + watcher.start(); + } + + @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"); + } + } + + private void handleVmDefinitionChange(V1APIResource vmsCrd, + Watch.Response item) { + V1ObjectMeta metadata = item.object.getMetadata(); + VmChannel channel = channels.computeIfAbsent(metadata.getName(), + k -> { + try { + return new VmChannel(channel(), newEventPipeline(), + Config.defaultClient()); + } catch (IOException e) { + logger.log(Level.SEVERE, e, () -> "Failed to create client" + + " for handling changes: " + e.getMessage()); + return null; + } + }); + if (channel == null) { + return; + } + + // if (event.type() == Type.DELETED) { + + // Get full definition and associate with channel as backup + var apiVersion = K8s.version(item.object.getApiVersion()); + DynamicKubernetesApi vmCrApi = new DynamicKubernetesApi(VM_OP_GROUP, + apiVersion, vmsCrd.getName(), channel.client()); + var vmDef = K8s.get(vmCrApi, metadata); + vmDef.ifPresent(def -> channel.setVmDefinition(def)); + + // Create and fire event + channel.pipeline().fire(new VmDefChanged(VmDefChanged.Type + .valueOf(item.type), + channel.setGeneration(item.object.getMetadata().getGeneration()), + vmsCrd, vmDef.orElse(channel.vmDefinition())), channel); + } + + /** + * Remove VM channel when VM is deleted. + * + * @param event the event + * @param channel the channel + */ + @Handler(priority = -10_000) + public void onVmDefChanged(VmDefChanged event, VmChannel channel) { + if (event.type() == Type.DELETED) { + channels.remove(event.vmDefinition().getMetadata().getName()); + } + } + +} diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/package-info.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/package-info.java index 1d05ec9..48bc158 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/package-info.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/package-info.java @@ -1,6 +1,6 @@ /* * VM-Operator - * Copyright (C) 2023,2025 Michael N. Lipp + * 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 @@ -57,7 +57,7 @@ * ``` * * Developers may also be interested in the usage of channels - * by the application's components: + * by the application's component: * * ![Main channels](app-channels.svg) * @@ -74,8 +74,6 @@ * * Component NioDispatcher as NioDispatcher <> * [Manager] *-up- [NioDispatcher] - * Component HttpConnector as HttpConnector <> - * [Manager] *-up- [HttpConnector] * Component FileSystemWatcher as FileSystemWatcher <> * [Manager] *-up- [FileSystemWatcher] * Component YamlConfigurationStore as YamlConfigurationStore <> @@ -83,18 +81,8 @@ * [YamlConfigurationStore] *-right[hidden]- [Controller] * * [Manager] *-- [Controller] - * Component VmMonitor as VmMonitor <> - * [Controller] *-- [VmMonitor] - * [VmMonitor] -right[hidden]- [PoolMonitor] - * Component PoolMonitor as PoolMonitor <> - * [Controller] *-- [PoolMonitor] - * Component PodMonitor as PodMonitor <> - * [Controller] *-- [PodMonitor] - * [PodMonitor] -up[hidden]- VmMonitor - * Component DisplaySecretMonitor as DisplaySecretMonitor <> - * [Controller] *-- [DisplaySecretMonitor] - * [DisplaySecretMonitor] -up[hidden]- VmMonitor - * [Controller] *-left- [Reconciler] + * [Controller] *-- [VmWatcher] + * [Controller] *-- [Reconciler] * [Controller] -right[hidden]- [GuiHttpServer] * * [Manager] *-down- [GuiSocketServer:8080] @@ -131,7 +119,6 @@ * [WebConsole] *-- [RoleConfigurator] * [WebConsole] *-- [RoleConletFilter] * [WebConsole] *-left- [LoginConlet] - * [WebConsole] *-right- [OidcClient] * * Component "ComponentCollector\nfor page resources" as cpr <> * [WebConsole] *-- [cpr] @@ -153,42 +140,27 @@ * mgr .left. [FileSystemWatcher] * mgr .right. [YamlConfigurationStore] * mgr .. [Controller] - * mgr .up. [Manager] * mgr .up. [VmWatcher] * mgr .. [Reconciler] * * () "guiTransport" as hT * hT .up. [GuiSocketServer:8080] * hT .down. [GuiHttpServer] - * hT .right[hidden]. [HttpConnector] * * [YamlConfigurationStore] -right[hidden]- hT * * () "guiHttp" as http * http .up. [GuiHttpServer] - * http .up. [HttpConnector] - * note top of [HttpConnector]: transport layer com-\nponents omitted * - * [PreferencesStore] .. http - * [OidcClient] .up. http - * [LanguageSelector] .left. http + * [PreferencesStore] .right. http * [InMemorySessionManager] .up. http + * [LanguageSelector] .up. http * * package "Conceptual WebConsole" { - * [ConsoleWeblet] .right. http + * [ConsoleWeblet] .left. http * [ConsoleWeblet] *-down- [WebConsole] * } * - * [Controller] .down[hidden]. [ConsoleWeblet] - * - * () "console" as console - * console .. WebConsole - * - * [OidcClient] .. console - * [LoginConlet] .right. console - * - * note right of console: More conlets\nconnect here - * * @enduml */ package org.jdrupes.vmoperator.manager; diff --git a/org.jdrupes.vmoperator.manager/test-resources/basic-vm.yaml b/org.jdrupes.vmoperator.manager/test-resources/basic-vm.yaml deleted file mode 100644 index 36054a2..0000000 --- a/org.jdrupes.vmoperator.manager/test-resources/basic-vm.yaml +++ /dev/null @@ -1,64 +0,0 @@ -apiVersion: "vmoperator.jdrupes.org/v1" -kind: VirtualMachine -metadata: - namespace: vmop-test - name: test-vm -spec: - image: - repository: docker-registry.lan.mnl.de - path: vmoperator/this.will.never.start - version: 0.0.0 - - cloudInit: - metaData: {} - - vm: - # state: Running - maximumRam: 4Gi - currentRam: 2Gi - maximumCpus: 4 - currentCpus: 2 - powerdownTimeout: 1 - - networks: - - user: {} - disks: - - cdrom: - image: https://test.com/test.iso - bootindex: 0 - - cdrom: - image: "image.iso" - - volumeClaimTemplate: - metadata: - name: system - annotations: - use_as: system-disk - spec: - storageClassName: local-path - resources: - requests: - storage: 1Gi - - volumeClaimTemplate: - spec: - storageClassName: local-path - resources: - requests: - storage: 1Gi - - display: - outputs: 2 - spice: - port: 5812 - usbRedirects: 2 - - resources: - requests: - cpu: 1 - memory: 2Gi - - loadBalancerService: - labels: - label2: replaced - label3: added - annotations: - anno1: added diff --git a/org.jdrupes.vmoperator.manager/test-resources/kustomization.yaml b/org.jdrupes.vmoperator.manager/test-resources/kustomization.yaml deleted file mode 100644 index 3a8451e..0000000 --- a/org.jdrupes.vmoperator.manager/test-resources/kustomization.yaml +++ /dev/null @@ -1,111 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: -- ../../deploy - -namespace: vmop-test - -patches: -- patch: |- - kind: PersistentVolumeClaim - apiVersion: v1 - metadata: - name: vmop-image-repository - spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 10Gi - storageClassName: local-path - -- patch: |- - kind: ConfigMap - apiVersion: v1 - metadata: - name: vm-operator - data: - # Keep in sync with config.yaml - config.yaml: | - "/Manager": - # clusterName: "test" - "/Controller": - "/Reconciler": - runnerData: - storageClassName: null - loadBalancerService: - labels: - label1: label1 - label2: toBeReplaced - annotations: - metallb.universe.tf/loadBalancerIPs: 192.168.168.1 - metallb.universe.tf/ip-allocated-from-pool: single-common - metallb.universe.tf/allow-shared-ip: single-common - "/GuiSocketServer": - port: 8888 - "/GuiHttpServer": - # This configures the GUI - "/ConsoleWeblet": - "/WebConsole": - "/LoginConlet": - users: - - name: admin - fullName: Administrator - password: "$2b$05$NiBd74ZGdplLC63ePZf1f.UtjMKkbQ23cQoO2OKOFalDBHWAOy21." - - name: test1 - fullName: Test Account - password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2" - - name: test2 - fullName: Test Account - password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2" - - name: test3 - fullName: Test Account - password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2" - "/RoleConfigurator": - rolesByUser: - # User admin has role admin - admin: - - admin - test1: - - user - test2: - - user - test3: - - user - # All users have role other - "*": - - other - replace: false - "/RoleConletFilter": - conletTypesByRole: - # Admins can use all conlets - admin: - - "*" - user: - - org.jdrupes.vmoperator.vmviewer.VmViewer - # Others cannot use any conlet (except login conlet to log out) - other: - - org.jgrapes.webconlet.locallogin.LoginConlet - "/ComponentCollector": - "/VmAccess": - displayResource: - preferredIpVersion: ipv4 - syncPreviewsFor: - - role: user -- target: - group: apps - version: v1 - kind: Deployment - name: vm-operator - patch: |- - - op: replace - path: /spec/template/spec/containers/0/image - value: docker-registry.lan.mnl.de/vmoperator/org.jdrupes.vmoperator.manager:test - - op: replace - path: /spec/template/spec/containers/0/imagePullPolicy - value: Always - - op: replace - path: /spec/replicas - value: 0 - \ No newline at end of file diff --git a/org.jdrupes.vmoperator.manager/test-resources/unittest-vm.yaml b/org.jdrupes.vmoperator.manager/test-resources/unittest-vm.yaml new file mode 100644 index 0000000..0d395bd --- /dev/null +++ b/org.jdrupes.vmoperator.manager/test-resources/unittest-vm.yaml @@ -0,0 +1,35 @@ +apiVersion: "vmoperator.jdrupes.org/v1" +kind: VirtualMachine +metadata: + namespace: vmop-dev + name: unittest-vm +spec: + resources: + requests: + cpu: 1 + memory: 2Gi + + loadBalancerService: + labels: + test2: null + test3: added + + vm: + # state: Running + maximumRam: 4Gi + currentRam: 2Gi + maximumCpus: 4 + currentCpus: 2 + powerdownTimeout: 1 + + networks: + - user: {} + disks: + - cdrom: + # image: "" + image: https://download.fedoraproject.org/pub/fedora/linux/releases/38/Workstation/x86_64/iso/Fedora-Workstation-Live-x86_64-38-1.6.iso + # image: "Fedora-Workstation-Live-x86_64-38-1.6.iso" + + display: + spice: + port: 5812 diff --git a/org.jdrupes.vmoperator.manager/test/org/jdrupes/vmoperator/manager/BasicTests.java b/org.jdrupes.vmoperator.manager/test/org/jdrupes/vmoperator/manager/BasicTests.java index d600d3c..26eb387 100644 --- a/org.jdrupes.vmoperator.manager/test/org/jdrupes/vmoperator/manager/BasicTests.java +++ b/org.jdrupes.vmoperator.manager/test/org/jdrupes/vmoperator/manager/BasicTests.java @@ -1,32 +1,13 @@ package org.jdrupes.vmoperator.manager; -import io.kubernetes.client.Discovery.APIResource; -import io.kubernetes.client.custom.Quantity; -import io.kubernetes.client.custom.V1Patch; -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.util.generic.options.ListOptions; -import io.kubernetes.client.util.generic.options.PatchOptions; -import java.io.FileReader; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientBuilder; +import io.fabric8.kubernetes.client.dsl.base.ResourceDefinitionContext; import java.io.IOException; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.List; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Map; -import org.jdrupes.vmoperator.common.Constants; -import static org.jdrupes.vmoperator.common.Constants.APP_NAME; -import org.jdrupes.vmoperator.common.Constants.Crd; -import org.jdrupes.vmoperator.common.Constants.DisplaySecret; -import static org.jdrupes.vmoperator.common.Constants.VM_OP_NAME; -import org.jdrupes.vmoperator.common.K8s; -import org.jdrupes.vmoperator.common.K8sClient; -import org.jdrupes.vmoperator.common.K8sDynamicStub; -import org.jdrupes.vmoperator.common.K8sV1ConfigMapStub; -import org.jdrupes.vmoperator.common.K8sV1DeploymentStub; -import org.jdrupes.vmoperator.common.K8sV1PodStub; -import org.jdrupes.vmoperator.common.K8sV1PvcStub; -import org.jdrupes.vmoperator.common.K8sV1SecretStub; -import org.jdrupes.vmoperator.common.K8sV1ServiceStub; -import org.jdrupes.vmoperator.util.DataPath; import org.junit.jupiter.api.AfterAll; import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.BeforeAll; @@ -37,12 +18,8 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; class BasicTests { - private static K8sClient client; - private static APIResource vmsContext; - private static K8sV1DeploymentStub mgrDeployment; - private static K8sDynamicStub vmStub; - private static final String VM_NAME = "test-vm"; - private static final Object EXISTS = new Object(); + private static KubernetesClient client; + private static ResourceDefinitionContext vmsContext; @BeforeAll static void setUpBeforeClass() throws Exception { @@ -50,44 +27,29 @@ class BasicTests { assertNotNull(testCluster); // Get client - client = new K8sClient(); - - // Update manager pod by scaling deployment - mgrDeployment - = K8sV1DeploymentStub.get(client, "vmop-test", "vm-operator"); - mgrDeployment.scale(0); - mgrDeployment.scale(1); - waitForManager(); + client = new KubernetesClientBuilder() + .withConfig(Config.autoConfigure(testCluster)).build(); // Context for working with our CR - var apiRes = K8s.context(client, Crd.GROUP, null, Crd.KIND_VM); - assertTrue(apiRes.isPresent()); - vmsContext = apiRes.get(); + vmsContext = new ResourceDefinitionContext.Builder() + .withGroup("vmoperator.jdrupes.org").withKind("VirtualMachine") + .withPlural("vms").withNamespaced(true).withVersion("v1").build(); - // Cleanup existing VM - K8sDynamicStub.get(client, vmsContext, "vmop-test", VM_NAME) - .delete(); - ListOptions listOpts = new ListOptions(); - listOpts.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + "," - + "app.kubernetes.io/instance=" + VM_NAME + "," - + "app.kubernetes.io/component=" + DisplaySecret.NAME); - var secrets = K8sV1SecretStub.list(client, "vmop-test", listOpts); - for (var secret : secrets) { - secret.delete(); - } - deletePvcs(); + // Cleanup + var resourcesInNamespace = client.genericKubernetesResources(vmsContext) + .inNamespace("vmop-dev"); + resourcesInNamespace.withName("unittest-vm").delete(); - // Load from Yaml - var rdr = new FileReader("test-resources/basic-vm.yaml"); - vmStub = K8sDynamicStub.createFromYaml(client, vmsContext, rdr); - assertTrue(vmStub.model().isPresent()); - } + // Update manager pod by scaling deployment + client.apps().deployments().inNamespace("vmop-dev") + .withName("vm-operator").scale(0); + client.apps().deployments().inNamespace("vmop-dev") + .withName("vm-operator").scale(1); - private static void waitForManager() - throws ApiException, InterruptedException { // Wait until available for (int i = 0; i < 10; i++) { - if (mgrDeployment.model().get().getStatus().getConditions() + if (client.apps().deployments().inNamespace("vmop-dev") + .withName("vm-operator").get().getStatus().getConditions() .stream().filter(c -> "Available".equals(c.getType())).findAny() .isPresent()) { return; @@ -97,245 +59,63 @@ class BasicTests { fail("vm-operator not deployed."); } - private static void deletePvcs() throws ApiException { - ListOptions listOpts = new ListOptions(); - listOpts.setLabelSelector( - "app.kubernetes.io/managed-by=" + VM_OP_NAME + "," - + "app.kubernetes.io/name=" + APP_NAME + "," - + "app.kubernetes.io/instance=" + VM_NAME); - var knownPvcs = K8sV1PvcStub.list(client, "vmop-test", listOpts); - for (var pvc : knownPvcs) { - pvc.delete(); - } - } - @AfterAll static void tearDownAfterClass() throws Exception { - // Cleanup - K8sDynamicStub.get(client, vmsContext, "vmop-test", VM_NAME) - .delete(); - deletePvcs(); - // Bring down manager - mgrDeployment.scale(0); + client.apps().deployments().inNamespace("vmop-dev") + .withName("vm-operator").scale(0); + client.close(); } @Test - void testConfigMap() - throws IOException, InterruptedException, ApiException { - K8sV1ConfigMapStub stub - = K8sV1ConfigMapStub.get(client, "vmop-test", VM_NAME); - for (int i = 0; i < 10; i++) { - if (stub.model().isPresent()) { - break; - } - Thread.sleep(1000); - } + void test() throws IOException, InterruptedException { + // Load from Yaml + var vm = client.genericKubernetesResources(vmsContext) + .load(Files + .newInputStream(Path.of("test-resources/unittest-vm.yaml"))); + // Create Custom Resource + vm.create(); + + // Wait for created resources + assertTrue(waitForConfigMap()); + assertTrue(waitForStatefulSet()); + // Check config map - var config = stub.model().get(); - Map, Object> toCheck = Map.of( - List.of("namespace"), "vmop-test", - List.of("name"), VM_NAME, - List.of("labels", "app.kubernetes.io/name"), Constants.APP_NAME, - List.of("labels", "app.kubernetes.io/instance"), VM_NAME, - List.of("labels", "app.kubernetes.io/managed-by"), VM_OP_NAME, - List.of("annotations", "vmoperator.jdrupes.org/version"), EXISTS, - List.of("ownerReferences", 0, "apiVersion"), - vmsContext.getGroup() + "/" + vmsContext.getVersions().get(0), - List.of("ownerReferences", 0, "kind"), Crd.KIND_VM, - List.of("ownerReferences", 0, "name"), VM_NAME, - List.of("ownerReferences", 0, "uid"), EXISTS); - checkProps(config.getMetadata(), toCheck); + var config = client.configMaps().inNamespace("vmop-dev") + .withName("unittest-vm").get(); + var yaml = new Yaml(new SafeConstructor(new LoaderOptions())) + .load((String) config.getData().get("config.yaml")); + @SuppressWarnings("unchecked") + var currentRam = ((Map>>) yaml) + .get("/Runner").get("vm").get("maximumRam"); + assertEquals("4 GiB", currentRam); - toCheck = new LinkedHashMap<>(); - toCheck.put(List.of("/Runner", "guestShutdownStops"), false); - toCheck.put(List.of("/Runner", "cloudInit", "metaData", "instance-id"), - EXISTS); - toCheck.put( - List.of("/Runner", "cloudInit", "metaData", "local-hostname"), - VM_NAME); - toCheck.put(List.of("/Runner", "cloudInit", "userData"), Map.of()); - toCheck.put(List.of("/Runner", "vm", "maximumRam"), "4 GiB"); - toCheck.put(List.of("/Runner", "vm", "currentRam"), "2 GiB"); - toCheck.put(List.of("/Runner", "vm", "maximumCpus"), 4); - toCheck.put(List.of("/Runner", "vm", "currentCpus"), 2); - toCheck.put(List.of("/Runner", "vm", "powerdownTimeout"), 1); - toCheck.put(List.of("/Runner", "vm", "network", 0, "type"), "user"); - toCheck.put(List.of("/Runner", "vm", "drives", 0, "type"), "ide-cd"); - toCheck.put(List.of("/Runner", "vm", "drives", 0, "file"), - "https://test.com/test.iso"); - toCheck.put(List.of("/Runner", "vm", "drives", 0, "bootindex"), 0); - toCheck.put(List.of("/Runner", "vm", "drives", 1, "type"), "ide-cd"); - toCheck.put(List.of("/Runner", "vm", "drives", 1, "file"), - "/var/local/vmop-image-repository/image.iso"); - toCheck.put(List.of("/Runner", "vm", "drives", 2, "type"), "raw"); - toCheck.put(List.of("/Runner", "vm", "drives", 2, "resource"), - "/dev/system-disk"); - toCheck.put(List.of("/Runner", "vm", "drives", 3, "type"), "raw"); - toCheck.put(List.of("/Runner", "vm", "drives", 3, "resource"), - "/dev/disk-1"); - toCheck.put(List.of("/Runner", "vm", "display", "outputs"), 2); - toCheck.put(List.of("/Runner", "vm", "display", "spice", "port"), 5812); - toCheck.put( - List.of("/Runner", "vm", "display", "spice", "usbRedirects"), 2); - var cm = new Yaml(new SafeConstructor(new LoaderOptions())) - .load(config.getData().get("config.yaml")); - checkProps(cm, toCheck); + // Cleanup + var resourcesInNamespace = client.genericKubernetesResources(vmsContext) + .inNamespace("vmop-dev"); + resourcesInNamespace.withName("unittest-vm").delete(); } - @Test - void testDisplaySecret() throws ApiException, InterruptedException { - ListOptions listOpts = new ListOptions(); - listOpts.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + "," - + "app.kubernetes.io/instance=" + VM_NAME + "," - + "app.kubernetes.io/component=" + DisplaySecret.NAME); - Collection secrets = null; + private boolean waitForConfigMap() throws InterruptedException { for (int i = 0; i < 10; i++) { - secrets = K8sV1SecretStub.list(client, "vmop-test", listOpts); - if (secrets.size() > 0) { - break; + if (client.configMaps().inNamespace("vmop-dev") + .withName("unittest-vm").get() != null) { + return true; } Thread.sleep(1000); } - assertEquals(1, secrets.size()); - var secretData = secrets.iterator().next().model().get().getData(); - checkProps(secretData, Map.of( - List.of("display-password"), EXISTS)); - assertEquals("now", new String(secretData.get("password-expiry"))); + return false; } - @Test - void testRunnerPvc() throws ApiException, InterruptedException { - var stub - = K8sV1PvcStub.get(client, "vmop-test", VM_NAME + "-runner-data"); + private boolean waitForStatefulSet() throws InterruptedException { for (int i = 0; i < 10; i++) { - if (stub.model().isPresent()) { - break; + if (client.apps().statefulSets().inNamespace("vmop-dev") + .withName("unittest-vm").get() != null) { + return true; } Thread.sleep(1000); } - var pvc = stub.model().get(); - checkProps(pvc.getMetadata(), Map.of( - List.of("labels", "app.kubernetes.io/name"), Constants.APP_NAME, - List.of("labels", "app.kubernetes.io/instance"), VM_NAME, - List.of("labels", "app.kubernetes.io/managed-by"), VM_OP_NAME)); - checkProps(pvc.getSpec(), Map.of( - List.of("resources", "requests", "storage"), - Quantity.fromString("1Mi"))); - } - - @Test - void testSystemDiskPvc() throws ApiException, InterruptedException { - var stub - = K8sV1PvcStub.get(client, "vmop-test", VM_NAME + "-system-disk"); - for (int i = 0; i < 10; i++) { - if (stub.model().isPresent()) { - break; - } - Thread.sleep(1000); - } - var pvc = stub.model().get(); - checkProps(pvc.getMetadata(), Map.of( - List.of("labels", "app.kubernetes.io/name"), Constants.APP_NAME, - List.of("labels", "app.kubernetes.io/instance"), VM_NAME, - List.of("labels", "app.kubernetes.io/managed-by"), VM_OP_NAME, - List.of("annotations", "use_as"), "system-disk")); - checkProps(pvc.getSpec(), Map.of( - List.of("resources", "requests", "storage"), - Quantity.fromString("1Gi"))); - } - - @Test - void testDisk1Pvc() throws ApiException, InterruptedException { - var stub - = K8sV1PvcStub.get(client, "vmop-test", VM_NAME + "-disk-1"); - for (int i = 0; i < 10; i++) { - if (stub.model().isPresent()) { - break; - } - Thread.sleep(1000); - } - var pvc = stub.model().get(); - checkProps(pvc.getMetadata(), Map.of( - List.of("labels", "app.kubernetes.io/name"), Constants.APP_NAME, - List.of("labels", "app.kubernetes.io/instance"), VM_NAME, - List.of("labels", "app.kubernetes.io/managed-by"), VM_OP_NAME)); - checkProps(pvc.getSpec(), Map.of( - List.of("resources", "requests", "storage"), - Quantity.fromString("1Gi"))); - } - - @Test - void testPod() throws ApiException, InterruptedException { - PatchOptions opts = new PatchOptions(); - opts.setForce(true); - opts.setFieldManager("kubernetes-java-kubectl-apply"); - assertTrue(vmStub.patch(V1Patch.PATCH_FORMAT_JSON_PATCH, - new V1Patch("[{\"op\": \"replace\", \"path\": \"/spec/vm/state" - + "\", \"value\": \"Running\"}]"), - client.defaultPatchOptions()).isPresent()); - var stub = K8sV1PodStub.get(client, "vmop-test", VM_NAME); - for (int i = 0; i < 20; i++) { - if (stub.model().isPresent()) { - break; - } - Thread.sleep(1000); - } - var pod = stub.model().get(); - checkProps(pod.getMetadata(), Map.of( - List.of("labels", "app.kubernetes.io/name"), APP_NAME, - List.of("labels", "app.kubernetes.io/instance"), VM_NAME, - List.of("labels", "app.kubernetes.io/component"), APP_NAME, - List.of("labels", "app.kubernetes.io/managed-by"), VM_OP_NAME, - List.of("annotations", "vmrunner.jdrupes.org/cmVersion"), EXISTS, - List.of("annotations", "vmoperator.jdrupes.org/version"), EXISTS, - List.of("ownerReferences", 0, "apiVersion"), - vmsContext.getGroup() + "/" + vmsContext.getVersions().get(0), - List.of("ownerReferences", 0, "kind"), Crd.KIND_VM, - List.of("ownerReferences", 0, "name"), VM_NAME, - List.of("ownerReferences", 0, "uid"), EXISTS)); - checkProps(pod.getSpec(), Map.of( - List.of("containers", 0, "image"), EXISTS, - List.of("containers", 0, "name"), VM_NAME, - List.of("containers", 0, "resources", "requests", "cpu"), - Quantity.fromString("1"))); - } - - @Test - public void testLoadBalancer() throws ApiException, InterruptedException { - var stub = K8sV1ServiceStub.get(client, "vmop-test", VM_NAME); - for (int i = 0; i < 10; i++) { - if (stub.model().isPresent()) { - break; - } - Thread.sleep(1000); - } - var svc = stub.model().get(); - checkProps(svc.getMetadata(), Map.of( - List.of("labels", "app.kubernetes.io/name"), APP_NAME, - List.of("labels", "app.kubernetes.io/instance"), VM_NAME, - List.of("labels", "app.kubernetes.io/managed-by"), VM_OP_NAME, - List.of("labels", "label1"), "label1", - List.of("labels", "label2"), "replaced", - List.of("labels", "label3"), "added", - List.of("annotations", "metallb.universe.tf/loadBalancerIPs"), - "192.168.168.1", - List.of("annotations", "anno1"), "added")); - } - - private void checkProps(Object obj, - Map, Object> toCheck) { - for (var entry : toCheck.entrySet()) { - var prop = DataPath.get(obj, entry.getKey().toArray()); - assertTrue(prop.isPresent(), () -> "Property " + entry.getKey() - + " not found in " + obj); - - // Check for existance only - if (entry.getValue() == EXISTS) { - continue; - } - assertEquals(entry.getValue(), prop.get()); - } + return false; } } diff --git a/org.jdrupes.vmoperator.runner.qemu/.eclipse-pmd b/org.jdrupes.vmoperator.runner.qemu/.eclipse-pmd index 5d69caa..8b394f8 100644 --- a/org.jdrupes.vmoperator.runner.qemu/.eclipse-pmd +++ b/org.jdrupes.vmoperator.runner.qemu/.eclipse-pmd @@ -2,6 +2,6 @@ - + diff --git a/org.jdrupes.vmoperator.runner.qemu/build.gradle b/org.jdrupes.vmoperator.runner.qemu/build.gradle index 695c815..ec7de7f 100644 --- a/org.jdrupes.vmoperator.runner.qemu/build.gradle +++ b/org.jdrupes.vmoperator.runner.qemu/build.gradle @@ -9,14 +9,14 @@ plugins { } dependencies { - implementation 'org.jgrapes:org.jgrapes.core:[1.22.1,2)' - implementation 'org.jgrapes:org.jgrapes.util:[1.38.1,2)' - implementation 'org.jgrapes:org.jgrapes.io:[2.12.1,3)' - implementation 'org.jgrapes:org.jgrapes.http:[3.5.0,4)' + implementation 'org.jgrapes:org.jgrapes.core:[1.19.0,2)' + implementation 'org.jgrapes:org.jgrapes.io:[2.7.0,3)' + implementation 'org.jgrapes:org.jgrapes.http:[3.1.0,4)' + implementation 'org.jgrapes:org.jgrapes.util:[1.31.0,2)' implementation project(':org.jdrupes.vmoperator.common') implementation 'commons-cli:commons-cli:1.5.0' - implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:[2.16.1]' + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:[2.15.1,3]' runtimeOnly 'org.slf4j:slf4j-jdk14:[2.0.7,3)' } @@ -31,99 +31,102 @@ application { mainClass = 'org.jdrupes.vmoperator.runner.qemu.Runner' } -project.ext.gitBranch = grgit.branch.current.name.replace('/', '-') -def registry = "${project.rootProject.properties['docker.registry']}" -def rootVersion = rootProject.version - -task buildImageArch(type: Exec) { +task buildArchImage(type: Exec) { dependsOn installDist inputs.files 'src/org/jdrupes/vmoperator/runner/qemu/Containerfile.arch' - commandLine 'podman', 'build', '--pull', - '-t', "${project.name}-arch:${project.gitBranch}",\ + commandLine 'podman', 'build', '-t', "${project.name}-arch:${project.version}",\ '-f', 'src/org/jdrupes/vmoperator/runner/qemu/Containerfile.arch', '.' } -task pushImageArch(type: Exec) { - dependsOn buildImageArch +task tagLatestArchImage(type: Exec) { + dependsOn buildArchImage + + enabled = !project.version.contains("SNAPSHOT") + && !project.version.contains("alpha") \ + && !project.version.contains("beta") \ + + commandLine 'podman', 'tag', "${project.name}-arch:${project.version}",\ + "${project.name}-arch:latest" +} + +task buildLatestArchImage { + dependsOn buildArchImage + dependsOn tagLatestArchImage +} + +task pushArchImage(type: Exec) { + dependsOn buildArchImage commandLine 'podman', 'push', '--tls-verify=false', \ - "${project.name}-arch:${project.gitBranch}", \ - "${registry}/${project.name}-arch:${project.gitBranch}" + "localhost/${project.name}-arch:${project.version}", \ + "${project.rootProject.properties['docker.registry']}" \ + + "/${project.name}-arch:${project.version}" } -task tagWithVersionArch(type: Exec) { - dependsOn pushImageArch +task pushArchLatestImage(type: Exec) { + dependsOn buildLatestArchImage - enabled = !rootVersion.contains("SNAPSHOT") - - commandLine 'podman', 'push', \ - "${project.name}-arch:${project.gitBranch}",\ - "${registry}/${project.name}-arch:${project.version}" + enabled = !project.version.contains("SNAPSHOT") + && !project.version.contains("alpha") \ + && !project.version.contains("beta") \ + + commandLine 'podman', 'push', '--tls-verify=false', \ + "localhost/${project.name}-arch:${project.version}", \ + "${project.rootProject.properties['docker.registry']}" \ + + "/${project.name}-arch:latest" } -task tagAsLatestArch(type: Exec) { - dependsOn tagWithVersionArch - - enabled = !rootVersion.contains("SNAPSHOT") - && !rootVersion.contains("alpha") \ - && !rootVersion.contains("beta") \ - || project.rootProject.properties['docker.testRegistry'] \ - && project.rootProject.properties['docker.registry'] \ - == project.rootProject.properties['docker.testRegistry'] - - commandLine 'podman', 'push', \ - "${project.name}-arch:${project.gitBranch}",\ - "${registry}/${project.name}-arch:latest" -} - -task buildImageAlpine(type: Exec) { +task buildAlpineImage(type: Exec) { dependsOn installDist inputs.files 'src/org/jdrupes/vmoperator/runner/qemu/Containerfile.alpine' - commandLine 'podman', 'build', '--pull', - '-t', "${project.name}-alpine:${project.gitBranch}",\ + commandLine 'podman', 'build', '-t', "${project.name}-alpine:${project.version}",\ '-f', 'src/org/jdrupes/vmoperator/runner/qemu/Containerfile.alpine', '.' } -task pushImageAlpine(type: Exec) { - dependsOn buildImageAlpine +task tagLatestAlpineImage(type: Exec) { + dependsOn buildAlpineImage + + enabled = !project.version.contains("SNAPSHOT") + && !project.version.contains("alpha") \ + && !project.version.contains("beta") \ + + commandLine 'podman', 'tag', "${project.name}-alpine:${project.version}",\ + "${project.name}-alpine:latest" +} + +task buildLatestAlpineImage { + dependsOn buildAlpineImage + dependsOn tagLatestAlpineImage +} + +task pushAlpineImage(type: Exec) { + dependsOn buildAlpineImage commandLine 'podman', 'push', '--tls-verify=false', \ - "localhost/${project.name}-alpine:${project.gitBranch}", \ - "${registry}/${project.name}-alpine:${project.gitBranch}" + "localhost/${project.name}-alpine:${project.version}", \ + "${project.rootProject.properties['docker.registry']}" \ + + "/${project.name}-alpine:${project.version}" } -task tagWithVersionAlpine(type: Exec) { - dependsOn pushImageAlpine +task pushAlpineLatestImage(type: Exec) { + dependsOn buildLatestAlpineImage - enabled = !rootVersion.contains("SNAPSHOT") - - commandLine 'podman', 'push', \ - "${project.name}-alpine:${project.gitBranch}",\ - "${registry}/${project.name}-alpine:${project.version}" + enabled = !project.version.contains("SNAPSHOT") + && !project.version.contains("alpha") \ + && !project.version.contains("beta") \ + + commandLine 'podman', 'push', '--tls-verify=false', \ + "localhost/${project.name}-alpine:${project.version}", \ + "${project.rootProject.properties['docker.registry']}" \ + + "/${project.name}-alpine:latest" } -task tagAsLatestAlpine(type: Exec) { - dependsOn tagWithVersionAlpine - - enabled = !rootVersion.contains("SNAPSHOT") - && !rootVersion.contains("alpha") \ - && !rootVersion.contains("beta") \ - || project.rootProject.properties['docker.testRegistry'] \ - && project.rootProject.properties['docker.registry'] \ - == project.rootProject.properties['docker.testRegistry'] - - commandLine 'podman', 'push', \ - "${project.name}-alpine:${project.gitBranch}",\ - "${registry}/${project.name}-alpine:latest" +task pushImages { + dependsOn pushArchImage + dependsOn pushArchLatestImage + dependsOn pushAlpineImage + dependsOn pushAlpineLatestImage } -task publishImage { - dependsOn pushImageArch - dependsOn tagWithVersionArch - dependsOn tagAsLatestArch - dependsOn pushImageAlpine - dependsOn tagWithVersionAlpine - dependsOn tagAsLatestAlpine -} diff --git a/org.jdrupes.vmoperator.runner.qemu/config-sample.yaml b/org.jdrupes.vmoperator.runner.qemu/config-sample.yaml index e23a2ec..461e79b 100644 --- a/org.jdrupes.vmoperator.runner.qemu/config-sample.yaml +++ b/org.jdrupes.vmoperator.runner.qemu/config-sample.yaml @@ -27,31 +27,6 @@ # be set when starting the runner during development e.g. from the IDE. # "namespace": ... - # Defines data for generating a cloud-init ISO image that is - # attached to the VM. - # "cloudInit": - # "metaData": - # ... - # "userData": - # ... - # "networkConfig": - # ... - # - # If .metaData.instance-id is missing, an id is generated from the - # config file's modification timestamp. .userData and .networkConfig - # are optional. - - # Whether a guest initiated shutdown event patches the state - # property in the CRD. - # "guestShutdownStops": - # false - - # When incremented, the VM is reset. The value has no default value, - # i.e. if you start the VM without a value for this property, and - # decide to trigger a reset later, you have to first set the value - # and then inrement it. - # "resetCounter": 1 - # Define the VM (required) "vm": # The VM's name (required) diff --git a/org.jdrupes.vmoperator.runner.qemu/display-password b/org.jdrupes.vmoperator.runner.qemu/display-password deleted file mode 100644 index 97c1abb..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/display-password +++ /dev/null @@ -1 +0,0 @@ -test-vm \ No newline at end of file diff --git a/org.jdrupes.vmoperator.runner.qemu/password-expiry b/org.jdrupes.vmoperator.runner.qemu/password-expiry deleted file mode 100644 index 8a606d5..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/password-expiry +++ /dev/null @@ -1 +0,0 @@ -+1800 \ No newline at end of file diff --git a/org.jdrupes.vmoperator.runner.qemu/resources/org/jdrupes/vmoperator/runner/qemu/defaults.yaml b/org.jdrupes.vmoperator.runner.qemu/resources/org/jdrupes/vmoperator/runner/qemu/defaults.yaml index 600f0ad..9aadbf6 100644 --- a/org.jdrupes.vmoperator.runner.qemu/resources/org/jdrupes/vmoperator/runner/qemu/defaults.yaml +++ b/org.jdrupes.vmoperator.runner.qemu/resources/org/jdrupes/vmoperator/runner/qemu/defaults.yaml @@ -8,16 +8,10 @@ - "/usr/share/edk2/ovmf/OVMF_CODE.fd" - "/usr/share/edk2/x64/OVMF_CODE.fd" - "/usr/share/OVMF/OVMF_CODE.fd" - # Use 4M version as fallback (if smaller version not available) - - "/usr/share/edk2/ovmf-4m/OVMF_CODE.fd" - - "/usr/share/edk2/x64/OVMF_CODE.4m.fd" "vars": - "/usr/share/edk2/ovmf/OVMF_VARS.fd" - "/usr/share/edk2/x64/OVMF_VARS.fd" - "/usr/share/OVMF/OVMF_VARS.fd" - # Use 4M version as fallback (if smaller version not available) - - "/usr/share/edk2/ovmf-4m/OVMF_VARS.fd" - - "/usr/share/edk2/x64/OVMF_VARS.4m.fd" "uefi-4m": "rom": - "/usr/share/edk2/ovmf-4m/OVMF_CODE.fd" diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/AgentConnector.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/AgentConnector.java deleted file mode 100644 index 6303794..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/AgentConnector.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * 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.runner.qemu; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.List; -import org.jdrupes.vmoperator.runner.qemu.events.VserportChangeEvent; -import org.jgrapes.core.Channel; -import org.jgrapes.core.annotation.Handler; - -/** - * A component that handles the communication with an agent - * running in the VM. - * - * If the log level for this class is set to fine, the messages - * exchanged on the socket are logged. - */ -public abstract class AgentConnector extends QemuConnector { - - protected String channelId; - - /** - * Instantiates a new agent connector. - * - * @param componentChannel the component channel - * @throws IOException Signals that an I/O exception has occurred. - */ - public AgentConnector(Channel componentChannel) throws IOException { - super(componentChannel); - } - - /** - * Extracts the channel id and the socket path from the QEMU - * command line. - * - * @param command the command - * @param chardev the chardev - */ - @SuppressWarnings("PMD.CognitiveComplexity") - protected void configureConnection(List command, String chardev) { - Path socketPath = null; - for (var arg : command) { - if (arg.startsWith("virtserialport,") - && arg.contains("chardev=" + chardev)) { - for (var prop : arg.split(",")) { - if (prop.startsWith("id=")) { - channelId = prop.substring(3); - } - } - } - if (arg.startsWith("socket,") - && arg.contains("id=" + chardev)) { - for (var prop : arg.split(",")) { - if (prop.startsWith("path=")) { - socketPath = Path.of(prop.substring(5)); - } - } - } - } - if (channelId == null || socketPath == null) { - logger.warning(() -> "Definition of chardev " + chardev - + " missing in runner template."); - return; - } - logger.fine(() -> getClass().getSimpleName() + " configured with" - + " channelId=" + channelId); - super.configure(socketPath); - } - - /** - * When the virtual serial port with the configured channel id has - * been opened call {@link #agentConnected()}. - * - * @param event the event - */ - @Handler - public void onVserportChanged(VserportChangeEvent event) { - if (event.id().equals(channelId)) { - if (event.isOpen()) { - agentConnected(); - } else { - agentDisconnected(); - } - } - } - - /** - * Called when the agent in the VM opens the connection. The - * default implementation does nothing. - */ - @SuppressWarnings("PMD.EmptyMethodInAbstractClassShouldBeAbstract") - protected void agentConnected() { - // Default is to do nothing. - } - - /** - * Called when the agent in the VM closes the connection. The - * default implementation does nothing. - */ - @SuppressWarnings("PMD.EmptyMethodInAbstractClassShouldBeAbstract") - protected void agentDisconnected() { - // Default is to do nothing. - } - -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CdMediaController.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CdMediaController.java index c4ac871..b7c960a 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CdMediaController.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CdMediaController.java @@ -25,9 +25,9 @@ import java.util.concurrent.ConcurrentHashMap; import org.jdrupes.vmoperator.runner.qemu.commands.QmpChangeMedium; import org.jdrupes.vmoperator.runner.qemu.commands.QmpOpenTray; import org.jdrupes.vmoperator.runner.qemu.commands.QmpRemoveMedium; -import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu; import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand; -import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.RunState; +import org.jdrupes.vmoperator.runner.qemu.events.RunnerConfigurationUpdate; +import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State; import org.jdrupes.vmoperator.runner.qemu.events.TrayMovedEvent; import org.jgrapes.core.Channel; import org.jgrapes.core.Component; @@ -36,6 +36,7 @@ import org.jgrapes.core.annotation.Handler; /** * The Class CdMediaController. */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") public class CdMediaController extends Component { /** @@ -54,6 +55,7 @@ public class CdMediaController extends Component { * * @param componentChannel the component channel */ + @SuppressWarnings("PMD.AssignmentToNonFinalStatic") public CdMediaController(Channel componentChannel) { super(componentChannel); } @@ -64,9 +66,10 @@ public class CdMediaController extends Component { * @param event the event */ @Handler - @SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops" }) - public void onConfigureQemu(ConfigureQemu event) { - if (event.runState() == RunState.TERMINATING) { + @SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition", + "PMD.AvoidInstantiatingObjectsInLoops" }) + public void onConfigureQemu(RunnerConfigurationUpdate event) { + if (event.state() == State.TERMINATING) { return; } @@ -79,7 +82,7 @@ public class CdMediaController extends Component { } var driveId = "cd" + cdCounter++; var newFile = Optional.ofNullable(drives[i].file).orElse(""); - if (event.runState() == RunState.STARTING) { + if (event.state() == State.STARTING) { current.put(driveId, newFile); continue; } @@ -113,8 +116,8 @@ public class CdMediaController extends Component { */ @Handler public void onTrayMovedEvent(TrayMovedEvent event) { - trayState.put(event.driveId(), event.trayState()); - if (event.trayState() == TrayState.OPEN + trayState.put(event.driveId(), event.state()); + if (event.state() == TrayState.OPEN && pending.containsKey(event.driveId())) { changeMedium(event.driveId()); } diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CommandDefinition.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CommandDefinition.java index 7aec209..fadc4a0 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CommandDefinition.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CommandDefinition.java @@ -42,7 +42,6 @@ class CommandDefinition { for (JsonNode path : jsonData.get("executable")) { if (Files.isExecutable(Path.of(path.asText()))) { command.add(path.asText()); - break; } } if (command.isEmpty()) { @@ -69,9 +68,4 @@ class CommandDefinition { public String name() { return name; } - - @Override - public String toString() { - return "Command " + name + ": " + command; - } } \ No newline at end of file diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java index 87e8c76..bbee56e 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java @@ -24,14 +24,10 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.PosixFilePermission; -import java.time.Instant; -import java.util.HashMap; -import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; -import static org.jdrupes.vmoperator.common.Constants.APP_NAME; import org.jdrupes.vmoperator.common.Convertions; import org.jdrupes.vmoperator.util.Dto; import org.jdrupes.vmoperator.util.FsdUtils; @@ -39,14 +35,11 @@ import org.jdrupes.vmoperator.util.FsdUtils; /** * The configuration information from the configuration file. */ +@SuppressWarnings("PMD.ExcessivePublicCount") public class Configuration implements Dto { - private static final String CI_INSTANCE_ID = "instance-id"; - + @SuppressWarnings("PMD.FieldNamingConventions") protected final Logger logger = Logger.getLogger(getClass().getName()); - /** Configuration timestamp. */ - public Instant asOf; - /** The data dir. */ public Path dataDir; @@ -71,42 +64,15 @@ public class Configuration implements Dto { /** The firmware vars. */ public Path firmwareVars; - /** The display password. */ - public boolean hasDisplayPassword; - - /** Optional cloud-init data. */ - public CloudInit cloudInit; - - /** If guest shutdown changes CRD .vm.state to "Stopped". */ - public boolean guestShutdownStops; - - /** Increments of the reset counter trigger a reset of the VM. */ - public Integer resetCounter; - /** The vm. */ @SuppressWarnings("PMD.ShortVariable") public Vm vm; - /** - * Subsection "cloud-init". - */ - public static class CloudInit implements Dto { - - /** The meta data. */ - public Map metaData; - - /** The user data. */ - public Map userData; - - /** The network config. */ - public Map networkConfig; - } - /** * Subsection "vm". */ @SuppressWarnings({ "PMD.ShortClassName", "PMD.TooManyFields", - "PMD.DataClass", "PMD.AvoidDuplicateLiterals" }) + "PMD.DataClass" }) public static class Vm implements Dto { /** The name. */ @@ -194,7 +160,6 @@ public class Configuration implements Dto { /** * Subsection "network". */ - @SuppressWarnings("PMD.DataClass") public static class Network implements Dto { /** The type. */ @@ -216,7 +181,6 @@ public class Configuration implements Dto { /** * Subsection "drive". */ - @SuppressWarnings("PMD.DataClass") public static class Drive implements Dto { /** The type. */ @@ -239,21 +203,12 @@ public class Configuration implements Dto { * The Class Display. */ public static class Display implements Dto { - - /** The number of outputs. */ - public int outputs = 1; - - /** The logged in user. */ - public String loggedInUser; - - /** The spice. */ public Spice spice; } /** * Subsection "spice". */ - @SuppressWarnings("PMD.DataClass") public static class Spice implements Dto { /** The port. */ @@ -289,11 +244,11 @@ public class Configuration implements Dto { } checkDrives(); - checkCloudInit(); return true; } + @SuppressWarnings("PMD.AvoidLiteralsInIfCondition") private void checkDrives() { for (Drive drive : vm.drives) { if (drive.file != null || drive.device != null @@ -313,10 +268,11 @@ public class Configuration implements Dto { } } + @SuppressWarnings("PMD.AvoidDeeplyNestedIfStmts") private boolean checkRuntimeDir() { // Runtime directory (sockets etc.) if (runtimeDir == null) { - var appDir = FsdUtils.runtimeDir(APP_NAME.replace("-", "")); + var appDir = FsdUtils.runtimeDir(Runner.APP_NAME.replace("-", "")); if (!Files.exists(appDir) && appDir.toFile().mkdirs()) { try { // When appDir is derived from XDG_RUNTIME_DIR @@ -332,7 +288,7 @@ public class Configuration implements Dto { runtimeDir)); } } - runtimeDir = FsdUtils.runtimeDir(APP_NAME.replace("-", "")) + runtimeDir = FsdUtils.runtimeDir(Runner.APP_NAME.replace("-", "")) .resolve(vm.name); runtimeDir.toFile().mkdir(); swtpmSocket = runtimeDir.resolve("swtpm-sock"); @@ -348,11 +304,12 @@ public class Configuration implements Dto { return true; } + @SuppressWarnings("PMD.AvoidDeeplyNestedIfStmts") private boolean checkDataDir() { // Data directory if (dataDir == null) { - dataDir - = FsdUtils.dataHome(APP_NAME.replace("-", "")).resolve(vm.name); + dataDir = FsdUtils.dataHome(Runner.APP_NAME.replace("-", "")) + .resolve(vm.name); } if (!Files.exists(dataDir)) { dataDir.toFile().mkdirs(); @@ -400,18 +357,4 @@ public class Configuration implements Dto { return true; } - - private void checkCloudInit() { - if (cloudInit == null) { - return; - } - - // Provide default for instance-id - if (cloudInit.metaData == null) { - cloudInit.metaData = new HashMap<>(); - } - if (!cloudInit.metaData.containsKey(CI_INSTANCE_ID)) { - cloudInit.metaData.put(CI_INSTANCE_ID, "v" + asOf.getEpochSecond()); - } - } } \ No newline at end of file diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/ConsoleTracker.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/ConsoleTracker.java deleted file mode 100644 index b50b481..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/ConsoleTracker.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu; - -import com.google.gson.JsonObject; -import io.kubernetes.client.apimachinery.GroupVersionKind; -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.openapi.models.EventsV1Event; -import java.io.IOException; -import java.util.logging.Level; -import static org.jdrupes.vmoperator.common.Constants.APP_NAME; -import org.jdrupes.vmoperator.common.Constants.Crd; -import org.jdrupes.vmoperator.common.Constants.Status; -import org.jdrupes.vmoperator.common.K8s; -import org.jdrupes.vmoperator.common.K8sClient; -import org.jdrupes.vmoperator.common.VmDefinitionStub; -import org.jdrupes.vmoperator.runner.qemu.events.Exit; -import org.jdrupes.vmoperator.runner.qemu.events.SpiceDisconnectedEvent; -import org.jdrupes.vmoperator.runner.qemu.events.SpiceInitializedEvent; -import org.jgrapes.core.Channel; -import org.jgrapes.core.annotation.Handler; -import org.jgrapes.core.events.Start; - -/** - * A (sub)component that updates the console status in the CR status. - * Created as child of {@link StatusUpdater}. - */ -public class ConsoleTracker extends VmDefUpdater { - - private VmDefinitionStub vmStub; - private String mainChannelClientHost; - private long mainChannelClientPort; - - /** - * Instantiates a new status updater. - * - * @param componentChannel the component channel - */ - public ConsoleTracker(Channel componentChannel) { - super(componentChannel); - apiClient = (K8sClient) io.kubernetes.client.openapi.Configuration - .getDefaultApiClient(); - } - - /** - * Handle the start event. - * - * @param event the event - * @throws IOException - * @throws ApiException - */ - @Handler - public void onStart(Start event) { - if (namespace == null) { - return; - } - try { - vmStub = VmDefinitionStub.get(apiClient, - new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM), - namespace, vmName); - } catch (ApiException e) { - logger.log(Level.SEVERE, e, - () -> "Cannot access VM object, terminating."); - event.cancel(true); - fire(new Exit(1)); - } - } - - /** - * On spice connected. - * - * @param event the event - * @throws ApiException the api exception - */ - @Handler - @SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition" }) - public void onSpiceInitialized(SpiceInitializedEvent event) - throws ApiException { - if (vmStub == null) { - return; - } - - // Only process connections using main channel. - if (event.channelType() != 1) { - return; - } - mainChannelClientHost = event.clientHost(); - mainChannelClientPort = event.clientPort(); - vmStub.updateStatus(from -> { - JsonObject status = updateCondition(from, "ConsoleConnected", true, - "Connected", "Connection from " + event.clientHost()); - status.addProperty(Status.CONSOLE_CLIENT, event.clientHost()); - return status; - }); - - // Log event - var evt = new EventsV1Event() - .reportingController(Crd.GROUP + "/" + APP_NAME) - .action("ConsoleConnectionUpdate") - .reason("Connection from " + event.clientHost()); - K8s.createEvent(apiClient, vmStub.model().get(), evt); - } - - /** - * On spice disconnected. - * - * @param event the event - * @throws ApiException the api exception - */ - @Handler - public void onSpiceDisconnected(SpiceDisconnectedEvent event) - throws ApiException { - if (vmStub == null) { - return; - } - - // Only process disconnects from main channel. - if (!event.clientHost().equals(mainChannelClientHost) - || event.clientPort() != mainChannelClientPort) { - return; - } - vmStub.updateStatus(from -> { - JsonObject status = updateCondition(from, "ConsoleConnected", false, - "Disconnected", event.clientHost() + " has disconnected"); - status.addProperty(Status.CONSOLE_CLIENT, ""); - return status; - }); - - // Log event - var evt = new EventsV1Event() - .reportingController(Crd.GROUP + "/" + APP_NAME) - .action("ConsoleConnectionUpdate") - .reason("Disconnected from " + event.clientHost()); - K8s.createEvent(apiClient, vmStub.model().get(), evt); - } -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Constants.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Constants.java deleted file mode 100644 index eac05fa..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Constants.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu; - -/** - * Some constants. - */ -public class Constants extends org.jdrupes.vmoperator.common.Constants { - - /** - * Process names. - */ - public static class ProcessName { - - /** The Constant QEMU. */ - public static final String QEMU = "qemu"; - - /** The Constant SWTPM. */ - public static final String SWTPM = "swtpm"; - - /** The Constant CLOUD_INIT_IMG. */ - public static final String CLOUD_INIT_IMG = "cloudInitImg"; - } - -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Containerfile.alpine b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Containerfile.alpine index d0104f3..def82ef 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Containerfile.alpine +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Containerfile.alpine @@ -2,7 +2,7 @@ FROM docker.io/alpine RUN apk update -RUN apk add qemu-system-x86_64 qemu-modules ovmf swtpm openjdk21 mtools +RUN apk add qemu-system-x86_64 qemu-modules ovmf swtpm openjdk17 RUN mkdir -p /etc/qemu && echo "allow all" > /etc/qemu/bridge.conf diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Containerfile.arch b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Containerfile.arch index 0c2fd86..379537b 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Containerfile.arch +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Containerfile.arch @@ -1,12 +1,10 @@ -FROM docker.io/archlinux/archlinux:latest +FROM archlinux/archlinux RUN systemd-firstboot -RUN pacman-key --init \ - && pacman -Sy --noconfirm archlinux-keyring && pacman -Su --noconfirm \ +RUN pacman -Suy --noconfirm \ && pacman -S --noconfirm which qemu-full virtiofsd \ - edk2-ovmf swtpm iproute2 bridge-utils jre21-openjdk-headless \ - mtools \ + edk2-ovmf swtpm iproute2 bridge-utils jre17-openjdk-headless \ && pacman -Scc --noconfirm # Remove all targets. diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CpuController.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CpuController.java index 440da91..f0face4 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CpuController.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CpuController.java @@ -22,18 +22,17 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import java.util.HashSet; import java.util.LinkedList; import java.util.List; -import java.util.Objects; import java.util.Optional; import java.util.Set; import org.jdrupes.vmoperator.runner.qemu.commands.QmpAddCpu; import org.jdrupes.vmoperator.runner.qemu.commands.QmpDelCpu; import org.jdrupes.vmoperator.runner.qemu.commands.QmpQueryHotpluggableCpus; -import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu; import org.jdrupes.vmoperator.runner.qemu.events.CpuAdded; import org.jdrupes.vmoperator.runner.qemu.events.CpuDeleted; import org.jdrupes.vmoperator.runner.qemu.events.HotpluggableCpuStatus; import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand; -import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.RunState; +import org.jdrupes.vmoperator.runner.qemu.events.RunnerConfigurationUpdate; +import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State; import org.jgrapes.core.Channel; import org.jgrapes.core.Component; import org.jgrapes.core.annotation.Handler; @@ -41,11 +40,12 @@ import org.jgrapes.core.annotation.Handler; /** * The Class CpuController. */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") public class CpuController extends Component { private Integer currentCpus; private Integer desiredCpus; - private ConfigureQemu suspendedConfigure; + private RunnerConfigurationUpdate suspendedConfigure; /** * Instantiates a new CPU controller. @@ -62,8 +62,8 @@ public class CpuController extends Component { * @param event the event */ @Handler - public void onConfigureQemu(ConfigureQemu event) { - if (event.runState() == RunState.TERMINATING) { + public void onConfigureQemu(RunnerConfigurationUpdate event) { + if (event.state() == State.TERMINATING) { return; } Optional.ofNullable(event.configuration().vm.currentCpus) @@ -170,7 +170,7 @@ public class CpuController extends Component { private void checkCpus() { if (suspendedConfigure != null && desiredCpus != null - && Objects.equals(currentCpus, desiredCpus)) { + && currentCpus == desiredCpus.intValue()) { suspendedConfigure.resumeHandling(); suspendedConfigure = null; } diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/DisplayController.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/DisplayController.java deleted file mode 100644 index c3bec93..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/DisplayController.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023,2025 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * 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.runner.qemu; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Objects; -import java.util.Optional; -import java.util.logging.Level; -import org.jdrupes.vmoperator.common.Constants.DisplaySecret; -import org.jdrupes.vmoperator.runner.qemu.commands.QmpSetDisplayPassword; -import org.jdrupes.vmoperator.runner.qemu.commands.QmpSetPasswordExpiry; -import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu; -import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand; -import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.RunState; -import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentConnected; -import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLogIn; -import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLogOut; -import org.jgrapes.core.Channel; -import org.jgrapes.core.Component; -import org.jgrapes.core.Event; -import org.jgrapes.core.annotation.Handler; -import org.jgrapes.util.events.FileChanged; -import org.jgrapes.util.events.WatchFile; - -/** - * The Class DisplayController. - */ -public class DisplayController extends Component { - - private String currentPassword; - private String protocol; - private final Path configDir; - private boolean canBeUpdated; - private boolean vmopAgentConnected; - private String loggedInUser; - - /** - * Instantiates a new Display controller. - * - * @param componentChannel the component channel - * @param configDir - */ - @SuppressWarnings({ "PMD.ConstructorCallsOverridableMethod" }) - public DisplayController(Channel componentChannel, Path configDir) { - super(componentChannel); - this.configDir = configDir; - fire(new WatchFile(configDir.resolve(DisplaySecret.PASSWORD))); - } - - /** - * On configure qemu. - * - * @param event the event - */ - @Handler - public void onConfigureQemu(ConfigureQemu event) { - if (event.runState() == RunState.TERMINATING) { - return; - } - protocol - = event.configuration().vm.display.spice != null ? "spice" : null; - loggedInUser = event.configuration().vm.display.loggedInUser; - configureLogin(); - if (event.runState() == RunState.STARTING) { - configurePassword(); - } - canBeUpdated = true; - } - - /** - * On vmop agent connected. - * - * @param event the event - */ - @Handler - public void onVmopAgentConnected(VmopAgentConnected event) { - vmopAgentConnected = true; - configureLogin(); - } - - private void configureLogin() { - if (!vmopAgentConnected) { - return; - } - Event evt = loggedInUser != null - ? new VmopAgentLogIn(loggedInUser) - : new VmopAgentLogOut(); - fire(evt); - } - - /** - * Watch for changes of the password file. - * - * @param event the event - */ - @Handler - public void onFileChanged(FileChanged event) { - if (event.path().equals(configDir.resolve(DisplaySecret.PASSWORD))) { - logger.fine(() -> "Display password updated"); - if (canBeUpdated) { - configurePassword(); - } - } - } - - private void configurePassword() { - if (protocol == null) { - return; - } - if (setDisplayPassword()) { - setPasswordExpiry(); - } - } - - private boolean setDisplayPassword() { - return readFromFile(DisplaySecret.PASSWORD).map(password -> { - if (Objects.equals(this.currentPassword, password)) { - return true; - } - this.currentPassword = password; - logger.fine(() -> "Updating display password"); - fire(new MonitorCommand( - new QmpSetDisplayPassword(protocol, password))); - return true; - }).orElse(false); - } - - private void setPasswordExpiry() { - readFromFile(DisplaySecret.EXPIRY).ifPresent(expiry -> { - logger.fine(() -> "Updating expiry time to " + expiry); - fire( - new MonitorCommand(new QmpSetPasswordExpiry(protocol, expiry))); - }); - } - - private Optional readFromFile(String dataItem) { - Path path = configDir.resolve(dataItem); - String label = dataItem.replace('-', ' '); - if (path.toFile().canRead()) { - logger.finer(() -> "Found " + label); - try { - return Optional.ofNullable(Files.readString(path)); - } catch (IOException e) { - logger.log(Level.WARNING, e, () -> "Cannot read " + label + ": " - + e.getMessage()); - return Optional.empty(); - } - } else { - logger.finer(() -> "No " + label); - return Optional.empty(); - } - } -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/GuestAgentClient.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/GuestAgentClient.java deleted file mode 100644 index 45d2487..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/GuestAgentClient.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * 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.runner.qemu; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.node.ObjectNode; -import java.io.IOException; -import java.time.Instant; -import java.util.LinkedList; -import java.util.Queue; -import java.util.logging.Level; -import org.jdrupes.vmoperator.runner.qemu.Constants.ProcessName; -import org.jdrupes.vmoperator.runner.qemu.commands.QmpCommand; -import org.jdrupes.vmoperator.runner.qemu.commands.QmpGuestGetOsinfo; -import org.jdrupes.vmoperator.runner.qemu.commands.QmpGuestPowerdown; -import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu; -import org.jdrupes.vmoperator.runner.qemu.events.GuestAgentCommand; -import org.jdrupes.vmoperator.runner.qemu.events.OsinfoEvent; -import org.jgrapes.core.Channel; -import org.jgrapes.core.Components; -import org.jgrapes.core.Components.Timer; -import org.jgrapes.core.annotation.Handler; -import org.jgrapes.core.events.Stop; -import org.jgrapes.io.events.ProcessExited; - -/** - * A component that handles the communication with the guest agent. - * - * If the log level for this class is set to fine, the messages - * exchanged on the monitor socket are logged. - */ -public class GuestAgentClient extends AgentConnector { - - private boolean connected; - private Instant powerdownStartedAt; - private int powerdownTimeout; - private Timer powerdownTimer; - private final Queue executing = new LinkedList<>(); - private Stop suspendedStop; - - /** - * Instantiates a new guest agent client. - * - * @param componentChannel the component channel - * @throws IOException Signals that an I/O exception has occurred. - */ - public GuestAgentClient(Channel componentChannel) throws IOException { - super(componentChannel); - } - - /** - * When the agent has connected, request the OS information. - */ - @Override - protected void agentConnected() { - logger.fine(() -> "Guest agent connected"); - connected = true; - rep().fire(new GuestAgentCommand(new QmpGuestGetOsinfo())); - } - - @Override - protected void agentDisconnected() { - logger.fine(() -> "Guest agent disconnected"); - connected = false; - } - - /** - * Process agent input. - * - * @param line the line - * @throws IOException Signals that an I/O exception has occurred. - */ - @Override - protected void processInput(String line) throws IOException { - logger.finer(() -> "guest agent(in): " + line); - try { - var response = mapper.readValue(line, ObjectNode.class); - if (response.has("return") || response.has("error")) { - QmpCommand executed = executing.poll(); - logger.finer(() -> String.format("(Previous \"guest agent(in)\"" - + " is result from executing %s)", executed)); - if (executed instanceof QmpGuestGetOsinfo) { - var osInfo = new OsinfoEvent(response.get("return")); - logger.fine(() -> "Guest agent triggers: " + osInfo); - rep().fire(osInfo); - } - } - } catch (JsonProcessingException e) { - throw new IOException(e); - } - } - - /** - * On guest agent command. - * - * @param event the event - * @throws IOException Signals that an I/O exception has occurred. - */ - @Handler - @SuppressWarnings({ "PMD.AvoidSynchronizedStatement", - "PMD.AvoidDuplicateLiterals" }) - public void onGuestAgentCommand(GuestAgentCommand event) - throws IOException { - if (qemuChannel() == null) { - return; - } - var command = event.command(); - logger.fine(() -> "Guest handles: " + event); - String asText; - try { - asText = command.asText(); - logger.finer(() -> "guest agent(out): " + asText); - } catch (JsonProcessingException e) { - logger.log(Level.SEVERE, e, - () -> "Cannot serialize Json: " + e.getMessage()); - return; - } - synchronized (executing) { - if (writer().isPresent()) { - executing.add(command); - sendCommand(asText); - } - } - } - - /** - * Shutdown the VM. - * - * @param event the event - */ - @Handler(priority = 200) - @SuppressWarnings("PMD.AvoidSynchronizedStatement") - public void onStop(Stop event) { - if (!connected) { - logger.fine(() -> "No guest agent connection," - + " cannot send shutdown command"); - return; - } - - // We have a connection to the guest agent attempt shutdown. - powerdownStartedAt = event.associated(Instant.class).orElseGet(() -> { - var now = Instant.now(); - event.setAssociated(Instant.class, now); - return now; - }); - var waitUntil = powerdownStartedAt.plusSeconds(powerdownTimeout); - if (waitUntil.isBefore(Instant.now())) { - return; - } - event.suspendHandling(); - suspendedStop = event; - logger.fine(() -> "Attempting shutdown through guest agent," - + " waiting for termination until " + waitUntil); - powerdownTimer = Components.schedule(t -> { - logger.fine(() -> "Powerdown timeout reached."); - synchronized (this) { - powerdownTimer = null; - if (suspendedStop != null) { - suspendedStop.resumeHandling(); - suspendedStop = null; - } - } - }, waitUntil); - rep().fire(new GuestAgentCommand(new QmpGuestPowerdown())); - } - - /** - * On process exited. - * - * @param event the event - */ - @Handler - @SuppressWarnings("PMD.AvoidSynchronizedStatement") - public void onProcessExited(ProcessExited event) { - if (!event.startedBy().associated(CommandDefinition.class) - .map(cd -> ProcessName.QEMU.equals(cd.name())).orElse(false)) { - return; - } - synchronized (this) { - if (powerdownTimer != null) { - powerdownTimer.cancel(); - } - if (suspendedStop != null) { - suspendedStop.resumeHandling(); - suspendedStop = null; - } - } - } - - /** - * On configure qemu. - * - * @param event the event - */ - @Handler - @SuppressWarnings("PMD.AvoidSynchronizedStatement") - public void onConfigureQemu(ConfigureQemu event) { - int newTimeout = event.configuration().vm.powerdownTimeout; - if (powerdownTimeout != newTimeout) { - powerdownTimeout = newTimeout; - synchronized (this) { - if (powerdownTimer != null) { - powerdownTimer - .reschedule(powerdownStartedAt.plusSeconds(newTimeout)); - } - - } - } - } -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/QemuConnector.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/QemuConnector.java deleted file mode 100644 index 777478e..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/QemuConnector.java +++ /dev/null @@ -1,249 +0,0 @@ -/* - * 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.runner.qemu; - -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.IOException; -import java.io.Writer; -import java.lang.reflect.UndeclaredThrowableException; -import java.net.UnixDomainSocketAddress; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Optional; -import org.jgrapes.core.Channel; -import org.jgrapes.core.Component; -import org.jgrapes.core.EventPipeline; -import org.jgrapes.core.annotation.Handler; -import org.jgrapes.core.events.Start; -import org.jgrapes.core.events.Stop; -import org.jgrapes.io.events.Closed; -import org.jgrapes.io.events.ConnectError; -import org.jgrapes.io.events.Input; -import org.jgrapes.io.events.OpenSocketConnection; -import org.jgrapes.io.util.ByteBufferWriter; -import org.jgrapes.io.util.LineCollector; -import org.jgrapes.net.SocketIOChannel; -import org.jgrapes.net.events.ClientConnected; -import org.jgrapes.util.events.ConfigurationUpdate; -import org.jgrapes.util.events.FileChanged; -import org.jgrapes.util.events.WatchFile; - -/** - * A component that handles the communication with QEMU over a socket. - * - * Derived classes should log the messages exchanged on the socket - * if the log level is set to fine. - */ -public abstract class QemuConnector extends Component { - - @SuppressWarnings("PMD.FieldNamingConventions") - protected static final ObjectMapper mapper = new ObjectMapper(); - - private EventPipeline rep; - private Path socketPath; - private SocketIOChannel qemuChannel; - - /** - * Instantiates a new QEMU connector. - * - * @param componentChannel the component channel - * @throws IOException Signals that an I/O exception has occurred. - */ - public QemuConnector(Channel componentChannel) throws IOException { - super(componentChannel); - } - - /** - * As the initial configuration of this component depends on the - * configuration of the {@link Runner}, it doesn't have a handler - * for the {@link ConfigurationUpdate} event. The values are - * forwarded from the {@link Runner} instead. - * - * @param socketPath the socket path - */ - /* default */ void configure(Path socketPath) { - this.socketPath = socketPath; - logger.fine(() -> getClass().getSimpleName() - + " configured with socketPath=" + socketPath); - } - - /** - * Note the runner's event processor and delete the socket. - * - * @param event the event - * @throws IOException Signals that an I/O exception has occurred. - */ - @Handler - public void onStart(Start event) throws IOException { - rep = event.associated(EventPipeline.class).get(); - if (socketPath == null) { - return; - } - Files.deleteIfExists(socketPath); - fire(new WatchFile(socketPath)); - } - - /** - * Return the runner's event pipeline. - * - * @return the event pipeline - */ - protected EventPipeline rep() { - return rep; - } - - /** - * Watch for the creation of the swtpm socket and start the - * qemu process if it has been created. - * - * @param event the event - */ - @Handler - public void onFileChanged(FileChanged event) { - if (event.change() == FileChanged.Kind.CREATED - && event.path().equals(socketPath)) { - // qemu running, open socket - fire(new OpenSocketConnection( - UnixDomainSocketAddress.of(socketPath)) - .setAssociated(this, this)); - } - } - - /** - * Check if this is from opening the agent socket and if true, - * save the socket in the context and associate the channel with - * the context. - * - * @param event the event - * @param channel the channel - */ - @SuppressWarnings("resource") - @Handler - public void onClientConnected(ClientConnected event, - SocketIOChannel channel) { - event.openEvent().associated(this, getClass()).ifPresent(qc -> { - qemuChannel = channel; - channel.setAssociated(this, this); - channel.setAssociated(Writer.class, new ByteBufferWriter( - channel).nativeCharset()); - channel.setAssociated(LineCollector.class, - new LineCollector() - .consumer(line -> { - try { - qc.processInput(line); - } catch (IOException e) { - throw new UndeclaredThrowableException(e); - } - })); - qc.socketConnected(); - }); - } - - /** - * Return the QEMU channel if the connection has been established. - * - * @return the socket IO channel - */ - protected Optional qemuChannel() { - return Optional.ofNullable(qemuChannel); - } - - /** - * Return the {@link Writer} for the connection if the connection - * has been established. - * - * @return the optional - */ - protected Optional writer() { - return qemuChannel().flatMap(c -> c.associated(Writer.class)); - } - - /** - * Send the given command to QEMU. A newline is appended to the - * command automatically. - * - * @param command the command - * @return true, if successful - * @throws IOException Signals that an I/O exception has occurred. - */ - protected boolean sendCommand(String command) throws IOException { - if (writer().isEmpty()) { - return false; - } - writer().get().append(command).append('\n').flush(); - return true; - } - - /** - * Called when the connector has been connected to the socket. - */ - @SuppressWarnings("PMD.EmptyMethodInAbstractClassShouldBeAbstract") - protected void socketConnected() { - // Default is to do nothing. - } - - /** - * Called when a connection attempt fails. - * - * @param event the event - */ - @Handler - public void onConnectError(ConnectError event) { - event.event().associated(this, getClass()).ifPresent(qc -> { - rep.fire(new Stop()); - }); - } - - /** - * Handle data from the socket connection. - * - * @param event the event - * @param channel the channel - */ - @Handler - public void onInput(Input event, SocketIOChannel channel) { - if (channel.associated(this, getClass()).isEmpty()) { - return; - } - channel.associated(LineCollector.class).ifPresent(collector -> { - collector.feed(event); - }); - } - - /** - * Process agent input. - * - * @param line the line - * @throws IOException Signals that an I/O exception has occurred. - */ - protected abstract void processInput(String line) throws IOException; - - /** - * On closed. - * - * @param event the event - * @param channel the channel - */ - @Handler - public void onClosed(Closed event, SocketIOChannel channel) { - channel.associated(this, getClass()).ifPresent(qc -> { - qemuChannel = null; - }); - } -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/QemuMonitor.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/QemuMonitor.java index feeb76a..3d22b26 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/QemuMonitor.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/QemuMonitor.java @@ -19,33 +19,47 @@ package org.jdrupes.vmoperator.runner.qemu; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import java.io.IOException; +import java.io.Writer; +import java.lang.reflect.UndeclaredThrowableException; +import java.net.UnixDomainSocketAddress; +import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; import java.time.Instant; import java.util.LinkedList; import java.util.Queue; import java.util.logging.Level; -import org.jdrupes.vmoperator.runner.qemu.Constants.ProcessName; import org.jdrupes.vmoperator.runner.qemu.commands.QmpCapabilities; import org.jdrupes.vmoperator.runner.qemu.commands.QmpCommand; import org.jdrupes.vmoperator.runner.qemu.commands.QmpPowerdown; -import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu; import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand; import org.jdrupes.vmoperator.runner.qemu.events.MonitorEvent; import org.jdrupes.vmoperator.runner.qemu.events.MonitorReady; import org.jdrupes.vmoperator.runner.qemu.events.MonitorResult; import org.jdrupes.vmoperator.runner.qemu.events.PowerdownEvent; +import org.jdrupes.vmoperator.runner.qemu.events.RunnerConfigurationUpdate; import org.jgrapes.core.Channel; +import org.jgrapes.core.Component; import org.jgrapes.core.Components; import org.jgrapes.core.Components.Timer; +import org.jgrapes.core.EventPipeline; import org.jgrapes.core.annotation.Handler; +import org.jgrapes.core.events.Start; import org.jgrapes.core.events.Stop; import org.jgrapes.io.events.Closed; -import org.jgrapes.io.events.ProcessExited; +import org.jgrapes.io.events.ConnectError; +import org.jgrapes.io.events.Input; +import org.jgrapes.io.events.OpenSocketConnection; +import org.jgrapes.io.util.ByteBufferWriter; +import org.jgrapes.io.util.LineCollector; import org.jgrapes.net.SocketIOChannel; +import org.jgrapes.net.events.ClientConnected; import org.jgrapes.util.events.ConfigurationUpdate; +import org.jgrapes.util.events.FileChanged; +import org.jgrapes.util.events.WatchFile; /** * A component that handles the communication over the Qemu monitor @@ -54,29 +68,32 @@ import org.jgrapes.util.events.ConfigurationUpdate; * If the log level for this class is set to fine, the messages * exchanged on the monitor socket are logged. */ -public class QemuMonitor extends QemuConnector { +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") +public class QemuMonitor extends Component { + private static ObjectMapper mapper = new ObjectMapper(); + + private EventPipeline rep; + private Path socketPath; private int powerdownTimeout; + private SocketIOChannel monitorChannel; private final Queue executing = new LinkedList<>(); private Instant powerdownStartedAt; private Stop suspendedStop; private Timer powerdownTimer; private boolean powerdownConfirmed; - private boolean monitorReady; /** - * Instantiates a new QEMU monitor. + * Instantiates a new qemu monitor. * * @param componentChannel the component channel - * @param configDir the config dir * @throws IOException Signals that an I/O exception has occurred. */ - public QemuMonitor(Channel componentChannel, Path configDir) - throws IOException { + @SuppressWarnings("PMD.AssignmentToNonFinalStatic") + public QemuMonitor(Channel componentChannel) throws IOException { super(componentChannel); attach(new RamController(channel())); attach(new CpuController(channel())); - attach(new DisplayController(channel(), configDir)); attach(new CdMediaController(channel())); } @@ -90,45 +107,120 @@ public class QemuMonitor extends QemuConnector { * @param powerdownTimeout */ /* default */ void configure(Path socketPath, int powerdownTimeout) { - super.configure(socketPath); + this.socketPath = socketPath; this.powerdownTimeout = powerdownTimeout; } /** - * When the socket is connected, send the capabilities command. + * Handle the start event. + * + * @param event the event + * @throws IOException Signals that an I/O exception has occurred. */ - @Override - protected void socketConnected() { - rep().fire(new MonitorCommand(new QmpCapabilities())); + @Handler + public void onStart(Start event) throws IOException { + rep = event.associated(EventPipeline.class).get(); + if (socketPath == null) { + return; + } + Files.deleteIfExists(socketPath); + fire(new WatchFile(socketPath)); } - @Override - protected void processInput(String line) + /** + * Watch for the creation of the swtpm socket and start the + * qemu process if it has been created. + * + * @param event the event + */ + @Handler + public void onFileChanged(FileChanged event) { + if (event.change() == FileChanged.Kind.CREATED + && event.path().equals(socketPath)) { + // qemu running, open socket + fire(new OpenSocketConnection( + UnixDomainSocketAddress.of(socketPath)) + .setAssociated(QemuMonitor.class, this)); + } + } + + /** + * Check if this is from opening the monitor socket and if true, + * save the socket in the context and associate the channel with + * the context. Then send the initial message to the socket. + * + * @param event the event + * @param channel the channel + */ + @Handler + public void onClientConnected(ClientConnected event, + SocketIOChannel channel) { + event.openEvent().associated(QemuMonitor.class).ifPresent(qm -> { + monitorChannel = channel; + channel.setAssociated(QemuMonitor.class, this); + channel.setAssociated(Writer.class, new ByteBufferWriter( + channel).nativeCharset()); + channel.setAssociated(LineCollector.class, + new LineCollector() + .consumer(line -> { + try { + processMonitorInput(line); + } catch (IOException e) { + throw new UndeclaredThrowableException(e); + } + })); + fire(new MonitorCommand(new QmpCapabilities())); + }); + } + + /** + * Called when a connection attempt fails. + * + * @param event the event + * @param channel the channel + */ + @Handler + public void onConnectError(ConnectError event, SocketIOChannel channel) { + event.event().associated(QemuMonitor.class).ifPresent(qm -> { + rep.fire(new Stop()); + }); + } + + /** + * Handle data from qemu monitor connection. + * + * @param event the event + * @param channel the channel + */ + @Handler + public void onInput(Input event, SocketIOChannel channel) { + if (channel.associated(QemuMonitor.class).isEmpty()) { + return; + } + channel.associated(LineCollector.class).ifPresent(collector -> { + collector.feed(event); + }); + } + + private void processMonitorInput(String line) throws IOException { - logger.finer(() -> "monitor(in): " + line); + logger.fine(() -> "monitor(in): " + line); try { var response = mapper.readValue(line, ObjectNode.class); if (response.has("QMP")) { - monitorReady = true; - logger.fine(() -> "QMP connection ready"); - rep().fire(new MonitorReady()); + rep.fire(new MonitorReady()); return; } if (response.has("return") || response.has("error")) { QmpCommand executed = executing.poll(); - logger.finer( + logger.fine( () -> String.format("(Previous \"monitor(in)\" is result " + "from executing %s)", executed)); - var monRes = MonitorResult.from(executed, response); - logger.fine(() -> "QMP triggers: " + monRes); - rep().fire(monRes); + rep.fire(MonitorResult.from(executed, response)); return; } if (response.has("event")) { - MonitorEvent.from(response).ifPresent(me -> { - logger.fine(() -> "QMP triggers: " + me); - rep().fire(me); - }); + MonitorEvent.from(response).ifPresent(rep::fire); } } catch (JsonProcessingException e) { throw new IOException(e); @@ -142,10 +234,17 @@ public class QemuMonitor extends QemuConnector { */ @Handler public void onClosed(Closed event, SocketIOChannel channel) { - channel.associated(this, getClass()).ifPresent(qm -> { - super.onClosed(event, channel); - logger.fine(() -> "QMP connection closed."); - monitorReady = false; + channel.associated(QemuMonitor.class).ifPresent(qm -> { + monitorChannel = null; + synchronized (this) { + if (powerdownTimer != null) { + powerdownTimer.cancel(); + } + if (suspendedStop != null) { + suspendedStop.resumeHandling(); + suspendedStop = null; + } + } }); } @@ -153,37 +252,29 @@ public class QemuMonitor extends QemuConnector { * On monitor command. * * @param event the event - * @throws IOException */ @Handler - @SuppressWarnings({ "PMD.AvoidSynchronizedStatement", - "PMD.AvoidDuplicateLiterals" }) - public void onMonitorCommand(MonitorCommand event) throws IOException { - // Check prerequisites - if (!monitorReady && !(event.command() instanceof QmpCapabilities)) { - logger.severe(() -> "Premature QMP command (not ready): " - + event.command()); - rep().fire(new Stop()); - return; - } - - // Send the command + public void onExecQmpCommand(MonitorCommand event) { var command = event.command(); - logger.fine(() -> "QMP handles: " + event.toString()); String asText; try { - asText = command.asText(); - logger.finer(() -> "monitor(out): " + asText); + asText = mapper.writeValueAsString(command.toJson()); } catch (JsonProcessingException e) { logger.log(Level.SEVERE, e, () -> "Cannot serialize Json: " + e.getMessage()); return; } + logger.fine(() -> "monitor(out): " + asText); synchronized (executing) { - if (writer().isPresent()) { - executing.add(command); - sendCommand(asText); - } + monitorChannel.associated(Writer.class).ifPresent(writer -> { + try { + executing.add(command); + writer.append(asText).append('\n').flush(); + } catch (IOException e) { + // Cannot happen, but... + logger.log(Level.WARNING, e, () -> e.getMessage()); + } + }); } } @@ -193,51 +284,37 @@ public class QemuMonitor extends QemuConnector { * @param event the event */ @Handler(priority = 100) - @SuppressWarnings("PMD.AvoidSynchronizedStatement") public void onStop(Stop event) { - if (!monitorReady) { - logger.fine(() -> "Not sending QMP powerdown command" - + " because QMP connection is closed"); - return; - } + if (monitorChannel != null) { + // We have a connection to Qemu, attempt ACPI shutdown. + event.suspendHandling(); + suspendedStop = event; - // We have a connection to Qemu, attempt ACPI shutdown if time left - powerdownStartedAt = event.associated(Instant.class).orElseGet(() -> { - var now = Instant.now(); - event.setAssociated(Instant.class, now); - return now; - }); - if (powerdownStartedAt.plusSeconds(powerdownTimeout) - .isBefore(Instant.now())) { - return; - } - event.suspendHandling(); - suspendedStop = event; - - // Send command. If not confirmed, assume "hanging" qemu process. - powerdownTimer = Components.schedule(t -> { - logger.fine(() -> "QMP powerdown command not confirmed"); - synchronized (this) { - powerdownTimer = null; - if (suspendedStop != null) { - suspendedStop.resumeHandling(); - suspendedStop = null; + // Attempt powerdown command. If not confirmed, assume + // "hanging" qemu process. + powerdownTimer = Components.schedule(t -> { + // Powerdown not confirmed + logger.fine(() -> "QMP powerdown command has not effect."); + synchronized (this) { + powerdownTimer = null; + if (suspendedStop != null) { + suspendedStop.resumeHandling(); + suspendedStop = null; + } } - } - }, Duration.ofSeconds(5)); - logger.fine(() -> "Attempting QMP (ACPI) powerdown."); - rep().fire(new MonitorCommand(new QmpPowerdown())); + }, Duration.ofSeconds(1)); + logger.fine(() -> "Attempting QMP powerdown."); + powerdownStartedAt = Instant.now(); + fire(new MonitorCommand(new QmpPowerdown())); + } } /** - * When the powerdown event is confirmed, wait for termination - * or timeout. Termination is detected by the qemu process exiting - * (see {@link #onProcessExited(ProcessExited)}). + * On powerdown event. * * @param event the event */ @Handler - @SuppressWarnings("PMD.AvoidSynchronizedStatement") public void onPowerdownEvent(PowerdownEvent event) { synchronized (this) { // Cancel confirmation timeout @@ -246,54 +323,27 @@ public class QemuMonitor extends QemuConnector { } // (Re-)schedule timer as fallback - var waitUntil = powerdownStartedAt.plusSeconds(powerdownTimeout); - logger.fine(() -> "QMP powerdown confirmed, waiting for" - + " termination until " + waitUntil); + logger.fine(() -> "QMP powerdown confirmed, waiting..."); powerdownTimer = Components.schedule(t -> { logger.fine(() -> "Powerdown timeout reached."); synchronized (this) { - powerdownTimer = null; if (suspendedStop != null) { suspendedStop.resumeHandling(); suspendedStop = null; } } - }, waitUntil); + }, powerdownStartedAt.plusSeconds(powerdownTimeout)); powerdownConfirmed = true; } } - /** - * On process exited. - * - * @param event the event - */ - @Handler - @SuppressWarnings("PMD.AvoidSynchronizedStatement") - public void onProcessExited(ProcessExited event) { - if (!event.startedBy().associated(CommandDefinition.class) - .map(cd -> ProcessName.QEMU.equals(cd.name())).orElse(false)) { - return; - } - synchronized (this) { - if (powerdownTimer != null) { - powerdownTimer.cancel(); - } - if (suspendedStop != null) { - suspendedStop.resumeHandling(); - suspendedStop = null; - } - } - } - /** * On configure qemu. * * @param event the event */ @Handler - @SuppressWarnings("PMD.AvoidSynchronizedStatement") - public void onConfigureQemu(ConfigureQemu event) { + public void onConfigureQemu(RunnerConfigurationUpdate event) { int newTimeout = event.configuration().vm.powerdownTimeout; if (powerdownTimeout != newTimeout) { powerdownTimeout = newTimeout; diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/RamController.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/RamController.java index 81a10f9..05fdde6 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/RamController.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/RamController.java @@ -21,8 +21,8 @@ package org.jdrupes.vmoperator.runner.qemu; import java.math.BigInteger; import java.util.Optional; import org.jdrupes.vmoperator.runner.qemu.commands.QmpSetBalloon; -import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu; import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand; +import org.jdrupes.vmoperator.runner.qemu.events.RunnerConfigurationUpdate; import org.jgrapes.core.Channel; import org.jgrapes.core.Component; import org.jgrapes.core.annotation.Handler; @@ -39,6 +39,7 @@ public class RamController extends Component { * * @param componentChannel the component channel */ + @SuppressWarnings("PMD.AssignmentToNonFinalStatic") public RamController(Channel componentChannel) { super(componentChannel); } @@ -49,7 +50,7 @@ public class RamController extends Component { * @param event the event */ @Handler - public void onConfigureQemu(ConfigureQemu event) { + public void onConfigureQemu(RunnerConfigurationUpdate event) { Optional.ofNullable(event.configuration().vm.currentRam) .ifPresent(cr -> { if (currentRam != null && currentRam.equals(cr)) { diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java index 4819dcd..3ace2e5 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java @@ -1,6 +1,6 @@ /* * VM-Operator - * Copyright (C) 2023,2025 Michael N. Lipp + * 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 @@ -24,7 +24,6 @@ import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; import freemarker.core.ParseException; import freemarker.template.MalformedTemplateNameException; import freemarker.template.TemplateException; @@ -39,34 +38,24 @@ import java.lang.reflect.UndeclaredThrowableException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.time.Instant; import java.util.Comparator; -import java.util.EnumSet; import java.util.HashMap; +import java.util.Map; import java.util.Optional; -import java.util.Set; import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; -import static org.jdrupes.vmoperator.common.Constants.APP_NAME; -import org.jdrupes.vmoperator.common.Constants.DisplaySecret; -import org.jdrupes.vmoperator.runner.qemu.Constants.ProcessName; import org.jdrupes.vmoperator.runner.qemu.commands.QmpCont; -import org.jdrupes.vmoperator.runner.qemu.commands.QmpReset; -import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu; -import org.jdrupes.vmoperator.runner.qemu.events.Exit; import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand; -import org.jdrupes.vmoperator.runner.qemu.events.OsinfoEvent; import org.jdrupes.vmoperator.runner.qemu.events.QmpConfigured; +import org.jdrupes.vmoperator.runner.qemu.events.RunnerConfigurationUpdate; import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange; -import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.RunState; +import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State; import org.jdrupes.vmoperator.util.ExtendedObjectWrapper; import org.jdrupes.vmoperator.util.FsdUtils; import org.jgrapes.core.Channel; @@ -118,27 +107,21 @@ import org.jgrapes.util.events.WatchFile; * * state "Starting (Processes)" as StartingProcess { * + * state which <> + * state "Start swtpm" as swtpm * state "Start qemu" as qemu * state "Open monitor" as monitor * state "Configure QMP" as waitForConfigured * state "Configure QEMU" as configure * state success <> * state error <> - * - * state prepFork <> - * state prepJoin <> - * state "Generate cloud-init image" as cloudInit - * prepFork --> cloudInit: [cloud-init data provided] - * swtpm --> prepJoin: FileChanged[swtpm socket created] - * state "Start swtpm" as swtpm - * prepFork --> swtpm: [use swtpm] + * + * which --> swtpm: [use swtpm] + * which --> qemu: [else] + * * swtpm: entry/start swtpm - * cloudInit --> prepJoin: ProcessExited - * cloudInit: entry/generate cloud-init image - * prepFork --> prepJoin: [else] - * - * prepJoin --> qemu - * + * swtpm -> qemu: FileChanged[swtpm socket created] + * * qemu: entry/start qemu * qemu --> monitor : FileChanged[monitor socket created] * @@ -149,23 +132,14 @@ import org.jgrapes.util.events.WatchFile; * waitForConfigured: entry/fire QmpCapabilities * waitForConfigured --> configure: QmpConfigured * - * configure: entry/fire ConfigureQemu - * configure --> success: ConfigureQemu (last handler)/fire cont command + * configure: entry/fire RunnerConfigurationUpdate + * configure --> success: RunnerConfigurationUpdate (last handler)/fire cont command * } * - * Initializing --> prepFork: Started + * Initializing --> which: Started * * success --> Running * - * state Running { - * state Booting - * state Booted - * - * [*] -right-> Booting - * Booting -down-> Booting: VserportChanged[guest agent connected]/fire GetOsinfo - * Booting --> Booted: Osinfo - * } - * * state Terminating { * state terminate <> * state qemuRunning <> @@ -201,46 +175,28 @@ import org.jgrapes.util.events.WatchFile; * */ @SuppressWarnings({ "PMD.ExcessiveImports", "PMD.AvoidPrintStackTrace", - "PMD.TooManyMethods", "PMD.CouplingBetweenObjects" }) + "PMD.DataflowAnomalyAnalysis" }) public class Runner extends Component { + /** The Constant APP_NAME. */ + public static final String APP_NAME = "vm-runner"; private static final String TEMPLATE_DIR = "/opt/" + APP_NAME.replace("-", "") + "/templates"; private static final String DEFAULT_TEMPLATE = "Standard-VM-latest.ftl.yaml"; private static final String SAVED_TEMPLATE = "VM.ftl.yaml"; private static final String FW_VARS = "fw-vars.fd"; - private static int exitStatus; - private final EventPipeline rep = newEventPipeline(); - private final ObjectMapper yamlMapper = new ObjectMapper(YAMLFactory - .builder().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER) - .build()); + private EventPipeline rep; + private final ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory()); private final JsonNode defaults; - private final File configFile; - private final Path configDir; - private Configuration initialConfig; - private Configuration pendingConfig; + @SuppressWarnings("PMD.UseConcurrentHashMap") + private Configuration config = new Configuration(); private final freemarker.template.Configuration fmConfig; private CommandDefinition swtpmDefinition; - private CommandDefinition cloudInitImgDefinition; private CommandDefinition qemuDefinition; private final QemuMonitor qemuMonitor; - private boolean qmpConfigured; - private final GuestAgentClient guestAgentClient; - private final VmopAgentClient vmopAgentClient; - private Integer resetCounter; - private RunState state = RunState.INITIALIZING; - - /** Preparatory actions for QEMU start */ - @SuppressWarnings("PMD.FieldNamingConventions") - private enum QemuPreps { - Config, - Tpm, - CloudInit - } - - private final Set qemuLatch = EnumSet.noneOf(QemuPreps.class); + private State state = State.INITIALIZING; /** * Instantiates a new runner. @@ -248,7 +204,7 @@ public class Runner extends Component { * @param cmdLine the cmd line * @throws IOException Signals that an I/O exception has occurred. */ - @SuppressWarnings({ "PMD.ConstructorCallsOverridableMethod" }) + @SuppressWarnings("PMD.SystemPrintln") public Runner(CommandLine cmdLine) throws IOException { yamlMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); @@ -257,17 +213,6 @@ public class Runner extends Component { defaults = yamlMapper.readValue( Runner.class.getResourceAsStream("defaults.yaml"), JsonNode.class); - // Get the config - configFile = new File(cmdLine.getOptionValue('c', - "/etc/opt/" + APP_NAME.replace("-", "") + "/config.yaml")); - // Don't rely on night config to produce a good exception - // for this simple case - if (!Files.isReadable(configFile.toPath())) { - throw new IOException( - "Cannot read configuration file " + configFile); - } - configDir = configFile.getParentFile().toPath().toRealPath(); - // Configure freemarker library fmConfig = new freemarker.template.Configuration( freemarker.template.Configuration.VERSION_2_3_32); @@ -284,12 +229,19 @@ public class Runner extends Component { attach(new FileSystemWatcher(channel())); attach(new ProcessManager(channel())); attach(new SocketConnector(channel())); - attach(qemuMonitor = new QemuMonitor(channel(), configDir)); - attach(guestAgentClient = new GuestAgentClient(channel())); - attach(vmopAgentClient = new VmopAgentClient(channel())); + attach(qemuMonitor = new QemuMonitor(channel())); attach(new StatusUpdater(channel())); - attach(new YamlConfigurationStore(channel(), configFile, false)); - fire(new WatchFile(configFile.toPath())); + + // Configuration store with file in /etc/opt (default) + File config = new File(cmdLine.getOptionValue('c', + "/etc/opt/" + APP_NAME.replace("-", "") + "/config.yaml")); + // Don't rely on night config to produce a good exception + // for this simple case + if (!Files.isReadable(config.toPath())) { + throw new IOException("Cannot read configuration file " + config); + } + attach(new YamlConfigurationStore(channel(), config, false)); + fire(new WatchFile(config.toPath())); } /** @@ -307,84 +259,57 @@ public class Runner extends Component { } /** - * Process the initial configuration. The initial configuration - * and any subsequent updates will be forwarded to other components - * only when the QMP connection is ready - * (see @link #onQmpConfigured(QmpConfigured)). + * On configuration update. * * @param event the event */ @Handler public void onConfigurationUpdate(ConfigurationUpdate event) { event.structured(componentPath()).ifPresent(c -> { - logger.fine(() -> "Runner configuratation updated"); - var newConf = yamlMapper.convertValue(c, Configuration.class); - - // Add some values from other sources to configuration - newConf.asOf = Instant.ofEpochSecond(configFile.lastModified()); - Path dsPath = configDir.resolve(DisplaySecret.PASSWORD); - newConf.hasDisplayPassword = dsPath.toFile().canRead(); - - // Special actions for initial configuration (startup) if (event instanceof InitialConfiguration) { - processInitialConfiguration(newConf); - } - - // Check if to be sent immediately or later - if (qmpConfigured) { - rep.fire(new ConfigureQemu(newConf, state)); - } else { - pendingConfig = newConf; + processInitialConfiguration(c); + return; } + logger.fine(() -> "Updating configuration"); + var newConf = yamlMapper.convertValue(c, Configuration.class); + rep.fire(new RunnerConfigurationUpdate(newConf, state)); }); } - @SuppressWarnings("PMD.LambdaCanBeMethodReference") - private void processInitialConfiguration(Configuration newConfig) { + private void processInitialConfiguration( + Map runnerConfiguration) { try { - if (!newConfig.check()) { + config = yamlMapper.convertValue(runnerConfiguration, + Configuration.class); + if (!config.check()) { // Invalid configuration, not used, problems already logged. - return; + config = null; } // Prepare firmware files and add to config - setFirmwarePaths(newConfig); + setFirmwarePaths(); // Obtain more context data from template - var tplData = dataFromTemplate(newConfig); - initialConfig = newConfig; - - // Configure - swtpmDefinition - = Optional.ofNullable(tplData.get(ProcessName.SWTPM)) - .map(d -> new CommandDefinition(ProcessName.SWTPM, d)) - .orElse(null); - logger.finest(() -> swtpmDefinition.toString()); - qemuDefinition = Optional.ofNullable(tplData.get(ProcessName.QEMU)) - .map(d -> new CommandDefinition(ProcessName.QEMU, d)) - .orElse(null); - logger.finest(() -> qemuDefinition.toString()); - cloudInitImgDefinition - = Optional.ofNullable(tplData.get(ProcessName.CLOUD_INIT_IMG)) - .map(d -> new CommandDefinition(ProcessName.CLOUD_INIT_IMG, - d)) - .orElse(null); - logger.finest(() -> cloudInitImgDefinition.toString()); + var tplData = dataFromTemplate(); + swtpmDefinition = Optional.ofNullable(tplData.get("swtpm")) + .map(d -> new CommandDefinition("swtpm", d)).orElse(null); + qemuDefinition = Optional.ofNullable(tplData.get("qemu")) + .map(d -> new CommandDefinition("qemu", d)).orElse(null); // Forward some values to child components - qemuMonitor.configure(initialConfig.monitorSocket, - initialConfig.vm.powerdownTimeout); - guestAgentClient.configureConnection(qemuDefinition.command, - "guest-agent-socket"); - vmopAgentClient.configureConnection(qemuDefinition.command, - "vmop-agent-socket"); + qemuMonitor.configure(config.monitorSocket, + config.vm.powerdownTimeout); } catch (IllegalArgumentException | IOException | TemplateException e) { logger.log(Level.SEVERE, e, () -> "Invalid configuration: " + e.getMessage()); + // Don't use default configuration + config = null; } } - private void setFirmwarePaths(Configuration config) throws IOException { + @SuppressWarnings({ "PMD.CognitiveComplexity", + "PMD.DataflowAnomalyAnalysis" }) + private void setFirmwarePaths() throws IOException { JsonNode firmware = defaults.path("firmware").path(config.vm.firmware); // Get file for firmware ROM JsonNode codePaths = firmware.path("rom"); @@ -395,12 +320,6 @@ public class Runner extends Component { break; } } - if (codePaths.iterator().hasNext() && config.firmwareRom == null) { - throw new IllegalArgumentException("No ROM found, candidates were: " - + StreamSupport.stream(codePaths.spliterator(), false) - .map(JsonNode::asText).collect(Collectors.joining(", "))); - } - // Get file for firmware vars, if necessary config.firmwareVars = config.dataDir.resolve(FW_VARS); if (!Files.exists(config.firmwareVars)) { @@ -414,7 +333,7 @@ public class Runner extends Component { } } - private JsonNode dataFromTemplate(Configuration config) + private JsonNode dataFromTemplate() throws IOException, TemplateNotFoundException, MalformedTemplateNameException, ParseException, TemplateException, JsonProcessingException, JsonMappingException { @@ -439,35 +358,20 @@ public class Runner extends Component { .map(Object::toString).orElse(null)); model.put("firmwareVars", Optional.ofNullable(config.firmwareVars) .map(Object::toString).orElse(null)); - model.put("hasDisplayPassword", config.hasDisplayPassword); - model.put("cloudInit", config.cloudInit); model.put("vm", config.vm); - logger.finest(() -> "Processing template with model: " + model); + if (Optional.ofNullable(config.vm.display) + .map(d -> d.spice).map(s -> s.ticket).isPresent()) { + model.put("ticketPath", config.runtimeDir.resolve("ticket.txt")); + } // Combine template and data and parse result // (tempting, but no need to use a pipe here) var fmTemplate = fmConfig.getTemplate(templatePath.toString()); StringWriter out = new StringWriter(); fmTemplate.process(model, out); - logger.finest(() -> "Result of processing template: " + out); return yamlMapper.readValue(out.toString(), JsonNode.class); } - /** - * Note ready state and send a {@link ConfigureQemu} event for - * any pending configuration (initial or change). - * - * @param event the event - */ - @Handler - public void onQmpConfigured(QmpConfigured event) { - qmpConfigured = true; - if (pendingConfig != null) { - rep.fire(new ConfigureQemu(pendingConfig, state)); - pendingConfig = null; - } - } - /** * Handle the start event. * @@ -475,7 +379,7 @@ public class Runner extends Component { */ @Handler(priority = 100) public void onStart(Start event) { - if (initialConfig == null) { + if (config == null) { // Missing configuration, fail event.cancel(true); fire(new Stop()); @@ -486,24 +390,25 @@ public class Runner extends Component { // https://github.com/kubernetes-client/java/issues/100 io.kubernetes.client.openapi.Configuration.setDefaultApiClient(null); - // Provide specific event pipeline to avoid concurrency. + // Prepare specific event pipeline to avoid concurrency. + rep = newEventPipeline(); event.setAssociated(EventPipeline.class, rep); try { // Store process id try (var pidFile = Files.newBufferedWriter( - initialConfig.runtimeDir.resolve("runner.pid"))) { + config.runtimeDir.resolve("runner.pid"))) { pidFile.write(ProcessHandle.current().pid() + "\n"); } // Files to watch for - Files.deleteIfExists(initialConfig.swtpmSocket); - fire(new WatchFile(initialConfig.swtpmSocket)); + Files.deleteIfExists(config.swtpmSocket); + fire(new WatchFile(config.swtpmSocket)); // Helper files - var ticket = Optional.ofNullable(initialConfig.vm.display) + var ticket = Optional.ofNullable(config.vm.display) .map(d -> d.spice).map(s -> s.ticket); if (ticket.isPresent()) { - Files.write(initialConfig.runtimeDir.resolve("ticket.txt"), + Files.write(config.runtimeDir.resolve("ticket.txt"), ticket.get().getBytes()); } } catch (IOException e) { @@ -520,73 +425,21 @@ public class Runner extends Component { */ @Handler public void onStarted(Started event) { - state = RunState.STARTING; + state = State.STARTING; rep.fire(new RunnerStateChange(state, "RunnerStarted", "Runner has been started")); - // Start first process(es) - qemuLatch.add(QemuPreps.Config); - if (initialConfig.vm.useTpm && swtpmDefinition != null) { + // Start first process + if (config.vm.useTpm && swtpmDefinition != null) { startProcess(swtpmDefinition); - qemuLatch.add(QemuPreps.Tpm); - } - if (initialConfig.cloudInit != null) { - generateCloudInitImg(initialConfig); - qemuLatch.add(QemuPreps.CloudInit); - } - mayBeStartQemu(QemuPreps.Config); - } - - @SuppressWarnings("PMD.AvoidSynchronizedStatement") - private void mayBeStartQemu(QemuPreps done) { - synchronized (qemuLatch) { - if (qemuLatch.isEmpty()) { - return; - } - qemuLatch.remove(done); - if (qemuLatch.isEmpty()) { - startProcess(qemuDefinition); - } - } - } - - private void generateCloudInitImg(Configuration config) { - try { - var cloudInitDir = config.dataDir.resolve("cloud-init"); - cloudInitDir.toFile().mkdir(); - try (var metaOut - = Files.newBufferedWriter(cloudInitDir.resolve("meta-data"))) { - if (config.cloudInit.metaData != null) { - yamlMapper.writer().writeValue(metaOut, - config.cloudInit.metaData); - } - } - try (var userOut - = Files.newBufferedWriter(cloudInitDir.resolve("user-data"))) { - userOut.write("#cloud-config\n"); - if (config.cloudInit.userData != null) { - yamlMapper.writer().writeValue(userOut, - config.cloudInit.userData); - } - } - if (config.cloudInit.networkConfig != null) { - try (var networkConfig = Files.newBufferedWriter( - cloudInitDir.resolve("network-config"))) { - yamlMapper.writer().writeValue(networkConfig, - config.cloudInit.networkConfig); - } - } - startProcess(cloudInitImgDefinition); - } catch (IOException e) { - logger.log(Level.SEVERE, e, - () -> "Cannot start runner: " + e.getMessage()); - fire(new Stop()); + return; } + startProcess(qemuDefinition); } private boolean startProcess(CommandDefinition toStart) { logger.info( () -> "Starting process: " + String.join(" ", toStart.command)); - rep.fire(new StartProcess(toStart.command) + fire(new StartProcess(toStart.command) .setAssociated(CommandDefinition.class, toStart)); return true; } @@ -600,9 +453,10 @@ public class Runner extends Component { @Handler public void onFileChanged(FileChanged event) { if (event.change() == Kind.CREATED - && event.path().equals(initialConfig.swtpmSocket)) { - // swtpm running, maybe start qemu - mayBeStartQemu(QemuPreps.Tpm); + && event.path().equals(config.swtpmSocket)) { + // swtpm running, start qemu + startProcess(qemuDefinition); + return; } } @@ -615,13 +469,15 @@ public class Runner extends Component { * @throws InterruptedException the interrupted exception */ @Handler + @SuppressWarnings({ "PMD.SwitchStmtsShouldHaveDefault", + "PMD.TooFewBranchesForASwitchStatement" }) public void onProcessStarted(ProcessStarted event, ProcessChannel channel) throws InterruptedException { event.startEvent().associated(CommandDefinition.class) .ifPresent(procDef -> { channel.setAssociated(CommandDefinition.class, procDef); try (var pidFile = Files.newBufferedWriter( - initialConfig.runtimeDir.resolve(procDef.name + ".pid"))) { + config.runtimeDir.resolve(procDef.name + ".pid"))) { pidFile.write(channel.process().toHandle().pid() + "\n"); } catch (IOException e) { throw new UndeclaredThrowableException(e); @@ -654,53 +510,30 @@ public class Runner extends Component { } /** - * Whenever a new QEMU configuration is available, check if it - * is supposed to trigger a reset. + * On monitor ready. * * @param event the event */ @Handler - public void onConfigureQemu(ConfigureQemu event) { - if (state.vmActive()) { - if (resetCounter != null - && event.configuration().resetCounter != null - && event.configuration().resetCounter > resetCounter) { - fire(new MonitorCommand(new QmpReset())); - } - resetCounter = event.configuration().resetCounter; - } + public void onQmpConfigured(QmpConfigured event) { + rep.fire(new RunnerConfigurationUpdate(config, state)); } /** - * As last step when handling a new configuration, check if - * QEMU is suspended after startup and should be continued. - * + * On configure qemu. + * * @param event the event */ @Handler(priority = -1000) - public void onConfigureQemuFinal(ConfigureQemu event) { - if (state == RunState.STARTING) { - state = RunState.BOOTING; + public void onConfigureQemu(RunnerConfigurationUpdate event) { + if (state == State.STARTING) { fire(new MonitorCommand(new QmpCont())); + state = State.RUNNING; rep.fire(new RunnerStateChange(state, "VmStarted", "Qemu has been configured and is continuing")); } } - /** - * Receiving the OSinfo means that the OS has been booted. - * - * @param event the event - */ - @Handler - public void onOsinfo(OsinfoEvent event) { - if (state == RunState.BOOTING) { - state = RunState.BOOTED; - rep.fire(new RunnerStateChange(state, "VmBooted", - "The VM has started the guest agent.")); - } - } - /** * On process exited. * @@ -710,43 +543,22 @@ public class Runner extends Component { @Handler public void onProcessExited(ProcessExited event, ProcessChannel channel) { channel.associated(CommandDefinition.class).ifPresent(procDef -> { - if (procDef.equals(cloudInitImgDefinition) - && event.exitValue() == 0) { - // Cloud-init ISO generation was successful. - mayBeStartQemu(QemuPreps.CloudInit); - return; - } - - // No other process(es) may exit during startup - if (state == RunState.STARTING) { + // No process(es) may exit during startup + if (state == State.STARTING) { logger.severe(() -> "Process " + procDef.name + " has exited with value " + event.exitValue() + " during startup."); rep.fire(new Stop()); return; } - - // No processes may exit while the VM is running normally - if (procDef.equals(qemuDefinition) && state.vmActive()) { - rep.fire(new Exit(event.exitValue())); + if (procDef.equals(qemuDefinition) && state == State.RUNNING) { + rep.fire(new Stop()); } logger.info(() -> "Process " + procDef.name + " has exited with value " + event.exitValue()); }); } - /** - * On exit. - * - * @param event the event - */ - @Handler(priority = 10_001) - public void onExit(Exit event) { - if (exitStatus == 0) { - exitStatus = event.exitStatus(); - } - } - /** * On stop. * @@ -754,9 +566,9 @@ public class Runner extends Component { */ @Handler(priority = 10_000) public void onStopFirst(Stop event) { - state = RunState.TERMINATING; + state = State.TERMINATING; rep.fire(new RunnerStateChange(state, "VmTerminating", - "The VM is being shut down", exitStatus != 0)); + "The VM is being shut down")); } /** @@ -766,13 +578,13 @@ public class Runner extends Component { */ @Handler(priority = -10_000) public void onStopLast(Stop event) { - state = RunState.STOPPED; + state = State.STOPPED; rep.fire(new RunnerStateChange(state, "VmStopped", "The VM has been shut down")); } private void shutdown() { - if (!Set.of(RunState.TERMINATING, RunState.STOPPED).contains(state)) { + if (state != State.TERMINATING) { fire(new Stop()); } try { @@ -781,7 +593,7 @@ public class Runner extends Component { logger.log(Level.WARNING, e, () -> "Proper shutdown failed."); } - Optional.ofNullable(initialConfig).map(c -> c.runtimeDir) + Optional.ofNullable(config).map(c -> c.runtimeDir) .ifPresent(runtimeDir -> { try { Files.walk(runtimeDir).sorted(Comparator.reverseOrder()) @@ -797,7 +609,7 @@ public class Runner extends Component { static { try { InputStream props; - var path = FsdUtils.findConfigFile(APP_NAME.replace("-", ""), + var path = FsdUtils.findConfigFile(Runner.APP_NAME.replace("-", ""), "logging.properties"); if (path.isPresent()) { props = Files.newInputStream(path.get()); @@ -805,10 +617,6 @@ public class Runner extends Component { props = Runner.class.getResourceAsStream("logging.properties"); } LogManager.getLogManager().readConfiguration(props); - Logger.getLogger(Runner.class.getName()).log(Level.CONFIG, - () -> path.isPresent() - ? "Using logging configuration from " + path.get() - : "Using default logging configuration"); } catch (IOException e) { e.printStackTrace(); } @@ -843,11 +651,6 @@ public class Runner extends Component { // Start the application Components.start(app); - - // Wait for (regular) termination - Components.awaitExhaustion(); - System.exit(exitStatus); - } catch (IOException | InterruptedException | org.apache.commons.cli.ParseException e) { Logger.getLogger(Runner.class.getName()).log(Level.SEVERE, e, diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java index 127c070..19e252f 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java @@ -1,6 +1,6 @@ /* * VM-Operator - * Copyright (C) 2023,2025 Michael N. Lipp + * 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 @@ -18,71 +18,54 @@ package org.jdrupes.vmoperator.runner.qemu; -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.apimachinery.GroupVersionKind; import io.kubernetes.client.custom.Quantity; import io.kubernetes.client.custom.Quantity.Format; -import io.kubernetes.client.custom.V1Patch; import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.openapi.JSON; -import io.kubernetes.client.openapi.models.EventsV1Event; +import io.kubernetes.client.openapi.apis.ApisApi; +import io.kubernetes.client.openapi.apis.CustomObjectsApi; +import io.kubernetes.client.openapi.models.V1APIGroup; +import io.kubernetes.client.openapi.models.V1GroupVersionForDiscovery; +import io.kubernetes.client.util.Config; +import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi; +import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; import java.io.IOException; import java.math.BigDecimal; -import java.math.BigInteger; +import java.nio.file.Files; +import java.nio.file.Path; import java.time.Instant; +import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.logging.Level; -import static org.jdrupes.vmoperator.common.Constants.APP_NAME; -import org.jdrupes.vmoperator.common.Constants.Crd; -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.common.K8s; -import org.jdrupes.vmoperator.common.VmDefinition; -import org.jdrupes.vmoperator.common.VmDefinitionStub; +import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP; +import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM; import org.jdrupes.vmoperator.runner.qemu.events.BalloonChangeEvent; -import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu; -import org.jdrupes.vmoperator.runner.qemu.events.DisplayPasswordChanged; -import org.jdrupes.vmoperator.runner.qemu.events.Exit; import org.jdrupes.vmoperator.runner.qemu.events.HotpluggableCpuStatus; -import org.jdrupes.vmoperator.runner.qemu.events.OsinfoEvent; +import org.jdrupes.vmoperator.runner.qemu.events.RunnerConfigurationUpdate; import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange; -import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.RunState; -import org.jdrupes.vmoperator.runner.qemu.events.ShutdownEvent; -import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentConnected; -import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLoggedIn; -import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLoggedOut; +import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State; import org.jdrupes.vmoperator.util.GsonPtr; import org.jgrapes.core.Channel; -import org.jgrapes.core.Components; -import org.jgrapes.core.Components.Timer; +import org.jgrapes.core.Component; import org.jgrapes.core.annotation.Handler; import org.jgrapes.core.events.HandlingError; import org.jgrapes.core.events.Start; +import org.jgrapes.util.events.ConfigurationUpdate; +import org.jgrapes.util.events.InitialConfiguration; /** * Updates the CR status. */ -@SuppressWarnings({ "PMD.CouplingBetweenObjects" }) -public class StatusUpdater extends VmDefUpdater { +public class StatusUpdater extends Component { - @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 static final Set RUNNING_STATES + = Set.of(State.RUNNING, State.TERMINATING); - private boolean guestShutdownStops; - private boolean shutdownByGuest; - private VmDefinitionStub vmStub; - private String loggedInUser; - private BigInteger lastRamValue; - private Instant lastRamChange; - private Timer balloonTimer; - private BigInteger targetRamValue; + private String namespace; + private String vmName; + private DynamicKubernetesApi vmCrApi; + private long observedGeneration; /** * Instantiates a new status updater. @@ -91,7 +74,6 @@ public class StatusUpdater extends VmDefUpdater { */ public StatusUpdater(Channel componentChannel) { super(componentChannel); - attach(new ConsoleTracker(componentChannel)); } /** @@ -108,6 +90,43 @@ public class StatusUpdater extends VmDefUpdater { } } + /** + * On configuration update. + * + * @param event the event + */ + @Handler + @SuppressWarnings("unchecked") + public void onConfigurationUpdate(ConfigurationUpdate event) { + event.structured("/Runner").ifPresent(c -> { + if (event instanceof InitialConfiguration) { + namespace = (String) c.get("namespace"); + updateNamespace(); + vmName = Optional.ofNullable((Map) c.get("vm")) + .map(vm -> vm.get("name")).orElse(null); + } + }); + } + + private void updateNamespace() { + if (namespace == null) { + var path = Path + .of("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); + if (Files.isReadable(path)) { + try { + namespace = Files.lines(path).findFirst().orElse(null); + } catch (IOException e) { + logger.log(Level.WARNING, e, + () -> "Cannot read namespace."); + } + } + } + if (namespace == null) { + logger.warning(() -> "Namespace is unknown, some functions" + + " won't be available."); + } + } + /** * Handle the start event. * @@ -116,32 +135,45 @@ public class StatusUpdater extends VmDefUpdater { * @throws ApiException */ @Handler - public void onStart(Start event) { + @SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", + "PMD.AvoidInstantiatingObjectsInLoops", "PMD.AvoidDuplicateLiterals" }) + public void onStart(Start event) throws IOException, ApiException { if (namespace == null) { return; } - try { - vmStub = VmDefinitionStub.get(apiClient, - new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM), - namespace, vmName); - var vmDef = vmStub.model().orElse(null); - if (vmDef == null) { - return; + var client = Config.defaultClient(); + var apis = new ApisApi(client).getAPIVersions(); + var crdVersions = apis.getGroups().stream() + .filter(g -> g.getName().equals(VM_OP_GROUP)).findFirst() + .map(V1APIGroup::getVersions).stream().flatMap(l -> l.stream()) + .map(V1GroupVersionForDiscovery::getVersion).toList(); + var coa = new CustomObjectsApi(client); + for (var crdVersion : crdVersions) { + var crdApiRes = coa.getAPIResources(VM_OP_GROUP, + crdVersion).getResources().stream() + .filter(r -> VM_OP_KIND_VM.equals(r.getKind())).findFirst(); + if (crdApiRes.isEmpty()) { + continue; + } + var crApi = new DynamicKubernetesApi(VM_OP_GROUP, + crdVersion, crdApiRes.get().getName(), client); + var vmCr = crApi.get(namespace, vmName); + if (vmCr.isSuccess()) { + vmCrApi = crApi; + observedGeneration + = vmCr.getObject().getMetadata().getGeneration(); + break; } - vmStub.updateStatus(from -> { - JsonObject status = from.statusJson(); - status.addProperty(Status.RUNNER_VERSION, Optional.ofNullable( - Runner.class.getPackage().getImplementationVersion()) - .orElse("(unknown)")); - status.remove(Status.LOGGED_IN_USER); - return status; - }); - } catch (ApiException e) { - logger.log(Level.SEVERE, e, - () -> "Cannot access VM object, terminating."); - event.cancel(true); - fire(new Exit(1)); } + if (vmCrApi == null) { + logger.warning(() -> "Cannot find VM's CR, status will not" + + " be updated."); + } + } + + @SuppressWarnings("PMD.AvoidDuplicateLiterals") + private JsonObject currentStatus(DynamicKubernetesObject vmCr) { + return vmCr.getRaw().getAsJsonObject("status").deepCopy(); } /** @@ -151,28 +183,25 @@ public class StatusUpdater extends VmDefUpdater { * @throws ApiException */ @Handler - public void onConfigureQemu(ConfigureQemu event) + public void onRunnerConfigurationUpdate(RunnerConfigurationUpdate event) throws ApiException { - guestShutdownStops = event.configuration().guestShutdownStops; - loggedInUser = event.configuration().vm.display.loggedInUser; - targetRamValue = event.configuration().vm.currentRam; - - // Remainder applies only if we have a connection to k8s. - if (vmStub == null) { + if (vmCrApi == null) { return; } - vmStub.updateStatus(from -> { - JsonObject status = from.statusJson(); - if (!event.configuration().hasDisplayPassword) { - status.addProperty(Status.DISPLAY_PASSWORD_SERIAL, -1); - } + // A change of the runner configuration is typically caused + // by a new version of the CR. So we observe the new CR. + var vmCr = vmCrApi.get(namespace, vmName).throwsApiException() + .getObject(); + if (vmCr.getMetadata().getGeneration() == observedGeneration) { + return; + } + vmCrApi.updateStatus(vmCr, from -> { + JsonObject status = currentStatus(from); status.getAsJsonArray("conditions").asList().stream() - .map(cond -> (JsonObject) cond) - .filter(cond -> Condition.RUNNING + .map(cond -> (JsonObject) cond).filter(cond -> "Running" .equals(cond.get("type").getAsString())) .forEach(cond -> cond.addProperty("observedGeneration", from.getMetadata().getGeneration())); - updateUserLoggedIn(from); return status; }); } @@ -184,146 +213,76 @@ public class StatusUpdater extends VmDefUpdater { * @throws ApiException */ @Handler - @SuppressWarnings({ "PMD.AssignmentInOperand" }) public void onRunnerStateChanged(RunnerStateChange event) throws ApiException { - VmDefinition vmDef; - if (vmStub == null || (vmDef = vmStub.model().orElse(null)) == null) { + if (vmCrApi == null) { return; } - vmStub.updateStatus(from -> { - boolean running = event.runState().vmRunning(); - updateCondition(vmDef, Condition.RUNNING, running, event.reason(), - event.message()); - JsonObject status = updateCondition(vmDef, Condition.BOOTED, - event.runState() == RunState.BOOTED, event.reason(), - event.message()); - if (event.runState() == RunState.STARTING) { - status.addProperty(Status.RAM, GsonPtr.to(from.data()) + var vmCr = vmCrApi.get(namespace, vmName).throwsApiException() + .getObject(); + vmCrApi.updateStatus(vmCr, from -> { + JsonObject status = currentStatus(from); + status.getAsJsonArray("conditions").asList().stream() + .map(cond -> (JsonObject) cond) + .forEach(cond -> { + if ("Running".equals(cond.get("type").getAsString())) { + updateRunningCondition(event, from, cond); + } + }); + if (event.state() == State.STARTING) { + status.addProperty("ram", GsonPtr.to(from.getRaw()) .getAsString("spec", "vm", "maximumRam").orElse("0")); - status.addProperty(Status.CPUS, 1); - } else if (event.runState() == RunState.STOPPED) { - status.addProperty(Status.RAM, "0"); - status.addProperty(Status.CPUS, 0); - status.remove(Status.LOGGED_IN_USER); - } - - if (!running) { - // In case console connection was still present - status.addProperty(Status.CONSOLE_CLIENT, ""); - updateCondition(from, Condition.CONSOLE_CONNECTED, false, - "VmStopped", - "The VM is not running"); - - // In case we had an irregular shutdown - updateCondition(from, Condition.USER_LOGGED_IN, false, - "VmStopped", "The VM is not running"); - status.remove(Status.OSINFO); - updateCondition(vmDef, "VmopAgentConnected", false, "VmStopped", - "The VM is not running"); + status.addProperty("cpus", 1); + } else if (event.state() == State.STOPPED) { + status.addProperty("ram", "0"); + status.addProperty("cpus", 0); } return status; - }, vmDef); - - // Maybe stop VM - if (event.runState() == RunState.TERMINATING && !event.failed() - && guestShutdownStops && shutdownByGuest) { - logger.info(() -> "Stopping VM because of shutdown by guest."); - var res = vmStub.patch(V1Patch.PATCH_FORMAT_JSON_PATCH, - new V1Patch("[{\"op\": \"replace\", \"path\": \"/spec/vm/state" - + "\", \"value\": \"Stopped\"}]"), - apiClient.defaultPatchOptions()); - if (!res.isPresent()) { - logger.warning( - () -> "Cannot patch pod annotations for: " + vmStub.name()); - } - } - - // Log event - var evt = new EventsV1Event() - .reportingController(Crd.GROUP + "/" + APP_NAME) - .action("StatusUpdate").reason(event.reason()) - .note(event.message()); - K8s.createEvent(apiClient, vmDef, evt); + }).throwsApiException(); } - private void updateUserLoggedIn(VmDefinition from) { - if (loggedInUser == null) { - updateCondition(from, Condition.USER_LOGGED_IN, false, - Reason.NOT_REQUESTED, "No user to be logged in"); - return; + private void updateRunningCondition(RunnerStateChange event, + DynamicKubernetesObject from, JsonObject cond) { + boolean reportedRunning + = "True".equals(cond.get("status").getAsString()); + if (RUNNING_STATES.contains(event.state()) + && !reportedRunning) { + cond.addProperty("status", "True"); + cond.addProperty("lastTransitionTime", + Instant.now().toString()); } - if (!from.conditionStatus(Condition.VMOP_AGENT).orElse(false)) { - updateCondition(from, Condition.USER_LOGGED_IN, false, - "VmopAgentDisconnected", "Waiting for VMOP agent to connect"); - return; + if (!RUNNING_STATES.contains(event.state()) + && reportedRunning) { + cond.addProperty("status", "False"); + cond.addProperty("lastTransitionTime", + Instant.now().toString()); } - if (!from.fromStatus(Status.LOGGED_IN_USER).map(loggedInUser::equals) - .orElse(false)) { - updateCondition(from, Condition.USER_LOGGED_IN, false, - "Processing", "Waiting for user to be logged in"); - } - updateCondition(from, Condition.USER_LOGGED_IN, true, - Reason.LOGGED_IN, "User is logged in"); + cond.addProperty("reason", event.reason()); + cond.addProperty("message", event.message()); + cond.addProperty("observedGeneration", + from.getMetadata().getGeneration()); } /** - * Update the current RAM size in the status. Balloon changes happen - * more than once every second during changes. While this is nice - * to watch, this puts a heavy load on the system. Therefore we - * only update the status once every 15 seconds or when the target - * value is reached. + * On ballon change. * * @param event the event * @throws ApiException */ @Handler public void onBallonChange(BalloonChangeEvent event) throws ApiException { - if (vmStub == null) { + if (vmCrApi == null) { return; } - Instant now = Instant.now(); - if (lastRamChange == null - || lastRamChange.isBefore(now.minusSeconds(15)) - || event.size().equals(targetRamValue)) { - if (balloonTimer != null) { - balloonTimer.cancel(); - balloonTimer = null; - } - lastRamChange = now; - lastRamValue = event.size(); - updateRam(); - return; - } - - // Save for later processing and maybe start timer - lastRamChange = now; - lastRamValue = event.size(); - if (balloonTimer != null) { - return; - } - final var pipeline = activeEventPipeline(); - balloonTimer = Components.schedule(t -> { - pipeline.submit("Update RAM size", () -> { - try { - updateRam(); - } catch (ApiException e) { - logger.log(Level.WARNING, e, - () -> "Failed to update ram size: " + e.getMessage()); - } - balloonTimer = null; - }); - }, now.plusSeconds(15)); - } - - private void updateRam() throws ApiException { - vmStub.updateStatus(from -> { - JsonObject status = from.statusJson(); - status.addProperty(Status.RAM, - new Quantity(new BigDecimal(lastRamValue), Format.BINARY_SI) + var vmCr = vmCrApi.get(namespace, vmName).throwsApiException() + .getObject(); + vmCrApi.updateStatus(vmCr, from -> { + JsonObject status = currentStatus(from); + status.addProperty("ram", + new Quantity(new BigDecimal(event.size()), Format.BINARY_SI) .toSuffixedString()); return status; - }); + }).throwsApiException(); } /** @@ -334,116 +293,15 @@ public class StatusUpdater extends VmDefUpdater { */ @Handler public void onCpuChange(HotpluggableCpuStatus event) throws ApiException { - if (vmStub == null) { + if (vmCrApi == null) { return; } - vmStub.updateStatus(from -> { - JsonObject status = from.statusJson(); - status.addProperty(Status.CPUS, event.usedCpus().size()); + var vmCr = vmCrApi.get(namespace, vmName).throwsApiException() + .getObject(); + vmCrApi.updateStatus(vmCr, from -> { + JsonObject status = currentStatus(from); + status.addProperty("cpus", event.usedCpus().size()); return status; - }); - } - - /** - * On ballon change. - * - * @param event the event - * @throws ApiException - */ - @Handler - public void onDisplayPasswordChanged(DisplayPasswordChanged event) - throws ApiException { - if (vmStub == null) { - return; - } - vmStub.updateStatus(from -> { - JsonObject status = from.statusJson(); - status.addProperty(Status.DISPLAY_PASSWORD_SERIAL, - status.get(Status.DISPLAY_PASSWORD_SERIAL).getAsLong() + 1); - return status; - }); - } - - /** - * On shutdown. - * - * @param event the event - * @throws ApiException the api exception - */ - @Handler - public void onShutdown(ShutdownEvent event) throws ApiException { - shutdownByGuest = event.byGuest(); - } - - /** - * On osinfo. - * - * @param event the event - * @throws ApiException - */ - @Handler - public void onOsinfo(OsinfoEvent event) throws ApiException { - if (vmStub == null) { - return; - } - var asGson = gson.toJsonTree( - objectMapper.convertValue(event.osinfo(), Object.class)); - vmStub.updateStatus(from -> { - JsonObject status = from.statusJson(); - status.add(Status.OSINFO, asGson); - return status; - }); - - } - - /** - * @param event the event - * @throws ApiException - */ - @Handler - @SuppressWarnings("PMD.AssignmentInOperand") - public void onVmopAgentConnected(VmopAgentConnected event) - throws ApiException { - VmDefinition vmDef; - if (vmStub == null || (vmDef = vmStub.model().orElse(null)) == null) { - return; - } - vmStub.updateStatus(from -> { - var status = updateCondition(vmDef, "VmopAgentConnected", - true, "VmopAgentStarted", "The VM operator agent is running"); - updateUserLoggedIn(from); - return status; - }, vmDef); - } - - /** - * @param event the event - * @throws ApiException - */ - @Handler - public void onVmopAgentLoggedIn(VmopAgentLoggedIn event) - throws ApiException { - vmStub.updateStatus(from -> { - JsonObject status = from.statusJson(); - status.addProperty(Status.LOGGED_IN_USER, - event.triggering().user()); - updateUserLoggedIn(from); - return status; - }); - } - - /** - * @param event the event - * @throws ApiException - */ - @Handler - public void onVmopAgentLoggedOut(VmopAgentLoggedOut event) - throws ApiException { - vmStub.updateStatus(from -> { - JsonObject status = from.statusJson(); - status.remove(Status.LOGGED_IN_USER); - updateUserLoggedIn(from); - return status; - }); + }).throwsApiException(); } } diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/VmDefUpdater.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/VmDefUpdater.java deleted file mode 100644 index 406a0bc..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/VmDefUpdater.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu; - -import com.google.gson.JsonObject; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.Instant; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.logging.Level; -import java.util.stream.Collectors; -import org.jdrupes.vmoperator.common.K8sClient; -import org.jdrupes.vmoperator.common.K8sGenericStub; -import org.jdrupes.vmoperator.common.VmDefinition; -import org.jdrupes.vmoperator.runner.qemu.events.Exit; -import org.jgrapes.core.Channel; -import org.jgrapes.core.Component; -import org.jgrapes.core.annotation.Handler; -import org.jgrapes.util.events.ConfigurationUpdate; -import org.jgrapes.util.events.InitialConfiguration; - -/** - * Updates the CR status. - */ -public class VmDefUpdater extends Component { - - protected String namespace; - protected String vmName; - protected K8sClient apiClient; - - /** - * Instantiates a new status updater. - * - * @param componentChannel the component channel - * @throws IOException - */ - @SuppressWarnings("PMD.ConstructorCallsOverridableMethod") - public VmDefUpdater(Channel componentChannel) { - super(componentChannel); - if (apiClient == null) { - try { - apiClient = new K8sClient(); - io.kubernetes.client.openapi.Configuration - .setDefaultApiClient(apiClient); - } catch (IOException e) { - logger.log(Level.SEVERE, e, - () -> "Cannot access events API, terminating."); - fire(new Exit(1)); - } - } - } - - /** - * On configuration update. - * - * @param event the event - */ - @Handler - @SuppressWarnings("unchecked") - public void onConfigurationUpdate(ConfigurationUpdate event) { - event.structured("/Runner").ifPresent(c -> { - if (event instanceof InitialConfiguration) { - namespace = (String) c.get("namespace"); - updateNamespace(); - vmName = Optional.ofNullable((Map) c.get("vm")) - .map(vm -> vm.get("name")).orElse(null); - } - }); - } - - private void updateNamespace() { - if (namespace == null) { - var path = Path - .of("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); - if (Files.isReadable(path)) { - try { - namespace = Files.lines(path).findFirst().orElse(null); - } catch (IOException e) { - logger.log(Level.WARNING, e, - () -> "Cannot read namespace."); - } - } - } - if (namespace == null) { - logger.warning(() -> "Namespace is unknown, some functions" - + " won't be available."); - } - } - - /** - * Update condition. The `from` VM definition is used to determine the - * observed generation and the current status. This method is intended - * to be called in the function passed to - * {@link K8sGenericStub#updateStatus}. - * - * @param from the VM definition - * @param type the condition type - * @param state the new state - * @param reason the reason for the change - * @param message the message - * @return the updated status - */ - protected JsonObject updateCondition(VmDefinition from, String type, - boolean state, String reason, String message) { - JsonObject status = from.statusJson(); - // Avoid redundant updates, as this may be called several times - var current = status.getAsJsonArray("conditions").asList().stream() - .map(cond -> (JsonObject) cond) - .filter(cond -> type.equals(cond.get("type").getAsString())) - .findFirst(); - var stateUnchanged = current.map(c -> c.get("status").getAsString()) - .map("True"::equals).map(s -> s == state).orElse(false); - if (stateUnchanged - && current.map(c -> c.get("reason").getAsString()) - .map(reason::equals).orElse(false) - && current.map(c -> c.get("observedGeneration").getAsLong()) - .map(from.getMetadata().getGeneration()::equals) - .orElse(false)) { - return status; - } - - // Do update - final var condition = new HashMap<>(Map.of("type", type, - "status", state ? "True" : "False", - "observedGeneration", from.getMetadata().getGeneration(), - "reason", reason, - "lastTransitionTime", stateUnchanged - ? current.get().get("lastTransitionTime").getAsString() - : Instant.now().toString())); - if (message != null) { - condition.put("message", message); - } - List toReplace = new ArrayList<>(List.of(condition)); - List newConds - = status.getAsJsonArray("conditions").asList().stream() - .map(cond -> (JsonObject) cond) - .map(cond -> type.equals(cond.get("type").getAsString()) - ? toReplace.remove(0) - : cond) - .collect(Collectors.toCollection(() -> new ArrayList<>())); - newConds.addAll(toReplace); - status.add("conditions", - apiClient.getJSON().getGson().toJsonTree(newConds)); - return status; - } -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/VmopAgentClient.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/VmopAgentClient.java deleted file mode 100644 index a940d73..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/VmopAgentClient.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * 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.runner.qemu; - -import java.io.IOException; -import java.util.Deque; -import java.util.concurrent.ConcurrentLinkedDeque; -import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentConnected; -import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLogIn; -import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLogOut; -import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLoggedIn; -import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLoggedOut; -import org.jgrapes.core.Channel; -import org.jgrapes.core.Event; -import org.jgrapes.core.annotation.Handler; - -/** - * A component that handles the communication over the vmop agent - * socket. - * - * If the log level for this class is set to fine, the messages - * exchanged on the socket are logged. - */ -public class VmopAgentClient extends AgentConnector { - - private final Deque> executing = new ConcurrentLinkedDeque<>(); - - /** - * Instantiates a new VM operator agent client. - * - * @param componentChannel the component channel - * @throws IOException Signals that an I/O exception has occurred. - */ - public VmopAgentClient(Channel componentChannel) throws IOException { - super(componentChannel); - } - - /** - * On vmop agent login. - * - * @param event the event - * @throws IOException Signals that an I/O exception has occurred. - */ - @Handler - public void onVmopAgentLogIn(VmopAgentLogIn event) throws IOException { - if (writer().isPresent()) { - logger.fine(() -> "Vmop agent handles:" + event); - executing.add(event); - logger.finer(() -> "vmop agent(out): login " + event.user()); - sendCommand("login " + event.user()); - } else { - logger - .warning(() -> "No vmop agent connection for sending " + event); - } - } - - /** - * On vmop agent logout. - * - * @param event the event - * @throws IOException Signals that an I/O exception has occurred. - */ - @Handler - public void onVmopAgentLogout(VmopAgentLogOut event) throws IOException { - if (writer().isPresent()) { - logger.fine(() -> "Vmop agent handles:" + event); - executing.add(event); - logger.finer(() -> "vmop agent(out): logout"); - sendCommand("logout"); - } - } - - @Override - @SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition" }) - protected void processInput(String line) throws IOException { - logger.finer(() -> "vmop agent(in): " + line); - - // Check validity - if (line.isEmpty() || !Character.isDigit(line.charAt(0))) { - logger.warning(() -> "Illegal vmop agent response: " + line); - return; - } - - // Check positive responses - if (line.startsWith("220 ")) { - var evt = new VmopAgentConnected(); - logger.fine(() -> "Vmop agent triggers " + evt); - rep().fire(evt); - return; - } - if (line.startsWith("201 ")) { - Event cmd = executing.pop(); - if (cmd instanceof VmopAgentLogIn login) { - var evt = new VmopAgentLoggedIn(login); - logger.fine(() -> "Vmop agent triggers " + evt); - rep().fire(evt); - } else { - logger.severe(() -> "Response " + line - + " does not match executing command " + cmd); - } - return; - } - if (line.startsWith("202 ")) { - Event cmd = executing.pop(); - if (cmd instanceof VmopAgentLogOut logout) { - var evt = new VmopAgentLoggedOut(logout); - logger.fine(() -> "Vmop agent triggers " + evt); - rep().fire(evt); - } else { - logger.severe(() -> "Response " + line - + "does not match executing command " + cmd); - } - return; - } - - // Ignore unhandled continuations - if (line.charAt(0) == '1') { - return; - } - - // Error - logger.warning(() -> "Error response from vmop agent: " + line); - executing.pop(); - } - -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpAddCpu.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpAddCpu.java index 86d92f6..e77a984 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpAddCpu.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpAddCpu.java @@ -47,7 +47,7 @@ public class QmpAddCpu extends QmpCommand { cmd.put("execute", "device_add"); ObjectNode args = mapper.createObjectNode(); cmd.set("arguments", args); - args.setAll((ObjectNode) unused.get("props")); + args.setAll((ObjectNode) (unused.get("props"))); args.set("driver", unused.get("type")); args.put("id", cpuId); return cmd; diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpCapabilities.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpCapabilities.java index 918b7d5..ffd6ca6 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpCapabilities.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpCapabilities.java @@ -25,7 +25,8 @@ import com.fasterxml.jackson.databind.JsonNode; */ public class QmpCapabilities extends QmpCommand { - @SuppressWarnings({ "PMD.FieldNamingConventions" }) + @SuppressWarnings({ "PMD.FieldNamingConventions", + "PMD.VariableNamingConventions" }) private static final JsonNode jsonTemplate = parseJson("{ \"execute\": \"qmp_capabilities\" }"); diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpChangeMedium.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpChangeMedium.java index b60b619..158a318 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpChangeMedium.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpChangeMedium.java @@ -27,7 +27,8 @@ import com.fasterxml.jackson.databind.node.ObjectNode; */ public class QmpChangeMedium extends QmpCommand { - @SuppressWarnings({ "PMD.FieldNamingConventions" }) + @SuppressWarnings({ "PMD.FieldNamingConventions", + "PMD.VariableNamingConventions" }) private static final JsonNode jsonTemplate = parseJson("{ \"execute\": \"blockdev-change-medium\",\"arguments\": {" + "\"id\": \"\",\"filename\": \"\",\"format\": \"raw\"," diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpCommand.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpCommand.java index 0db58e2..8a03ab0 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpCommand.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpCommand.java @@ -18,7 +18,6 @@ package org.jdrupes.vmoperator.runner.qemu.commands; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; @@ -30,7 +29,8 @@ import java.util.logging.Logger; */ public abstract class QmpCommand { - @SuppressWarnings({ "PMD.FieldNamingConventions" }) + @SuppressWarnings({ "PMD.FieldNamingConventions", + "PMD.VariableNamingConventions" }) protected static final ObjectMapper mapper = new ObjectMapper(); /** @@ -55,30 +55,4 @@ public abstract class QmpCommand { * @return the json node */ public abstract JsonNode toJson(); - - /** - * Returns the string representation. - * - * @return the string - * @throws JsonProcessingException the JSON processing exception - */ - public String asText() throws JsonProcessingException { - return mapper.writeValueAsString(toJson()); - } - - /** - * Calls {@link #asText()} but suppresses the - * {@link JsonProcessingException}. - * - * @return the string - */ - @Override - public String toString() { - try { - return asText(); - } catch (JsonProcessingException e) { - return "(no string representation)"; - } - } - } diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpCont.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpCont.java index 0e06e34..7b1abbd 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpCont.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpCont.java @@ -25,7 +25,8 @@ import com.fasterxml.jackson.databind.JsonNode; */ public class QmpCont extends QmpCommand { - @SuppressWarnings({ "PMD.FieldNamingConventions" }) + @SuppressWarnings({ "PMD.FieldNamingConventions", + "PMD.VariableNamingConventions" }) private static final JsonNode jsonTemplate = parseJson("{ \"execute\": \"cont\" }"); diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpDelCpu.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpDelCpu.java index a97e6c6..46fba32 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpDelCpu.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpDelCpu.java @@ -27,7 +27,8 @@ import com.fasterxml.jackson.databind.node.ObjectNode; */ public class QmpDelCpu extends QmpCommand { - @SuppressWarnings({ "PMD.FieldNamingConventions" }) + @SuppressWarnings({ "PMD.FieldNamingConventions", + "PMD.VariableNamingConventions" }) private static final JsonNode jsonTemplate = parseJson("{ \"execute\": \"device_del\", " + "\"arguments\": " + "{ \"id\": 0 } }"); diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpGuestGetOsinfo.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpGuestGetOsinfo.java deleted file mode 100644 index cf4ba72..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpGuestGetOsinfo.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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.runner.qemu.commands; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; - -/** - * A {@link QmpCommand} that pings the guest agent. - */ -public class QmpGuestGetOsinfo extends QmpCommand { - - @Override - public JsonNode toJson() { - ObjectNode cmd = mapper.createObjectNode(); - cmd.put("execute", "guest-get-osinfo"); - return cmd; - } - - @Override - public String toString() { - return "QmpGuestGetOsinfo()"; - } - -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpGuestInfo.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpGuestInfo.java deleted file mode 100644 index 75fdf73..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpGuestInfo.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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.runner.qemu.commands; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; - -/** - * A {@link QmpCommand} that requests the guest info. - */ -public class QmpGuestInfo extends QmpCommand { - - @Override - public JsonNode toJson() { - ObjectNode cmd = mapper.createObjectNode(); - cmd.put("execute", "guest-info"); - return cmd; - } - - @Override - public String toString() { - return "QmpGuestInfo()"; - } - -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpGuestPing.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpGuestPing.java deleted file mode 100644 index 257c838..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpGuestPing.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu.commands; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; - -/** - * A {@link QmpCommand} that pings the guest agent. - */ -public class QmpGuestPing extends QmpCommand { - - @Override - public JsonNode toJson() { - ObjectNode cmd = mapper.createObjectNode(); - cmd.put("execute", "guest-ping"); - return cmd; - } - - @Override - public String toString() { - return "QmpGuestPing()"; - } - -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpGuestPowerdown.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpGuestPowerdown.java deleted file mode 100644 index 04110a5..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpGuestPowerdown.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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.runner.qemu.commands; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; - -/** - * A {@link QmpCommand} that powers down the guest. - */ -public class QmpGuestPowerdown extends QmpCommand { - - @Override - public JsonNode toJson() { - ObjectNode cmd = mapper.createObjectNode(); - cmd.put("execute", "guest-shutdown"); - return cmd; - } - - @Override - public String toString() { - return "QmpGuestPowerdown()"; - } - -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpOpenTray.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpOpenTray.java index 88a392c..2f9ad55 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpOpenTray.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpOpenTray.java @@ -27,7 +27,8 @@ import com.fasterxml.jackson.databind.node.ObjectNode; */ public class QmpOpenTray extends QmpCommand { - @SuppressWarnings({ "PMD.FieldNamingConventions" }) + @SuppressWarnings({ "PMD.FieldNamingConventions", + "PMD.VariableNamingConventions" }) private static final JsonNode jsonTemplate = parseJson("{ \"execute\": \"blockdev-open-tray\",\"arguments\": {" + "\"id\": \"\" } }"); diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpPowerdown.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpPowerdown.java index dfb7d96..108a355 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpPowerdown.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpPowerdown.java @@ -25,7 +25,8 @@ import com.fasterxml.jackson.databind.JsonNode; */ public class QmpPowerdown extends QmpCommand { - @SuppressWarnings({ "PMD.FieldNamingConventions" }) + @SuppressWarnings({ "PMD.FieldNamingConventions", + "PMD.VariableNamingConventions" }) private static final JsonNode jsonTemplate = parseJson("{ \"execute\": \"system_powerdown\" }"); diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpQueryHotpluggableCpus.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpQueryHotpluggableCpus.java index d4fb5cc..6f87d10 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpQueryHotpluggableCpus.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpQueryHotpluggableCpus.java @@ -25,7 +25,8 @@ import com.fasterxml.jackson.databind.JsonNode; */ public class QmpQueryHotpluggableCpus extends QmpCommand { - @SuppressWarnings({ "PMD.FieldNamingConventions" }) + @SuppressWarnings({ "PMD.FieldNamingConventions", + "PMD.VariableNamingConventions" }) private static final JsonNode jsonTemplate = parseJson( "{\"execute\":\"query-hotpluggable-cpus\",\"arguments\":{}}"); diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpRemoveMedium.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpRemoveMedium.java index 71360cf..cc74555 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpRemoveMedium.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpRemoveMedium.java @@ -27,7 +27,8 @@ import com.fasterxml.jackson.databind.node.ObjectNode; */ public class QmpRemoveMedium extends QmpCommand { - @SuppressWarnings({ "PMD.FieldNamingConventions" }) + @SuppressWarnings({ "PMD.FieldNamingConventions", + "PMD.VariableNamingConventions" }) private static final JsonNode jsonTemplate = parseJson("{ \"execute\": \"blockdev-remove-medium\",\"arguments\": {" + "\"id\": \"\" } }"); diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpSetBalloon.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpSetBalloon.java index f9d4c5d..c7f6bed 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpSetBalloon.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpSetBalloon.java @@ -28,7 +28,8 @@ import java.math.BigInteger; */ public class QmpSetBalloon extends QmpCommand { - @SuppressWarnings({ "PMD.FieldNamingConventions" }) + @SuppressWarnings({ "PMD.FieldNamingConventions", + "PMD.VariableNamingConventions" }) private static final JsonNode jsonTemplate = parseJson("{ \"execute\": \"balloon\", " + "\"arguments\": " + "{ \"value\": 0 } }"); diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpSetDisplayPassword.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpSetDisplayPassword.java deleted file mode 100644 index 0048b9a..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpSetDisplayPassword.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu.commands; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.node.TextNode; - -/** - * A {@link QmpCommand} that sets the display password. - */ -public class QmpSetDisplayPassword extends QmpCommand { - - private final String password; - private final String protocol; - - /** - * Instantiates a new command. - * - * @param protocol the protocol - * @param password the password - */ - public QmpSetDisplayPassword(String protocol, String password) { - this.protocol = protocol; - this.password = password; - } - - @Override - public JsonNode toJson() { - ObjectNode cmd = mapper.createObjectNode(); - cmd.put("execute", "set_password"); - ObjectNode args = mapper.createObjectNode(); - cmd.set("arguments", args); - args.set("protocol", new TextNode(protocol)); - args.set("password", new TextNode(password)); - return cmd; - } - - @Override - public String toString() { - try { - var json = toJson(); - ((ObjectNode) json.get("arguments")).set("password", - new TextNode("********")); - return mapper.writeValueAsString(json); - } catch (JsonProcessingException e) { - return "(no string representation)"; - } - } - -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpSetPasswordExpiry.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpSetPasswordExpiry.java deleted file mode 100644 index 672767d..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpSetPasswordExpiry.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu.commands; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.node.TextNode; - -/** - * A {@link QmpCommand} that sets the password expiry. - */ -public class QmpSetPasswordExpiry extends QmpCommand { - - private final String protocol; - private final String expiry; - - /** - * Instantiates a new command. - * - * @param protocol the protocol - * @param expiry the expiry time - */ - public QmpSetPasswordExpiry(String protocol, String expiry) { - this.protocol = protocol; - this.expiry = expiry; - } - - @Override - public JsonNode toJson() { - ObjectNode cmd = mapper.createObjectNode(); - cmd.put("execute", "expire_password"); - ObjectNode args = mapper.createObjectNode(); - cmd.set("arguments", args); - args.set("protocol", new TextNode(protocol)); - args.set("time", new TextNode(expiry)); - return cmd; - } - - @Override - public String toString() { - try { - var json = toJson(); - return mapper.writeValueAsString(json); - } catch (JsonProcessingException e) { - return "(no string representation)"; - } - } - -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/DisplayPasswordChanged.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/DisplayPasswordChanged.java deleted file mode 100644 index 0814f50..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/DisplayPasswordChanged.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu.events; - -import com.fasterxml.jackson.databind.JsonNode; -import org.jdrupes.vmoperator.runner.qemu.commands.QmpCommand; - -/** - * A {@link MonitorResult} that indicates that the display password has changed. - */ -public class DisplayPasswordChanged extends MonitorResult { - - /** - * Instantiates a new display password changed. - * - * @param command the command - * @param response the response - */ - public DisplayPasswordChanged(QmpCommand command, JsonNode response) { - super(command, response); - } - -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/Exit.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/Exit.java deleted file mode 100644 index bb608f6..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/Exit.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu.events; - -import org.jgrapes.core.events.Stop; - -/** - * Like {@link Stop}, but sets an exit status. - */ -@SuppressWarnings("PMD.ShortClassName") -public class Exit extends Stop { - - private final int exitStatus; - - /** - * Instantiates a new exit. - * - * @param exitStatus the exit status - */ - public Exit(int exitStatus) { - this.exitStatus = exitStatus; - } - - public int exitStatus() { - return exitStatus; - } -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/GuestAgentCommand.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/GuestAgentCommand.java deleted file mode 100644 index a1b585d..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/GuestAgentCommand.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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.runner.qemu.events; - -import org.jdrupes.vmoperator.runner.qemu.commands.QmpCommand; -import org.jgrapes.core.Channel; -import org.jgrapes.core.Components; -import org.jgrapes.core.Event; - -/** - * An {@link Event} that causes some component to send a QMP - * command to the guest agent process. - */ -public class GuestAgentCommand extends Event { - - private final QmpCommand command; - - /** - * Instantiates a new exec qmp command. - * - * @param command the command - */ - public GuestAgentCommand(QmpCommand command) { - this.command = command; - } - - /** - * Gets the command. - * - * @return the command - */ - public QmpCommand command() { - return command; - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append(Components.objectName(this)) - .append(" [").append(command); - if (channels() != null) { - builder.append(", channels=").append(Channel.toString(channels())); - } - builder.append(']'); - return builder.toString(); - } -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/HotpluggableCpuStatus.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/HotpluggableCpuStatus.java index 2ab2c5a..68641c9 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/HotpluggableCpuStatus.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/HotpluggableCpuStatus.java @@ -30,9 +30,7 @@ import org.jdrupes.vmoperator.runner.qemu.commands.QmpCommand; */ public class HotpluggableCpuStatus extends MonitorResult { - @SuppressWarnings("PMD.ImmutableField") private List usedCpus = new ArrayList<>(); - @SuppressWarnings("PMD.ImmutableField") private List unusedCpus = new ArrayList<>(); /** diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorCommand.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorCommand.java index d2b5e8c..36d5b40 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorCommand.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorCommand.java @@ -55,7 +55,8 @@ public class MonitorCommand extends Event { builder.append(Components.objectName(this)) .append(" [").append(command); if (channels() != null) { - builder.append(", channels=").append(Channel.toString(channels())); + builder.append(", channels="); + builder.append(Channel.toString(channels())); } builder.append(']'); return builder.toString(); diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorEvent.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorEvent.java index 93e7785..72647a1 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorEvent.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorEvent.java @@ -20,8 +20,6 @@ package org.jdrupes.vmoperator.runner.qemu.events; import com.fasterxml.jackson.databind.JsonNode; import java.util.Optional; -import org.jgrapes.core.Channel; -import org.jgrapes.core.Components; import org.jgrapes.core.Event; /** @@ -30,14 +28,11 @@ import org.jgrapes.core.Event; */ public class MonitorEvent extends Event { - private static final String EVENT_DATA = "data"; - /** * The kind of monitor event. */ public enum Kind { - READY, POWERDOWN, DEVICE_TRAY_MOVED, BALLOON_CHANGE, SHUTDOWN, - SPICE_CONNECTED, SPICE_INITIALIZED, SPICE_DISCONNECTED, VSERPORT_CHANGE + READY, POWERDOWN, DEVICE_TRAY_MOVED, BALLOON_CHANGE } private final Kind kind; @@ -49,36 +44,23 @@ public class MonitorEvent extends Event { * @param response the response * @return the optional */ + @SuppressWarnings("PMD.TooFewBranchesForASwitchStatement") public static Optional from(JsonNode response) { try { - var kind = Kind.valueOf(response.get("event").asText()); + var kind + = MonitorEvent.Kind.valueOf(response.get("event").asText()); switch (kind) { case POWERDOWN: return Optional.of(new PowerdownEvent(kind, null)); case DEVICE_TRAY_MOVED: return Optional - .of(new TrayMovedEvent(kind, response.get(EVENT_DATA))); + .of(new TrayMovedEvent(kind, response.get("data"))); case BALLOON_CHANGE: - return Optional.of( - new BalloonChangeEvent(kind, response.get(EVENT_DATA))); - case SHUTDOWN: return Optional - .of(new ShutdownEvent(kind, response.get(EVENT_DATA))); - case SPICE_CONNECTED: - return Optional.of(new SpiceConnectedEvent(kind, - response.get(EVENT_DATA))); - case SPICE_INITIALIZED: - return Optional.of(new SpiceInitializedEvent(kind, - response.get(EVENT_DATA))); - case SPICE_DISCONNECTED: - return Optional.of(new SpiceDisconnectedEvent(kind, - response.get(EVENT_DATA))); - case VSERPORT_CHANGE: - return Optional.of(new VserportChangeEvent(kind, - response.get(EVENT_DATA))); + .of(new BalloonChangeEvent(kind, response.get("data"))); default: return Optional - .of(new MonitorEvent(kind, response.get(EVENT_DATA))); + .of(new MonitorEvent(kind, response.get("data"))); } } catch (IllegalArgumentException e) { return Optional.empty(); @@ -113,20 +95,4 @@ public class MonitorEvent extends Event { public JsonNode data() { return data; } - - /* - * (non-Javadoc) - * - * @see java.lang.Object#toString() - */ - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append(Components.objectName(this)).append(" [").append(data); - if (channels() != null) { - builder.append(", channels=").append(Channel.toString(channels())); - } - builder.append(']'); - return builder.toString(); - } } diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorResult.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorResult.java index 6d7278c..c0f55fe 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorResult.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorResult.java @@ -25,7 +25,6 @@ import org.jdrupes.vmoperator.runner.qemu.commands.QmpCapabilities; import org.jdrupes.vmoperator.runner.qemu.commands.QmpCommand; import org.jdrupes.vmoperator.runner.qemu.commands.QmpDelCpu; import org.jdrupes.vmoperator.runner.qemu.commands.QmpQueryHotpluggableCpus; -import org.jdrupes.vmoperator.runner.qemu.commands.QmpSetDisplayPassword; import org.jgrapes.core.Channel; import org.jgrapes.core.Components; import org.jgrapes.core.Event; @@ -58,9 +57,6 @@ public class MonitorResult extends Event { if (command instanceof QmpDelCpu) { return new CpuDeleted(command, response); } - if (command instanceof QmpSetDisplayPassword) { - return new DisplayPasswordChanged(command, response); - } return new MonitorResult(command, response); } @@ -152,7 +148,8 @@ public class MonitorResult extends Event { builder.append(Components.objectName(this)) .append(" [").append(executed).append(", ").append(successful()); if (channels() != null) { - builder.append(", channels=").append(Channel.toString(channels())); + builder.append(", channels="); + builder.append(Channel.toString(channels())); } builder.append(']'); return builder.toString(); diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/OsinfoEvent.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/OsinfoEvent.java deleted file mode 100644 index 0e90019..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/OsinfoEvent.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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.runner.qemu.events; - -import com.fasterxml.jackson.databind.JsonNode; -import org.jgrapes.core.Channel; -import org.jgrapes.core.Components; -import org.jgrapes.core.Event; - -/** - * Signals information about the guest OS. - */ -public class OsinfoEvent extends Event { - - private final JsonNode osinfo; - - /** - * Instantiates a new osinfo event. - * - * @param data the data - */ - public OsinfoEvent(JsonNode data) { - osinfo = data; - } - - public JsonNode osinfo() { - return osinfo; - } - - /* - * (non-Javadoc) - * - * @see java.lang.Object#toString() - */ - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append(Components.objectName(this)).append(" [") - .append(osinfo); - if (channels() != null) { - builder.append(", channels=").append(Channel.toString(channels())); - } - builder.append(']'); - return builder.toString(); - } -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/ConfigureQemu.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/RunnerConfigurationUpdate.java similarity index 90% rename from org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/ConfigureQemu.java rename to org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/RunnerConfigurationUpdate.java index 7afa738..bdb0c73 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/ConfigureQemu.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/RunnerConfigurationUpdate.java @@ -19,7 +19,7 @@ package org.jdrupes.vmoperator.runner.qemu.events; import org.jdrupes.vmoperator.runner.qemu.Configuration; -import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.RunState; +import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State; import org.jgrapes.core.Channel; import org.jgrapes.core.Event; @@ -31,17 +31,17 @@ import org.jgrapes.core.Event; * on the event and only {@link Event#resumeHandling() resume handling} * when the adaption has completed. */ -public class ConfigureQemu extends Event { +public class RunnerConfigurationUpdate extends Event { private final Configuration configuration; - private final RunState state; + private final State state; /** * Instantiates a new configuration event. * * @param channels the channels */ - public ConfigureQemu(Configuration configuration, RunState state, + public RunnerConfigurationUpdate(Configuration configuration, State state, Channel... channels) { super(channels); this.state = state; @@ -62,7 +62,7 @@ public class ConfigureQemu extends Event { * * @return the state */ - public RunState runState() { + public State state() { return state; } } diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/RunnerStateChange.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/RunnerStateChange.java index 261eebf..46fa1f8 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/RunnerStateChange.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/RunnerStateChange.java @@ -18,7 +18,6 @@ package org.jdrupes.vmoperator.runner.qemu.events; -import java.util.EnumSet; import org.jgrapes.core.Channel; import org.jgrapes.core.Components; import org.jgrapes.core.Event; @@ -29,63 +28,26 @@ import org.jgrapes.core.Event; public class RunnerStateChange extends Event { /** - * The states. + * The state. */ - public enum RunState { - INITIALIZING, STARTING, BOOTING, BOOTED, TERMINATING, STOPPED; - - /** - * Checks if the state is one of the states in which the VM is running. - * - * @return true, if is running - */ - public boolean vmRunning() { - return EnumSet.of(BOOTING, BOOTED, TERMINATING).contains(this); - } - - /** - * Checks if the state is one of the states in which the VM is active. - * - * @return true, if is active - */ - public boolean vmActive() { - return EnumSet.of(BOOTING, BOOTED).contains(this); - } + public enum State { + INITIALIZING, STARTING, RUNNING, TERMINATING, STOPPED } - private final RunState state; + private final State state; private final String reason; private final String message; - private final boolean failed; /** * Instantiates a new runner state change. * - * @param state the state - * @param reason the reason - * @param message the message * @param channels the channels */ - public RunnerStateChange(RunState state, String reason, String message, + public RunnerStateChange(State state, String reason, String message, Channel... channels) { - this(state, reason, message, false, channels); - } - - /** - * Instantiates a new runner state change. - * - * @param state the state - * @param reason the reason - * @param message the message - * @param failed the failed - * @param channels the channels - */ - public RunnerStateChange(RunState state, String reason, String message, - boolean failed, Channel... channels) { super(channels); this.state = state; this.reason = reason; - this.failed = failed; this.message = message; } @@ -94,7 +56,7 @@ public class RunnerStateChange extends Event { * * @return the state */ - public RunState runState() { + public State state() { return state; } @@ -116,25 +78,14 @@ public class RunnerStateChange extends Event { return message; } - /** - * Checks if is failed. - * - * @return the failed - */ - public boolean failed() { - return failed; - } - @Override public String toString() { - StringBuilder builder = new StringBuilder(50); + StringBuilder builder = new StringBuilder(); builder.append(Components.objectName(this)) .append(" [").append(state).append(": ").append(reason); - if (failed) { - builder.append(" (failed)"); - } if (channels() != null) { - builder.append(", channels=").append(Channel.toString(channels())); + builder.append(", channels="); + builder.append(Channel.toString(channels())); } builder.append(']'); return builder.toString(); diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/ShutdownEvent.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/ShutdownEvent.java deleted file mode 100644 index 1804232..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/ShutdownEvent.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu.events; - -import com.fasterxml.jackson.databind.JsonNode; - -/** - * Signals the reception of a SHUTDOWN event. - */ -public class ShutdownEvent extends MonitorEvent { - - /** - * Instantiates a new shutdown event. - * - * @param kind the kind - * @param data the data - */ - public ShutdownEvent(Kind kind, JsonNode data) { - super(kind, data); - } - - /** - * returns if this is initiated by the guest. - * - * @return the value - */ - public boolean byGuest() { - return data().get("guest").asBoolean(); - } - -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/SpiceConnectedEvent.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/SpiceConnectedEvent.java deleted file mode 100644 index c133307..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/SpiceConnectedEvent.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu.events; - -import com.fasterxml.jackson.databind.JsonNode; - -/** - * Signals a connection from a client. - */ -public class SpiceConnectedEvent extends SpiceEvent { - - /** - * Instantiates a new spice connected event. - * - * @param kind the kind - * @param data the data - */ - public SpiceConnectedEvent(Kind kind, JsonNode data) { - super(kind, data); - } -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/SpiceDisconnectedEvent.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/SpiceDisconnectedEvent.java deleted file mode 100644 index cfcb489..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/SpiceDisconnectedEvent.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu.events; - -import com.fasterxml.jackson.databind.JsonNode; - -/** - * Signals a connection from a client. - */ -public class SpiceDisconnectedEvent extends SpiceEvent { - - /** - * Instantiates a new spice disconnected event. - * - * @param kind the kind - * @param data the data - */ - public SpiceDisconnectedEvent(Kind kind, JsonNode data) { - super(kind, data); - } -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/SpiceEvent.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/SpiceEvent.java deleted file mode 100644 index 4ce27e2..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/SpiceEvent.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu.events; - -import com.fasterxml.jackson.databind.JsonNode; - -/** - * Signals a connection from a client. - */ -public class SpiceEvent extends MonitorEvent { - - /** - * Instantiates a new tray moved. - * - * @param kind the kind - * @param data the data - */ - public SpiceEvent(Kind kind, JsonNode data) { - super(kind, data); - } - - /** - * Returns the client's host. - * - * @return the client's host address - */ - public String clientHost() { - return data().get("client").get("host").asText(); - } - - /** - * Returns the client's port. - * - * @return the client's port number - */ - public long clientPort() { - return data().get("client").get("port").asLong(); - } -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/SpiceInitializedEvent.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/SpiceInitializedEvent.java deleted file mode 100644 index 7bb84b7..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/SpiceInitializedEvent.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu.events; - -import com.fasterxml.jackson.databind.JsonNode; - -/** - * Signals a connection from a client. - */ -public class SpiceInitializedEvent extends SpiceEvent { - - /** - * Instantiates a new spice connected event. - * - * @param kind the kind - * @param data the data - */ - public SpiceInitializedEvent(Kind kind, JsonNode data) { - super(kind, data); - } - - /** - * Returns the channel type. - * - * @return the channel type - */ - public int channelType() { - return data().get("client").get("channel-type").asInt(); - } -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/TrayMovedEvent.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/TrayMovedEvent.java index e2d2286..f5ef725 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/TrayMovedEvent.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/TrayMovedEvent.java @@ -50,7 +50,7 @@ public class TrayMovedEvent extends MonitorEvent { * * @return the tray state */ - public TrayState trayState() { + public TrayState state() { return data().get("tray-open").asBoolean() ? TrayState.OPEN : TrayState.CLOSED; diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentConnected.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentConnected.java deleted file mode 100644 index dc13569..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentConnected.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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.runner.qemu.events; - -import org.jgrapes.core.Event; - -/** - * Signals information about the guest OS. - */ -public class VmopAgentConnected extends Event { -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLogIn.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLogIn.java deleted file mode 100644 index 96db884..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLogIn.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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.runner.qemu.events; - -import org.jgrapes.core.Event; - -/** - * Sends the login command to the VM operator agent. - */ -public class VmopAgentLogIn extends Event { - - private final String user; - - /** - * Instantiates a new vmop agent logout. - */ - public VmopAgentLogIn(String user) { - this.user = user; - } - - /** - * Returns the user. - * - * @return the user - */ - public String user() { - return user; - } -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLogOut.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLogOut.java deleted file mode 100644 index 1502200..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLogOut.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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.runner.qemu.events; - -import org.jgrapes.core.Event; - -/** - * Sends the logout command to the VM operator agent. - */ -public class VmopAgentLogOut extends Event { -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLoggedIn.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLoggedIn.java deleted file mode 100644 index f59ed71..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLoggedIn.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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.runner.qemu.events; - -import org.jgrapes.core.Event; - -/** - * Signals that the logout command has been processes by the - * VM operator agent. - */ -public class VmopAgentLoggedIn extends Event { - - private final VmopAgentLogIn triggering; - - /** - * Instantiates a new vmop agent logged in. - * - * @param triggeringEvent the triggering event - */ - public VmopAgentLoggedIn(VmopAgentLogIn triggeringEvent) { - this.triggering = triggeringEvent; - } - - /** - * Gets the triggering event. - * - * @return the triggering - */ - public VmopAgentLogIn triggering() { - return triggering; - } - -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLoggedOut.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLoggedOut.java deleted file mode 100644 index 5f60e00..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VmopAgentLoggedOut.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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.runner.qemu.events; - -import org.jgrapes.core.Event; - -/** - * Signals that the logout command has been processes by the - * VM operator agent. - */ -public class VmopAgentLoggedOut extends Event { - - private final VmopAgentLogOut triggering; - - /** - * Instantiates a new vmop agent logged out. - * - * @param triggeringEvent the triggering event - */ - public VmopAgentLoggedOut(VmopAgentLogOut triggeringEvent) { - this.triggering = triggeringEvent; - } - - /** - * Gets the triggering event. - * - * @return the triggering - */ - public VmopAgentLogOut triggering() { - return triggering; - } - -} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VserportChangeEvent.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VserportChangeEvent.java deleted file mode 100644 index b590cd3..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VserportChangeEvent.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.runner.qemu.events; - -import com.fasterxml.jackson.databind.JsonNode; - -/** - * Signals a virtual serial port's open state change. - */ -public class VserportChangeEvent extends MonitorEvent { - - /** - * Initializes a new instance. - * - * @param kind the kind - * @param data the data - */ - public VserportChangeEvent(Kind kind, JsonNode data) { - super(kind, data); - } - - /** - * Return the channel's id. - * - * @return the string - */ - @SuppressWarnings("PMD.ShortMethodName") - public String id() { - return data().get("id").asText(); - } - - /** - * Returns the open state of the port. - * - * @return true, if is open - */ - public boolean isOpen() { - return Boolean.parseBoolean(data().get("open").asText()); - } -} diff --git a/org.jdrupes.vmoperator.runner.qemu/templates/Standard-VM-latest.ftl.yaml b/org.jdrupes.vmoperator.runner.qemu/templates/Standard-VM-latest.ftl.yaml index c5c0252..b21db8f 100644 --- a/org.jdrupes.vmoperator.runner.qemu/templates/Standard-VM-latest.ftl.yaml +++ b/org.jdrupes.vmoperator.runner.qemu/templates/Standard-VM-latest.ftl.yaml @@ -11,22 +11,6 @@ - [ "--ctrl", "type=unixio,path=${ runtimeDir }/swtpm-sock,mode=0600" ] - "--terminate" -"cloudInitImg": - # Candidate paths for the executable - "executable": [ "/bin/sh", "/usr/bin/sh" ] - - # Arguments may be specified as nested lists for better readability. - # The arguments are flattened before being passed to the process. - "arguments": - - "-c" - - >- - mformat -C -f 1440 -v CIDATA -i ${ runtimeDir }/cloud-init.img - && mcopy -i ${ runtimeDir }/cloud-init.img - ${ dataDir }/cloud-init/meta-data ${ dataDir }/cloud-init/user-data :: - && if [ -r ${ dataDir }/cloud-init/network-config ]; then - mcopy -i ${ runtimeDir }/cloud-init.img - ${ dataDir }/cloud-init/network-config :: ; fi - "qemu": # Candidate paths for the executable "executable": [ "/usr/bin/qemu-system-x86_64" ] @@ -122,16 +106,11 @@ # Best explanation found: # https://fedoraproject.org/wiki/Features/VirtioSerial - [ "-device", "virtio-serial-pci,id=virtio-serial0" ] - # - Guest agent serial connection. + # - Guest agent serial connection - [ "-device", "virtserialport,id=channel0,name=org.qemu.guest_agent.0,\ chardev=guest-agent-socket" ] - [ "-chardev","socket,id=guest-agent-socket,\ path=${ runtimeDir }/org.qemu.guest_agent.0,server=on,wait=off" ] - # - VM operator agent serial connection. - - [ "-device", "virtserialport,id=channel1,name=org.jdrupes.vmop_agent.0,\ - chardev=vmop-agent-socket" ] - - [ "-chardev","socket,id=vmop-agent-socket,\ - path=${ runtimeDir }/org.jdrupes.vmop_agent.0,server=on,wait=off" ] # * USB Hub and devices (more in SPICE configuration below) # https://qemu-project.gitlab.io/qemu/system/devices/usb.html # https://github.com/qemu/qemu/blob/master/hw/usb/hcd-xhci.c @@ -142,8 +121,7 @@ - [ "-device", "virtio-rng-pci,rng=objrng0,id=rng0" ] # * Graphics and Audio Card # This is the only video "card" without a flickering cursor. - - [ "-device", "virtio-vga,id=video0,max_outputs=${ vm.display.outputs },\ - max_hostmem=${ (vm.display.outputs * 256 * 1024 * 1024)?c }" ] + - [ "-device", "virtio-vga,id=video0,max_outputs=1" ] - [ "-device", "ich9-intel-hda,id=sound0" ] # Network <#assign nwCounter = 0/> @@ -205,24 +183,18 @@ <#break> - # Cloud-init image - <#if cloudInit??> - - [ "-blockdev", "node-name=drive-${ drvCounter }-host-resource,\ - driver=file,filename=${ runtimeDir }/cloud-init.img" ] - # - how to use the file (as sequence of literal blocks) - - [ "-blockdev", "node-name=drive-${ drvCounter }-backend,driver=raw,\ - file=drive-${ drvCounter }-host-resource" ] - # - the driver (what the guest sees) - - [ "-device", "virtio-blk-pci,drive=drive-${ drvCounter }-backend" ] - <#if vm.display??> <#if vm.display.spice??> <#assign spice = vm.display.spice/> # SPICE (display, channels ...) # https://www.linux-kvm.org/page/SPICE + <#if ticketPath??> + - [ "-object", "secret,id=spiceTicket,file=${ ticketPath }" ] + - [ "-spice", "port=${ spice.port?c }\ - ,disable-ticketing=<#if hasDisplayPassword!false>off<#else>on\ + <#if spice.ticket??>,password-secret=spiceTicket\ + <#else>,disable-ticketing=on\ <#if spice.streamingVideo??>,streaming-video=${ spice.streamingVideo }\ ,seamless-migration=on" ] - [ "-chardev", "spicevmc,id=vdagentdev,name=vdagent" ] diff --git a/org.jdrupes.vmoperator.util/.eclipse-pmd b/org.jdrupes.vmoperator.util/.eclipse-pmd index 5d69caa..8b394f8 100644 --- a/org.jdrupes.vmoperator.util/.eclipse-pmd +++ b/org.jdrupes.vmoperator.util/.eclipse-pmd @@ -2,6 +2,6 @@ - + diff --git a/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/DataPath.java b/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/DataPath.java deleted file mode 100644 index e83cf27..0000000 --- a/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/DataPath.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.util; - -import java.lang.reflect.Array; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.logging.Logger; - -/** - * Utility class that supports navigation through arbitrary data structures. - */ -public final class DataPath { - - private static final Logger logger - = Logger.getLogger(DataPath.class.getName()); - - private DataPath() { - } - - /** - * Apply the given selectors on the given object and return the - * value reached. - * - * Selectors can be if type {@link String} or {@link Number}. The - * former are used to access a property of an object, the latter to - * access an element in an array or a {@link List}. - * - * Depending on the object currently visited, a {@link String} can - * be the key of a {@link Map}, the property part of a getter method - * or the name of a method that has an empty parameter list. - * - * @param the generic type - * @param from the from - * @param selectors the selectors - * @return the result - */ - public static Optional get(Object from, Object... selectors) { - Object cur = from; - for (var selector : selectors) { - if (cur == null) { - return Optional.empty(); - } - if (selector instanceof String && cur instanceof Map map) { - cur = map.get(selector); - continue; - } - if (selector instanceof Number index && cur instanceof List list) { - cur = list.get(index.intValue()); - continue; - } - if (selector instanceof String property) { - var retrieved = tryAccess(cur, property); - if (retrieved.isEmpty()) { - return Optional.empty(); - } - cur = retrieved.get(); - } - } - @SuppressWarnings("unchecked") - var result = Optional.ofNullable((T) cur); - return result; - } - - @SuppressWarnings("PMD.UseLocaleWithCaseConversions") - private static Optional tryAccess(Object obj, String property) { - Method acc = null; - try { - // Try getter - acc = obj.getClass().getMethod("get" + property.substring(0, 1) - .toUpperCase() + property.substring(1)); - } catch (SecurityException e) { - return Optional.empty(); - } catch (NoSuchMethodException e) { // NOPMD - // Can happen... - } - if (acc == null) { - try { - // Try method - acc = obj.getClass().getMethod(property); - } catch (SecurityException | NoSuchMethodException e) { - return Optional.empty(); - } - } - if (acc != null) { - try { - return Optional.ofNullable(acc.invoke(obj)); - } catch (IllegalAccessException - | InvocationTargetException e) { - return Optional.empty(); - } - } - return Optional.empty(); - } - - /** - * Attempts to make a as-deep-as-possible copy of the given - * container. New containers will be created for Maps, Lists and - * Arrays. The method is invoked recursively for the entries/items. - * - * If invoked with an object that is neither a map, list or array, - * the methods checks if the object implements {@link Cloneable} - * and if it does, invokes its {@link Object#clone()} method. - * Else the method return the object. - * - * @param the generic type - * @param object the container - * @return the t - */ - @SuppressWarnings({ "PMD.CognitiveComplexity", "unchecked" }) - public static T deepCopy(T object) { - if (object instanceof Map map) { - Map copy; - try { - copy = (Map) object.getClass().getConstructor() - .newInstance(); - } catch (InstantiationException | IllegalAccessException - | IllegalArgumentException | InvocationTargetException - | NoSuchMethodException | SecurityException e) { - logger.severe( - () -> "Cannot create new instance of " + object.getClass()); - return null; - } - for (var entry : ((Map) map).entrySet()) { - copy.put(entry.getKey(), - deepCopy(entry.getValue())); - } - return (T) copy; - } - if (object instanceof List list) { - List copy = new ArrayList<>(); - for (var item : list) { - copy.add(deepCopy(item)); - } - return (T) copy; - } - if (object.getClass().isArray()) { - var copy = Array.newInstance(object.getClass().getComponentType(), - Array.getLength(object)); - for (int i = 0; i < Array.getLength(object); i++) { - Array.set(copy, i, deepCopy(Array.get(object, i))); - } - return (T) copy; - } - if (object instanceof Cloneable) { - try { - return (T) object.getClass().getMethod("clone") - .invoke(object); - } catch (IllegalAccessException | InvocationTargetException - | NoSuchMethodException | SecurityException e) { - return object; - } - } - return object; - } -} diff --git a/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/GsonPtr.java b/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/GsonPtr.java index c6fb101..c16b3e0 100644 --- a/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/GsonPtr.java +++ b/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/GsonPtr.java @@ -23,16 +23,14 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import java.math.BigInteger; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; import java.util.Optional; import java.util.function.Supplier; /** * Utility class for pointing to elements on a Gson (Json) tree. */ -@SuppressWarnings({ "PMD.ClassWithOnlyPrivateConstructorsShouldBeFinal" }) +@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", + "PMD.ClassWithOnlyPrivateConstructorsShouldBeFinal" }) public class GsonPtr { private final JsonElement position; @@ -62,8 +60,7 @@ public class GsonPtr { * @param selectors the selectors * @return the Gson pointer */ - @SuppressWarnings({ "PMD.ShortMethodName", "PMD.PreserveStackTrace", - "PMD.AvoidDuplicateLiterals" }) + @SuppressWarnings({ "PMD.ShortMethodName", "PMD.PreserveStackTrace" }) public GsonPtr to(Object... selectors) { JsonElement element = position; for (Object sel : selectors) { @@ -92,42 +89,6 @@ public class GsonPtr { return new GsonPtr(element); } - /** - * Create a new instance pointing to the {@link JsonElement} - * selected by the given selectors. If a selector of type - * {@link String} denotes a non-existant member of a - * {@link JsonObject} the result is empty. - * - * @param selectors the selectors - * @return the Gson pointer - */ - @SuppressWarnings({ "PMD.PreserveStackTrace" }) - public Optional get(Object... selectors) { - JsonElement element = position; - for (Object sel : selectors) { - if (element instanceof JsonObject obj - && sel instanceof String member) { - element = obj.get(member); - if (element == null) { - return Optional.empty(); - } - continue; - } - if (element instanceof JsonArray arr - && sel instanceof Integer index) { - try { - element = arr.get(index); - } catch (IndexOutOfBoundsException e) { - throw new IllegalStateException("Selected array index" - + " may not be empty."); - } - continue; - } - throw new IllegalStateException("Invalid selection"); - } - return Optional.of(new GsonPtr(element)); - } - /** * Returns {@link JsonElement} that the pointer points to. * @@ -145,7 +106,8 @@ public class GsonPtr { * @param cls the cls * @return the result */ - public T getAs(Class cls) { + @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop" }) + public T get(Class cls) { if (cls.isAssignableFrom(position.getClass())) { return cls.cast(position); } @@ -164,7 +126,7 @@ public class GsonPtr { */ @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop" }) public Optional - getAs(Class cls, Object... selectors) { + get(Class cls, Object... selectors) { JsonElement element = position; for (Object sel : selectors) { if (element instanceof JsonObject obj @@ -199,7 +161,7 @@ public class GsonPtr { * @return the as string */ public Optional getAsString(Object... selectors) { - return getAs(JsonPrimitive.class, selectors) + return get(JsonPrimitive.class, selectors) .map(JsonPrimitive::getAsString); } @@ -210,7 +172,7 @@ public class GsonPtr { * @return the as string */ public Optional getAsInt(Object... selectors) { - return getAs(JsonPrimitive.class, selectors) + return get(JsonPrimitive.class, selectors) .map(JsonPrimitive::getAsInt); } @@ -221,7 +183,7 @@ public class GsonPtr { * @return the as string */ public Optional getAsBigInteger(Object... selectors) { - return getAs(JsonPrimitive.class, selectors) + return get(JsonPrimitive.class, selectors) .map(JsonPrimitive::getAsBigInteger); } @@ -232,7 +194,7 @@ public class GsonPtr { * @return the as string */ public Optional getAsLong(Object... selectors) { - return getAs(JsonPrimitive.class, selectors) + return get(JsonPrimitive.class, selectors) .map(JsonPrimitive::getAsLong); } @@ -243,25 +205,10 @@ public class GsonPtr { * @return the boolean */ public Optional getAsBoolean(Object... selectors) { - return getAs(JsonPrimitive.class, selectors) + return get(JsonPrimitive.class, selectors) .map(JsonPrimitive::getAsBoolean); } - /** - * Returns the elements of the selected {@link JsonArray} as list. - * - * @param the generic type - * @param cls the cls - * @param selectors the selectors - * @return the list - */ - @SuppressWarnings("unchecked") - public List getAsListOf(Class cls, - Object... selectors) { - return getAs(JsonArray.class, selectors).map(a -> (List) a.asList()) - .orElse(Collections.emptyList()); - } - /** * Sets the selected value. This pointer must point to a * {@link JsonObject} or {@link JsonArray}. The selector must @@ -301,42 +248,6 @@ public class GsonPtr { return set(selector, new JsonPrimitive(value)); } - /** - * Short for `set(selector, new JsonPrimitive(value))`. - * - * @param selector the selector - * @param value the value - * @return the gson ptr - * @see #set(Object, JsonElement) - */ - public GsonPtr set(Object selector, Long value) { - return set(selector, new JsonPrimitive(value)); - } - - /** - * Short for `set(selector, new JsonPrimitive(value))`. - * - * @param selector the selector - * @param value the value - * @return the gson ptr - * @see #set(Object, JsonElement) - */ - public GsonPtr set(Object selector, BigInteger value) { - return set(selector, new JsonPrimitive(value)); - } - - /** - * Short for `set(selector, new JsonPrimitive(value))`. - * - * @param selector the selector - * @param value the value - * @return the gson ptr - * @see #set(Object, JsonElement) - */ - public GsonPtr set(Object selector, Boolean value) { - return set(selector, new JsonPrimitive(value)); - } - /** * Same as {@link #set(Object, JsonElement)}, but sets the value * only if it doesn't exist yet, else returns the existing value. @@ -384,22 +295,4 @@ public class GsonPtr { return this; } - /** - * Removes all properties except the specified ones. - * - * @param properties the properties - */ - public void removeExcept(String... properties) { - if (!position.isJsonObject()) { - return; - } - for (var itr = ((JsonObject) position).entrySet().iterator(); - itr.hasNext();) { - var entry = itr.next(); - if (Arrays.asList(properties).contains(entry.getKey())) { - continue; - } - itr.remove(); - } - } } diff --git a/org.jdrupes.vmoperator.util/test/org/jdrupes/vmoperator/util/DataPathTests.java b/org.jdrupes.vmoperator.util/test/org/jdrupes/vmoperator/util/DataPathTests.java deleted file mode 100644 index 9c7855f..0000000 --- a/org.jdrupes.vmoperator.util/test/org/jdrupes/vmoperator/util/DataPathTests.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.jdrupes.vmoperator.util; - -import static org.junit.jupiter.api.Assertions.*; -import org.junit.jupiter.api.Test; - -class DataPathTests { - - @Test - void testArray() { - int[] orig - = { Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3) }; - var copy = DataPath.deepCopy(orig); - for (int i = 0; i < orig.length; i++) { - assertEquals(orig[i], copy[i]); - } - } -} diff --git a/org.jdrupes.vmoperator.vmaccess/.eclipse-pmd b/org.jdrupes.vmoperator.vmaccess/.eclipse-pmd deleted file mode 100644 index 60d7780..0000000 --- a/org.jdrupes.vmoperator.vmaccess/.eclipse-pmd +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/org.jdrupes.vmoperator.vmaccess/.eslintignore b/org.jdrupes.vmoperator.vmaccess/.eslintignore deleted file mode 100644 index 139d3ee..0000000 --- a/org.jdrupes.vmoperator.vmaccess/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -rollup.config.mjs diff --git a/org.jdrupes.vmoperator.vmaccess/.eslintrc.json b/org.jdrupes.vmoperator.vmaccess/.eslintrc.json deleted file mode 100644 index e4f80f1..0000000 --- a/org.jdrupes.vmoperator.vmaccess/.eslintrc.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { "project": ["./tsconfig.json"] }, - "plugins": [ - "@typescript-eslint" - ], - "rules": { - "constructor-super": "off" - } -} - diff --git a/org.jdrupes.vmoperator.vmaccess/build.gradle b/org.jdrupes.vmoperator.vmaccess/build.gradle deleted file mode 100644 index 606c6cd..0000000 --- a/org.jdrupes.vmoperator.vmaccess/build.gradle +++ /dev/null @@ -1,57 +0,0 @@ -plugins { - id 'org.jdrupes.vmoperator.java-library-conventions' -} - -dependencies { - implementation project(':org.jdrupes.vmoperator.manager.events') - - implementation 'org.jgrapes:org.jgrapes.webconsole.base:[2.1.0,3)' - implementation 'org.jgrapes:org.jgrapes.webconsole.provider.vue:[1,2)' - implementation 'org.jgrapes:org.jgrapes.webconsole.provider.jgwcvuecomponents:[1.2,2)' - implementation 'org.jgrapes:org.jgrapes.webconsole.provider.chartjs:[1.2,2)' - -} - -apply plugin: 'com.github.node-gradle.node' - -node { - download = true -} - -task extractDependencies(type: Copy) { - from configurations.compileClasspath - .findAll{ it.name.contains('.provider.') - || it.name.contains('org.jgrapes.webconsole.base') - } - .collect{ zipTree (it) } - exclude '*.class' - into 'build/unpacked' - duplicatesStrategy 'include' - } - -task compileTs(type: NodeTask) { - dependsOn ':npmInstall' - dependsOn extractDependencies - inputs.dir project.file('src') - inputs.file project.file('tsconfig.json') - inputs.file project.file('rollup.config.mjs') - outputs.dir project.file('build/generated/resources') - script = file("${rootProject.rootDir}/node_modules/rollup/dist/bin/rollup") - args = ["-c"] -} - -sourceSets { - main { - resources { - srcDir project.file('build/generated/resources') - } - } -} - -processResources { - dependsOn compileTs -} - -eclipse { - autoBuildTasks compileTs -} diff --git a/org.jdrupes.vmoperator.vmaccess/resources/META-INF/services/org.jgrapes.webconsole.base.ConletComponentFactory b/org.jdrupes.vmoperator.vmaccess/resources/META-INF/services/org.jgrapes.webconsole.base.ConletComponentFactory deleted file mode 100644 index ec5cf30..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/META-INF/services/org.jgrapes.webconsole.base.ConletComponentFactory +++ /dev/null @@ -1 +0,0 @@ -org.jdrupes.vmoperator.vmaccess.VmAccessFactory diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/VmAccess-confirmReset.ftl.html b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/VmAccess-confirmReset.ftl.html deleted file mode 100644 index d7b9405..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/VmAccess-confirmReset.ftl.html +++ /dev/null @@ -1,13 +0,0 @@ -
-

${_("confirmResetMsg")}

-

- - - - - - -

-
\ No newline at end of file diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/VmAccess-edit.ftl.html b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/VmAccess-edit.ftl.html deleted file mode 100644 index a34f725..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/VmAccess-edit.ftl.html +++ /dev/null @@ -1,39 +0,0 @@ -
-
-
-
- {{ localize("Select VM or pool") }} -
    -
  • - -
  • -
  • - -
  • -
-
-
-
-
diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/VmAccess-l10nBundles.ftl.js b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/VmAccess-l10nBundles.ftl.js deleted file mode 100644 index 96928ef..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/VmAccess-l10nBundles.ftl.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -"use strict"; - -const l10nBundles = new Map(); -let entries = null; -// <#list supportedLanguages() as l> -entries = new Map(); -l10nBundles.set("${l.locale.toLanguageTag()}", entries); -// <#list l.l10nBundle.keys as key> -entries.set("${key}", "${l.l10nBundle.getString(key)}"); -// -// - -export default l10nBundles; diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/VmAccess-preview.ftl.html b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/VmAccess-preview.ftl.html deleted file mode 100644 index 57693ea..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/VmAccess-preview.ftl.html +++ /dev/null @@ -1,7 +0,0 @@ -
-
diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/computer-in-use.svg b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/computer-in-use.svg deleted file mode 100644 index 00e4cc0..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/computer-in-use.svg +++ /dev/null @@ -1,90 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/computer-off.svg b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/computer-off.svg deleted file mode 100644 index 27c11ae..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/computer-off.svg +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/computer.svg b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/computer.svg deleted file mode 100644 index f7a6b94..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/computer.svg +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/l10n.properties b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/l10n.properties deleted file mode 100644 index 6ec24aa..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/l10n.properties +++ /dev/null @@ -1,9 +0,0 @@ -conletName = VM Access - -okayLabel = Apply and Close - -confirmResetTitle = Confirm reset -confirmResetMsg = Resetting the VM may cause loss of data. \ - Please confirm to continue. -consoleInaccessibleNotification = Console is not ready or in use. -poolEmptyNotification = No VM available. Please consult your administrator. diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/l10n_de.properties b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/l10n_de.properties deleted file mode 100644 index 28c01f0..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/l10n_de.properties +++ /dev/null @@ -1,17 +0,0 @@ -conletName = VM-Zugriff - -okayLabel = Anwenden und Schließen -Select\ VM\ or\ pool = VM oder Pool auswählen - -Start\ VM = VM starten -Stop\ VM = VM anhalten -Reset\ VM = VM zurücksetzen -Open\ console = Konsole anzeigen - -confirmResetTitle = Zurücksetzen bestätigen -confirmResetMsg = Zurücksetzen der VM kann zu Datenverlust führen. \ - Bitte bestätigen um fortzufahren. -consoleInaccessibleNotification = Die Konsole ist nicht bereit oder belegt. -poolEmptyNotification = Keine VM verfügbar. Wenden Sie sich bitte an den \ - Systemadministrator. - \ No newline at end of file diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/Licenses.txt b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/Licenses.txt deleted file mode 100644 index ac24b16..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/Licenses.txt +++ /dev/null @@ -1,20 +0,0 @@ -almalinux.svg: - Source: https://commons.wikimedia.org/wiki/File:AlmaLinux_Icon_Logo.svg - License: https://github.com/AlmaLinux/wiki/blob/master/LICENSE - -archlinux.svg: - Source: https://commons.wikimedia.org/wiki/File:Arch_Linux_%22Crystal%22_icon.svghttps://commons.wikimedia.org/wiki/File:Arch_Linux_%22Crystal%22_icon.svg - License: GPL v2 or later - -debian.svg: - Source: https://commons.wikimedia.org/wiki/File:Openlogo-debianV2.svg - License : LGPL - -fedora.svg: - Source: https://commons.wikimedia.org/wiki/File:Fedora_icon_(2021).svg - License: Public Domain - -tux.svg: - Source: https://commons.wikimedia.org/wiki/File:Tux.svghttps://commons.wikimedia.org/wiki/File:Tux.svg - License: Creative Commons CC0 1.0 Universal Public Domain Dedication. Creative Commons CC0 1.0 Universal Public Domain Dedication. - diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/almalinux.svg b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/almalinux.svg deleted file mode 100644 index b2e050a..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/almalinux.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/arch.svg b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/arch.svg deleted file mode 100644 index ca8204c..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/arch.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/debian.svg b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/debian.svg deleted file mode 100644 index 685f632..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/debian.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/fedora.svg b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/fedora.svg deleted file mode 100644 index e227311..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/fedora.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/tux.svg b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/tux.svg deleted file mode 100644 index 6b558e7..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/tux.svg +++ /dev/null @@ -1,438 +0,0 @@ - - - Tux - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/ubuntu.svg b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/ubuntu.svg deleted file mode 100644 index f217bc8..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/ubuntu.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/unknown.svg b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/unknown.svg deleted file mode 100644 index 51f3016..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/unknown.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - - - - OS - - - diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/windows.svg b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/windows.svg deleted file mode 100644 index 2c7392e..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/osicons/windows.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/reset-icon.svg b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/reset-icon.svg deleted file mode 100644 index d47e33d..0000000 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/reset-icon.svg +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - diff --git a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/VmAccess.java b/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/VmAccess.java deleted file mode 100644 index f30b771..0000000 --- a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/VmAccess.java +++ /dev/null @@ -1,991 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023,2025 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * 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.vmaccess; - -import com.fasterxml.jackson.annotation.JsonGetter; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.google.gson.JsonSyntaxException; -import freemarker.core.ParseException; -import freemarker.template.MalformedTemplateNameException; -import freemarker.template.Template; -import freemarker.template.TemplateNotFoundException; -import io.kubernetes.client.util.Strings; -import java.io.IOException; -import java.net.Inet4Address; -import java.net.Inet6Address; -import java.time.Duration; -import java.util.Collections; -import java.util.EnumSet; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.ResourceBundle; -import java.util.Set; -import java.util.logging.Level; -import java.util.stream.Collectors; -import org.bouncycastle.util.Objects; -import org.jdrupes.vmoperator.common.K8sObserver; -import org.jdrupes.vmoperator.common.VmDefinition; -import org.jdrupes.vmoperator.common.VmDefinition.Assignment; -import org.jdrupes.vmoperator.common.VmDefinition.Permission; -import org.jdrupes.vmoperator.common.VmPool; -import org.jdrupes.vmoperator.manager.events.AssignVm; -import org.jdrupes.vmoperator.manager.events.GetDisplaySecret; -import org.jdrupes.vmoperator.manager.events.GetPools; -import org.jdrupes.vmoperator.manager.events.GetVms; -import org.jdrupes.vmoperator.manager.events.GetVms.VmData; -import org.jdrupes.vmoperator.manager.events.ModifyVm; -import org.jdrupes.vmoperator.manager.events.ResetVm; -import org.jdrupes.vmoperator.manager.events.VmChannel; -import org.jdrupes.vmoperator.manager.events.VmPoolChanged; -import org.jdrupes.vmoperator.manager.events.VmResourceChanged; -import org.jgrapes.core.Channel; -import org.jgrapes.core.Components; -import org.jgrapes.core.Event; -import org.jgrapes.core.EventPipeline; -import org.jgrapes.core.Manager; -import org.jgrapes.core.annotation.Handler; -import org.jgrapes.core.events.Start; -import org.jgrapes.http.Session; -import org.jgrapes.util.events.ConfigurationUpdate; -import org.jgrapes.util.events.KeyValueStoreQuery; -import org.jgrapes.util.events.KeyValueStoreUpdate; -import org.jgrapes.webconsole.base.Conlet.RenderMode; -import org.jgrapes.webconsole.base.ConletBaseModel; -import org.jgrapes.webconsole.base.ConsoleConnection; -import org.jgrapes.webconsole.base.ConsoleRole; -import org.jgrapes.webconsole.base.ConsoleUser; -import org.jgrapes.webconsole.base.WebConsoleUtils; -import org.jgrapes.webconsole.base.events.AddConletRequest; -import org.jgrapes.webconsole.base.events.AddConletType; -import org.jgrapes.webconsole.base.events.AddPageResources.ScriptResource; -import org.jgrapes.webconsole.base.events.ConletDeleted; -import org.jgrapes.webconsole.base.events.ConsoleConfigured; -import org.jgrapes.webconsole.base.events.ConsolePrepared; -import org.jgrapes.webconsole.base.events.ConsoleReady; -import org.jgrapes.webconsole.base.events.DeleteConlet; -import org.jgrapes.webconsole.base.events.DisplayNotification; -import org.jgrapes.webconsole.base.events.NotifyConletModel; -import org.jgrapes.webconsole.base.events.NotifyConletView; -import org.jgrapes.webconsole.base.events.OpenModalDialog; -import org.jgrapes.webconsole.base.events.RenderConlet; -import org.jgrapes.webconsole.base.events.RenderConletRequestBase; -import org.jgrapes.webconsole.base.events.SetLocale; -import org.jgrapes.webconsole.base.events.UpdateConletType; -import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet; - -/** - * The Class {@link VmAccess}. The component supports the following - * configuration properties: - * - * * `displayResource`: a map with the following entries: - * - `preferredIpVersion`: `ipv4` or `ipv6` (default: `ipv4`). - * Determines the IP addresses uses in the generated - * connection file. - * * `deleteConnectionFile`: `true` or `false` (default: `true`). - * If `true`, the downloaded connection file will be deleted by - * the remote viewer when opened. - * * `syncPreviewsFor`: a list objects with either property `user` or - * `role` and the associated name (default: `[]`). - * The remote viewer will synchronize the previews for the specified - * users and roles. - * - */ -@SuppressWarnings({ "PMD.ExcessiveImports", "PMD.CouplingBetweenObjects", - "PMD.GodClass", "PMD.TooManyMethods", "PMD.CyclomaticComplexity" }) -public class VmAccess extends FreeMarkerConlet { - - private static final String VM_NAME_PROPERTY = "vmName"; - private static final String POOL_NAME_PROPERTY = "poolName"; - private static final String RENDERED - = VmAccess.class.getName() + ".rendered"; - private static final String PENDING - = VmAccess.class.getName() + ".pending"; - private static final Set MODES = RenderMode.asSet( - RenderMode.Preview, RenderMode.Edit); - private static final Set MODES_FOR_GENERATED = RenderMode.asSet( - RenderMode.Preview, RenderMode.StickyPreview); - private EventPipeline appPipeline; - private static ObjectMapper objectMapper - = new ObjectMapper().registerModule(new JavaTimeModule()); - - private Class preferredIpVersion = Inet4Address.class; - private Set syncUsers = Collections.emptySet(); - private Set syncRoles = Collections.emptySet(); - private boolean deleteConnectionFile = true; - - /** - * The periodically generated update event. - */ - public static class Update extends Event { - } - - /** - * Creates a new component with its channel set to the given channel. - * - * @param componentChannel the channel that the component's handlers listen - * on by default and that {@link Manager#fire(Event, Channel...)} - * sends the event to - */ - public VmAccess(Channel componentChannel) { - super(componentChannel); - } - - /** - * On start. - * - * @param event the event - */ - @Handler - public void onStart(Start event) { - appPipeline = event.processedBy().get(); - } - - /** - * Configure the component. - * - * @param event the event - */ - @SuppressWarnings({ "unchecked" }) - @Handler - public void onConfigurationUpdate(ConfigurationUpdate event) { - event.structured(componentPath()) - .or(() -> { - var oldConfig = event.structured("/Manager/GuiHttpServer" - + "/ConsoleWeblet/WebConsole/ComponentCollector/VmViewer"); - if (oldConfig.isPresent()) { - logger.warning(() -> "Using configuration with old " - + "component name \"VmViewer\", please update to " - + "\"VmAccess\""); - } - return oldConfig; - }) - .ifPresent(c -> { - try { - var dispRes = (Map) c - .getOrDefault("displayResource", - Collections.emptyMap()); - switch ((String) dispRes.getOrDefault("preferredIpVersion", - "")) { - case "ipv6": - preferredIpVersion = Inet6Address.class; - break; - case "ipv4": - default: - preferredIpVersion = Inet4Address.class; - break; - } - - // Delete connection file - deleteConnectionFile - = Optional.ofNullable(c.get("deleteConnectionFile")) - .map(Object::toString).map(Boolean::parseBoolean) - .orElse(true); - - // Users or roles for which previews should be synchronized - syncUsers = ((List>) c.getOrDefault( - "syncPreviewsFor", Collections.emptyList())).stream() - .map(m -> m.get("user")) - .filter(s -> s != null).collect(Collectors.toSet()); - logger.finest(() -> "Syncing previews for users: " - + syncUsers.toString()); - syncRoles = ((List>) c.getOrDefault( - "syncPreviewsFor", Collections.emptyList())).stream() - .map(m -> m.get("role")) - .filter(s -> s != null).collect(Collectors.toSet()); - logger.finest(() -> "Syncing previews for roles: " - + syncRoles.toString()); - } catch (ClassCastException e) { - logger.config("Malformed configuration: " + e.getMessage()); - } - }); - } - - private boolean syncPreviews(Session session) { - return WebConsoleUtils.userFromSession(session) - .filter(u -> syncUsers.contains(u.getName())).isPresent() - || WebConsoleUtils.rolesFromSession(session).stream() - .filter(cr -> syncRoles.contains(cr.getName())).findAny() - .isPresent(); - } - - /** - * On {@link ConsoleReady}, fire the {@link AddConletType}. - * - * @param event the event - * @param channel the channel - * @throws TemplateNotFoundException the template not found exception - * @throws MalformedTemplateNameException the malformed template name - * exception - * @throws ParseException the parse exception - * @throws IOException Signals that an I/O exception has occurred. - */ - @Handler - public void onConsoleReady(ConsoleReady event, ConsoleConnection channel) - throws TemplateNotFoundException, MalformedTemplateNameException, - ParseException, IOException { - // Add conlet resources to page - channel.respond(new AddConletType(type()) - .setDisplayNames( - localizations(channel.supportedLocales(), "conletName")) - .addRenderMode(RenderMode.Preview) - .addScript(new ScriptResource().setScriptType("module") - .setScriptUri(event.renderSupport().conletResource( - type(), "VmAccess-functions.js")))); - channel.session().put(RENDERED, new HashSet<>()); - } - - /** - * On console configured. - * - * @param event the event - * @param connection the console connection - * @throws InterruptedException the interrupted exception - */ - @Handler - public void onConsoleConfigured(ConsoleConfigured event, - ConsoleConnection connection) throws InterruptedException, - IOException { - @SuppressWarnings({ "unchecked" }) - final var rendered - = (Set) connection.session().get(RENDERED); - connection.session().remove(RENDERED); - if (!syncPreviews(connection.session())) { - return; - } - addMissingConlets(event, connection, rendered); - } - - @SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops" }) - private void addMissingConlets(ConsoleConfigured event, - ConsoleConnection connection, final Set rendered) - throws InterruptedException { - var session = connection.session(); - - // Evaluate missing VMs - var missingVms = appPipeline.fire(new GetVms().accessibleFor( - WebConsoleUtils.userFromSession(session) - .map(ConsoleUser::getName).orElse(null), - WebConsoleUtils.rolesFromSession(session).stream() - .map(ConsoleRole::getName).toList())) - .get().stream().map(d -> d.definition().name()) - .collect(Collectors.toCollection(HashSet::new)); - missingVms.removeAll(rendered.stream() - .filter(r -> r.mode() == ResourceModel.Mode.VM) - .map(ResourceModel::name).toList()); - - // Evaluate missing pools - var missingPools = appPipeline.fire(new GetPools().accessibleFor( - WebConsoleUtils.userFromSession(session) - .map(ConsoleUser::getName).orElse(null), - WebConsoleUtils.rolesFromSession(session).stream() - .map(ConsoleRole::getName).toList())) - .get().stream().map(VmPool::name) - .collect(Collectors.toCollection(HashSet::new)); - missingPools.removeAll(rendered.stream() - .filter(r -> r.mode() == ResourceModel.Mode.POOL) - .map(ResourceModel::name).toList()); - - // Nothing to do - if (missingVms.isEmpty() && missingPools.isEmpty()) { - return; - } - - // Suspending to allow rendering of conlets to be noticed - var failSafe = Components.schedule(t -> event.resumeHandling(), - Duration.ofSeconds(1)); - event.suspendHandling(failSafe::cancel); - connection.setAssociated(PENDING, event); - - // Create conlets for VMs and pools that haven't been rendered - for (var vmName : missingVms) { - fire(new AddConletRequest(event.event().event().renderSupport(), - VmAccess.class.getName(), RenderMode.asSet(RenderMode.Preview)) - .addProperty(VM_NAME_PROPERTY, vmName), - connection); - } - for (var poolName : missingPools) { - fire(new AddConletRequest(event.event().event().renderSupport(), - VmAccess.class.getName(), RenderMode.asSet(RenderMode.Preview)) - .addProperty(POOL_NAME_PROPERTY, poolName), - connection); - } - } - - /** - * On console prepared. - * - * @param event the event - * @param connection the connection - */ - @Handler - public void onConsolePrepared(ConsolePrepared event, - ConsoleConnection connection) { - if (syncPreviews(connection.session())) { - connection.respond(new UpdateConletType(type())); - } - } - - private String storagePath(Session session, String conletId) { - return "/" + WebConsoleUtils.userFromSession(session) - .map(ConsoleUser::getName).orElse("") - + "/" + VmAccess.class.getName() + "/" + conletId; - } - - @Override - protected Optional createNewState(AddConletRequest event, - ConsoleConnection connection, String conletId) throws Exception { - var model = new ResourceModel(conletId); - var poolName = (String) event.properties().get(POOL_NAME_PROPERTY); - if (poolName != null) { - model.setMode(ResourceModel.Mode.POOL); - model.setName(poolName); - } else { - model.setMode(ResourceModel.Mode.VM); - model.setName((String) event.properties().get(VM_NAME_PROPERTY)); - } - String jsonState = objectMapper.writeValueAsString(model); - connection.respond(new KeyValueStoreUpdate().update( - storagePath(connection.session(), model.getConletId()), jsonState)); - return Optional.of(model); - } - - @Override - protected Optional createStateRepresentation(Event event, - ConsoleConnection connection, String conletId) throws Exception { - var model = new ResourceModel(conletId); - String jsonState = objectMapper.writeValueAsString(model); - connection.respond(new KeyValueStoreUpdate().update( - storagePath(connection.session(), model.getConletId()), jsonState)); - return Optional.of(model); - } - - @Override - @SuppressWarnings("PMD.EmptyCatchBlock") - protected Optional recreateState(Event event, - ConsoleConnection channel, String conletId) throws Exception { - KeyValueStoreQuery query = new KeyValueStoreQuery( - storagePath(channel.session(), conletId), channel); - newEventPipeline().fire(query, channel); - try { - if (!query.results().isEmpty()) { - var json = query.results().get(0).values().stream().findFirst() - .get(); - ResourceModel model - = objectMapper.readValue(json, ResourceModel.class); - return Optional.of(model); - } - } catch (InterruptedException e) { - // Means we have no result. - } - - // Fall back to creating default state. - return createStateRepresentation(event, channel, conletId); - } - - @Override - protected Set doRenderConlet(RenderConletRequestBase event, - ConsoleConnection channel, String conletId, ResourceModel model) - throws Exception { - if (event.renderAs().contains(RenderMode.Preview)) { - return renderPreview(event, channel, conletId, model); - } - - // Render edit - ResourceBundle resourceBundle = resourceBundle(channel.locale()); - Set renderedAs = EnumSet.noneOf(RenderMode.class); - if (event.renderAs().contains(RenderMode.Edit)) { - var session = channel.session(); - var vmNames = appPipeline.fire(new GetVms().accessibleFor( - WebConsoleUtils.userFromSession(session) - .map(ConsoleUser::getName).orElse(null), - WebConsoleUtils.rolesFromSession(session).stream() - .map(ConsoleRole::getName).toList())) - .get().stream().map(d -> d.definition().name()).sorted() - .toList(); - var poolNames = appPipeline.fire(new GetPools().accessibleFor( - WebConsoleUtils.userFromSession(session) - .map(ConsoleUser::getName).orElse(null), - WebConsoleUtils.rolesFromSession(session).stream() - .map(ConsoleRole::getName).toList())) - .get().stream().map(VmPool::name).sorted().toList(); - Template tpl - = freemarkerConfig().getTemplate("VmAccess-edit.ftl.html"); - var fmModel = fmModel(event, channel, conletId, model); - fmModel.put("vmNames", vmNames); - fmModel.put("poolNames", poolNames); - channel.respond(new OpenModalDialog(type(), conletId, - processTemplate(event, tpl, fmModel)) - .addOption("cancelable", true) - .addOption("okayLabel", - resourceBundle.getString("okayLabel"))); - } - return renderedAs; - } - - @SuppressWarnings("unchecked") - private Set renderPreview(RenderConletRequestBase event, - ConsoleConnection channel, String conletId, ResourceModel model) - throws TemplateNotFoundException, MalformedTemplateNameException, - ParseException, IOException, InterruptedException { - channel.associated(PENDING, Event.class) - .ifPresent(e -> { - e.resumeHandling(); - channel.setAssociated(PENDING, null); - }); - - VmDefinition vmDef = null; - if (model.mode() == ResourceModel.Mode.VM && model.name() != null) { - // Remove conlet if VM definition has been removed - // or user has not at least one permission - vmDef = getVmData(model, channel).map(VmData::definition) - .orElse(null); - if (vmDef == null) { - channel.respond( - new DeleteConlet(conletId, Collections.emptySet())); - return Collections.emptySet(); - } - } - - if (model.mode() == ResourceModel.Mode.POOL && model.name() != null) { - // Remove conlet if pool definition has been removed - // or user has not at least one permission - VmPool pool = appPipeline - .fire(new GetPools().withName(model.name())).get() - .stream().findFirst().orElse(null); - if (pool == null - || permissions(pool, channel.session()).isEmpty()) { - channel.respond( - new DeleteConlet(conletId, Collections.emptySet())); - return Collections.emptySet(); - } - vmDef = getVmData(model, channel).map(VmData::definition) - .orElse(null); - } - - // Render - Template tpl - = freemarkerConfig().getTemplate("VmAccess-preview.ftl.html"); - channel.respond(new RenderConlet(type(), conletId, - processTemplate(event, tpl, - fmModel(event, channel, conletId, model))) - .setRenderAs( - RenderMode.Preview.addModifiers(event.renderAs())) - .setSupportedModes(syncPreviews(channel.session()) - ? MODES_FOR_GENERATED - : MODES)); - if (!Strings.isNullOrEmpty(model.name())) { - Optional.ofNullable(channel.session().get(RENDERED)) - .ifPresent(s -> ((Set) s).add(model)); - updatePreview(channel, model, vmDef); - } - return EnumSet.of(RenderMode.Preview); - } - - private Optional getVmData(ResourceModel model, - ConsoleConnection channel) throws InterruptedException { - if (model.mode() == ResourceModel.Mode.VM) { - // Get the VM data by name. - var session = channel.session(); - return appPipeline.fire(new GetVms().withName(model.name()) - .accessibleFor(WebConsoleUtils.userFromSession(session) - .map(ConsoleUser::getName).orElse(null), - WebConsoleUtils.rolesFromSession(session).stream() - .map(ConsoleRole::getName).toList())) - .get().stream().findFirst(); - } - - // Look for an (already) assigned VM - var user = WebConsoleUtils.userFromSession(channel.session()) - .map(ConsoleUser::getName).orElse(null); - return appPipeline.fire(new GetVms().assignedFrom(model.name()) - .assignedTo(user)).get().stream().findFirst(); - } - - /** - * Returns the permissions from the VM definition. - * - * @param vmDef the VM definition - * @param session the session - * @return the sets the - */ - private Set permissions(VmDefinition vmDef, Session session) { - var user = WebConsoleUtils.userFromSession(session) - .map(ConsoleUser::getName).orElse(null); - var roles = WebConsoleUtils.rolesFromSession(session) - .stream().map(ConsoleRole::getName).toList(); - return vmDef.permissionsFor(user, roles); - } - - /** - * Returns the permissions from the pool. - * - * @param pool the pool - * @param session the session - * @return the sets the - */ - private Set permissions(VmPool pool, Session session) { - var user = WebConsoleUtils.userFromSession(session) - .map(ConsoleUser::getName).orElse(null); - var roles = WebConsoleUtils.rolesFromSession(session) - .stream().map(ConsoleRole::getName).toList(); - return pool.permissionsFor(user, roles); - } - - /** - * Returns the permissions from the VM definition or the pool depending - * on the state of the model. - * - * @param session the session - * @param model the model - * @param vmDef the vm def - * @return the sets the - * @throws InterruptedException the interrupted exception - */ - private Set permissions(Session session, ResourceModel model, - VmDefinition vmDef) throws InterruptedException { - var user = WebConsoleUtils.userFromSession(session) - .map(ConsoleUser::getName).orElse(null); - var roles = WebConsoleUtils.rolesFromSession(session) - .stream().map(ConsoleRole::getName).toList(); - if (model.mode() == ResourceModel.Mode.POOL) { - // Use permissions from pool - var pool = appPipeline.fire(new GetPools().withName(model.name())) - .get().stream().findFirst().orElse(null); - if (pool == null) { - return Collections.emptySet(); - } - return pool.permissionsFor(user, roles); - } - - // Use permissions from VM - if (vmDef == null) { - vmDef = appPipeline.fire(new GetVms().assignedFrom(model.name()) - .assignedTo(user)).get().stream().map(VmData::definition) - .findFirst().orElse(null); - } - if (vmDef == null) { - return Collections.emptySet(); - } - return vmDef.permissionsFor(user, roles); - } - - private void updatePreview(ConsoleConnection channel, ResourceModel model, - VmDefinition vmDef) throws InterruptedException { - updateConfig(channel, model, vmDef); - updateVmDef(channel, model, vmDef); - } - - private void updateConfig(ConsoleConnection channel, ResourceModel model, - VmDefinition vmDef) throws InterruptedException { - channel.respond(new NotifyConletView(type(), - model.getConletId(), "updateConfig", model.mode(), model.name(), - permissions(channel.session(), model, vmDef).stream() - .map(VmDefinition.Permission::toString).toList())); - } - - private void updateVmDef(ConsoleConnection channel, ResourceModel model, - VmDefinition vmDef) throws InterruptedException { - Map data = null; - if (vmDef == null) { - model.setAssignedVm(null); - } else { - model.setAssignedVm(vmDef.name()); - var session = channel.session(); - var user = WebConsoleUtils.userFromSession(session) - .map(ConsoleUser::getName).orElse(null); - var perms = permissions(session, model, vmDef); - try { - data = Map.of( - "metadata", Map.of("namespace", vmDef.namespace(), - "name", vmDef.name()), - "spec", vmDef.spec(), - "status", vmDef.status(), - "consoleAccessible", vmDef.consoleAccessible(user, perms)); - } catch (JsonSyntaxException e) { - logger.log(Level.SEVERE, e, - () -> "Failed to serialize VM definition"); - return; - } - } - channel.respond(new NotifyConletView(type(), - model.getConletId(), "updateVmDefinition", data)); - } - - @Override - protected void doConletDeleted(ConletDeleted event, - ConsoleConnection channel, String conletId, - ResourceModel conletState) - throws Exception { - if (event.renderModes().isEmpty()) { - channel.respond(new KeyValueStoreUpdate().delete( - storagePath(channel.session(), conletId))); - } - } - - /** - * Track the VM definitions and update conlets. - * - * @param event the event - * @param channel the channel - * @throws IOException - * @throws InterruptedException - */ - @Handler(namedChannels = "manager") - @SuppressWarnings({ "PMD.CognitiveComplexity", - "PMD.AvoidInstantiatingObjectsInLoops" }) - public void onVmResourceChanged(VmResourceChanged event, VmChannel channel) - throws IOException, InterruptedException { - var vmDef = event.vmDefinition(); - - // Update known conlets - for (var entry : conletIdsByConsoleConnection().entrySet()) { - var connection = entry.getKey(); - var user = WebConsoleUtils.userFromSession(connection.session()) - .map(ConsoleUser::getName).orElse(null); - for (var conletId : entry.getValue()) { - var model = stateFromSession(connection.session(), conletId); - if (model.isEmpty() - || Strings.isNullOrEmpty(model.get().name())) { - continue; - } - if (model.get().mode() == ResourceModel.Mode.VM) { - // Check if this VM is used by conlet - if (!Objects.areEqual(model.get().name(), vmDef.name())) { - continue; - } - if (event.type() == K8sObserver.ResponseType.DELETED - || permissions(vmDef, connection.session()).isEmpty()) { - connection.respond( - new DeleteConlet(conletId, Collections.emptySet())); - continue; - } - } else { - // Check if VM is used by pool conlet or to be assigned to - // it - var toBeUsedByConlet = vmDef.assignment() - .map(Assignment::pool) - .map(p -> p.equals(model.get().name())).orElse(false) - && vmDef.assignment().map(Assignment::user) - .map(u -> u.equals(user)).orElse(false); - if (!Objects.areEqual(model.get().assignedVm(), - vmDef.name()) && !toBeUsedByConlet) { - continue; - } - - // Now unassigned if VM is deleted or no longer to be used - if (event.type() == K8sObserver.ResponseType.DELETED - || !toBeUsedByConlet) { - updateVmDef(connection, model.get(), null); - continue; - } - } - - // Full update because permissions may have changed - updatePreview(connection, model.get(), vmDef); - } - } - } - - /** - * On vm pool changed. - * - * @param event the event - * @throws InterruptedException the interrupted exception - */ - @Handler(namedChannels = "manager") - @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") - public void onVmPoolChanged(VmPoolChanged event) - throws InterruptedException { - var poolName = event.vmPool().name(); - // Update known conlets - for (var entry : conletIdsByConsoleConnection().entrySet()) { - var connection = entry.getKey(); - for (var conletId : entry.getValue()) { - var model = stateFromSession(connection.session(), conletId); - if (model.isEmpty() - || model.get().mode() != ResourceModel.Mode.POOL - || !Objects.areEqual(model.get().name(), poolName)) { - continue; - } - if (event.deleted() - || permissions(event.vmPool(), connection.session()) - .isEmpty()) { - connection.respond( - new DeleteConlet(conletId, Collections.emptySet())); - continue; - } - updateConfig(connection, model.get(), null); - } - } - } - - @SuppressWarnings({ "PMD.NcssCount", "PMD.CognitiveComplexity", - "PMD.AvoidLiteralsInIfCondition" }) - @Override - protected void doUpdateConletState(NotifyConletModel event, - ConsoleConnection channel, ResourceModel model) throws Exception { - event.stop(); - if ("selectedResource".equals(event.method())) { - selectResource(event, channel, model); - return; - } - - Optional vmData = getVmData(model, channel); - if (vmData.isEmpty()) { - if (model.mode() == ResourceModel.Mode.VM) { - return; - } - if ("start".equals(event.method())) { - // Assign a VM. - var user = WebConsoleUtils.userFromSession(channel.session()) - .map(ConsoleUser::getName).orElse(null); - vmData = Optional.ofNullable(appPipeline - .fire(new AssignVm(model.name(), user)).get()); - if (vmData.isEmpty()) { - ResourceBundle resourceBundle - = resourceBundle(channel.locale()); - channel.respond(new DisplayNotification( - resourceBundle.getString("poolEmptyNotification"), - Map.of("autoClose", 10_000, "type", "Error"))); - return; - } - } - } - - // Handle command for selected VM - var vmChannel = vmData.get().channel(); - var vmDef = vmData.get().definition(); - var vmName = vmDef.metadata().getName(); - var perms = permissions(channel.session(), model, vmDef); - var resourceBundle = resourceBundle(channel.locale()); - switch (event.method()) { - case "start": - if (perms.contains(VmDefinition.Permission.START)) { - vmChannel.fire(new ModifyVm(vmName, "state", "Running")); - } - break; - case "stop": - if (perms.contains(VmDefinition.Permission.STOP)) { - vmChannel.fire(new ModifyVm(vmName, "state", "Stopped")); - } - break; - case "reset": - if (perms.contains(VmDefinition.Permission.RESET)) { - confirmReset(event, channel, model, resourceBundle); - } - break; - case "resetConfirmed": - if (perms.contains(VmDefinition.Permission.RESET)) { - vmChannel.fire(new ResetVm(vmName)); - } - break; - case "openConsole": - openConsole(channel, model, vmChannel, vmDef, perms); - break; - default:// ignore - break; - } - } - - private void confirmReset(NotifyConletModel event, - ConsoleConnection channel, ResourceModel model, - ResourceBundle resourceBundle) throws TemplateNotFoundException, - MalformedTemplateNameException, ParseException, IOException { - Template tpl = freemarkerConfig() - .getTemplate("VmAccess-confirmReset.ftl.html"); - channel.respond(new OpenModalDialog(type(), model.getConletId(), - processTemplate(event, tpl, - fmModel(event, channel, model.getConletId(), model))) - .addOption("cancelable", true).addOption("closeLabel", "") - .addOption("title", - resourceBundle.getString("confirmResetTitle"))); - } - - private void openConsole(ConsoleConnection channel, ResourceModel model, - VmChannel vmChannel, VmDefinition vmDef, Set perms) { - var resourceBundle = resourceBundle(channel.locale()); - var user = WebConsoleUtils.userFromSession(channel.session()) - .map(ConsoleUser::getName).orElse(""); - if (!vmDef.consoleAccessible(user, perms)) { - channel.respond(new DisplayNotification( - resourceBundle.getString("consoleInaccessibleNotification"), - Map.of("autoClose", 5_000, "type", "Warning"))); - return; - } - var pwQuery = Event.onCompletion(new GetDisplaySecret(vmDef, user), - e -> gotPassword(channel, model, vmDef, e)); - vmChannel.fire(pwQuery); - } - - private void gotPassword(ConsoleConnection channel, ResourceModel model, - VmDefinition vmDef, GetDisplaySecret event) { - if (!event.secretAvailable()) { - return; - } - vmDef.extra().connectionFile(event.secret(), - preferredIpVersion, deleteConnectionFile) - .ifPresent(cf -> channel.respond(new NotifyConletView(type(), - model.getConletId(), "openConsole", cf))); - } - - @SuppressWarnings({ "PMD.UseLocaleWithCaseConversions" }) - private void selectResource(NotifyConletModel event, - ConsoleConnection channel, ResourceModel model) - throws JsonProcessingException, InterruptedException { - try { - model.setMode(ResourceModel.Mode - .valueOf(event. param(0).toUpperCase())); - model.setName(event.param(1)); - String jsonState = objectMapper.writeValueAsString(model); - channel.respond(new KeyValueStoreUpdate().update(storagePath( - channel.session(), model.getConletId()), jsonState)); - updatePreview(channel, model, - getVmData(model, channel).map(VmData::definition).orElse(null)); - } catch (IllegalArgumentException e) { - logger.warning(() -> "Invalid resource type: " + e.getMessage()); - } - } - - @Override - protected boolean doSetLocale(SetLocale event, ConsoleConnection channel, - String conletId) throws Exception { - return true; - } - - /** - * The Class AccessModel. - */ - public static class ResourceModel extends ConletBaseModel { - - /** - * The Enum ResourceType. - */ - @SuppressWarnings("PMD.ShortVariable") - public enum Mode { - VM, POOL - } - - private Mode mode; - private String name; - private String assignedVm; - - /** - * Instantiates a new resource model. - * - * @param conletId the conlet id - */ - public ResourceModel(@JsonProperty("conletId") String conletId) { - super(conletId); - } - - /** - * Returns the mode. - * - * @return the resourceType - */ - @JsonGetter("mode") - public Mode mode() { - return mode; - } - - /** - * Sets the mode. - * - * @param mode the resource mode to set - */ - public void setMode(Mode mode) { - this.mode = mode; - } - - /** - * Gets the resource name. - * - * @return the string - */ - @JsonGetter("name") - public String name() { - return name; - } - - /** - * Sets the name. - * - * @param name the resource name to set - */ - public void setName(String name) { - this.name = name; - } - - /** - * Gets the assigned vm. - * - * @return the string - */ - @JsonGetter("assignedVm") - public String assignedVm() { - return assignedVm; - } - - /** - * Sets the assigned vm. - * - * @param name the assigned vm - */ - public void setAssignedVm(String name) { - this.assignedVm = name; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = super.hashCode(); - result = prime * result + java.util.Objects.hash(mode, name); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (!super.equals(obj)) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - ResourceModel other = (ResourceModel) obj; - return mode == other.mode - && java.util.Objects.equals(name, other.name); - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(50); - builder.append("AccessModel [mode=").append(mode) - .append(", name=").append(name).append(']'); - return builder.toString(); - } - } -} diff --git a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/browser/VmAccess-functions.ts b/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/browser/VmAccess-functions.ts deleted file mode 100644 index 47e6e11..0000000 --- a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/browser/VmAccess-functions.ts +++ /dev/null @@ -1,306 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2024,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 . - */ - -import { - reactive, ref, createApp, computed, watch -} from "vue"; -import JGConsole from "jgconsole"; -import JgwcPlugin, { JGWC } from "jgwc"; -import { provideApi, getApi } from "aash-plugin"; -import l10nBundles from "l10nBundles"; - -import "./VmAccess-style.scss"; - -// For global access -declare global { - interface Window { - orgJDrupesVmOperatorVmAccess: { - initPreview?: (previewDom: HTMLElement, isUpdate: boolean) => void, - initEdit?: (viewDom: HTMLElement, isUpdate: boolean) => void, - applyEdit?: (viewDom: HTMLElement, apply: boolean) => void, - confirmReset?: (conletType: string, conletId: string) => void - } - } -} - -window.orgJDrupesVmOperatorVmAccess = {}; - -interface Api { - /* eslint-disable @typescript-eslint/no-explicit-any */ - vmName: string; - vmDefinition: any; - poolName: string | null; - permissions: string[]; -} - -const localize = (key: string) => { - return JGConsole.localize( - l10nBundles, JGWC.lang(), key); -}; - -window.orgJDrupesVmOperatorVmAccess.initPreview = (previewDom: HTMLElement, - _isUpdate: boolean) => { - const app = createApp({ - setup(_props: object) { - const conletId = (previewDom.closest( - "[data-conlet-id]")!).dataset["conletId"]!; - const resourceBase = (previewDom.closest( - "*[data-conlet-resource-base]")!).dataset.conletResourceBase; - - const previewApi: Api = reactive({ - vmName: "", - vmDefinition: {}, - poolName: null, - permissions: [] - }); - const poolName = computed(() => previewApi.poolName); - const vmName = computed(() => previewApi.vmDefinition.name); - const configured = computed(() => previewApi.vmDefinition.spec); - const accessible = computed(() => previewApi.vmDefinition.consoleAccessible); - const busy = computed(() => previewApi.vmDefinition.spec - && (previewApi.vmDefinition.spec.vm.state === 'Running' - && (!previewApi.vmDefinition.consoleAccessible) - || previewApi.vmDefinition.spec.vm.state === 'Stopped' - && previewApi.vmDefinition.running)); - const startable = computed(() => previewApi.vmDefinition.spec - && previewApi.vmDefinition.spec.vm.state !== 'Running' - && !previewApi.vmDefinition.running - && previewApi.permissions.includes('start') - || previewApi.poolName !== null && !previewApi.vmDefinition.name); - const stoppable = computed(() => previewApi.vmDefinition.spec && - previewApi.vmDefinition.spec.vm.state !== 'Stopped' - && previewApi.vmDefinition.running); - const running = computed(() => previewApi.vmDefinition.running); - const inUse = computed(() => previewApi.vmDefinition.usedBy != ''); - const permissions = computed(() => previewApi.permissions); - const osicon = computed(() => { - if (!previewApi.vmDefinition.status?.osinfo?.id) { - return null; - } - switch(previewApi.vmDefinition.status.osinfo.id) { - case "almalinux": return "almalinux.svg"; - case "arch": return "arch.svg"; - case "debian": return "debian.svg"; - case "fedora": return "fedora.svg"; - case "mswindows": return "windows.svg"; - case "ubuntu": return "ubuntu.svg"; - default: { - if ((previewApi.vmDefinition.status.osinfo.name || "") - .toLowerCase().includes("linux")) { - return "tux.svg"; - } - return "unknown.svg"; - } - } - }); - - watch(previewApi, (api: Api) => { - JGConsole.instance.updateConletTitle(conletId, - api.poolName || api.vmDefinition.name || ""); - }); - - provideApi(previewDom, previewApi); - - const vmAction = (action: string) => { - JGConsole.notifyConletModel(conletId, action); - }; - - return { localize, resourceBase, vmAction, poolName, vmName, - configured, accessible, busy, startable, stoppable, running, - inUse, permissions, osicon }; - }, - template: ` - - - - - - - - - - - -
{{ vmName }}
- - - - - - - - -
` - }); - app.use(JgwcPlugin, []); - app.config.globalProperties.window = window; - app.mount(previewDom); -}; - -JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmaccess.VmAccess", - "updateConfig", - function(conletId: string, type: string, resource: string, - permissions: []) { - const conlet = JGConsole.findConletPreview(conletId); - if (!conlet) { - return; - } - const api = getApi(conlet.element().querySelector( - ":scope .jdrupes-vmoperator-vmaccess-preview"))!; - if (type === "VM") { - api.vmName = resource; - api.poolName = ""; - } else { - api.poolName = resource; - api.vmName = ""; - } - api.permissions = permissions; - }); - -JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmaccess.VmAccess", - "updateVmDefinition", function(conletId: string, vmDefinition: any | null) { - const conlet = JGConsole.findConletPreview(conletId); - if (!conlet) { - return; - } - const api = getApi(conlet.element().querySelector( - ":scope .jdrupes-vmoperator-vmaccess-preview"))!; - if (vmDefinition) { - // Add some short-cuts for rendering - vmDefinition.name = vmDefinition.metadata.name; - vmDefinition.currentCpus = vmDefinition.status.cpus; - vmDefinition.currentRam = Number(vmDefinition.status.ram); - vmDefinition.usedBy = vmDefinition.status.consoleClient || ""; - // safety fallbacks - vmDefinition.status.conditions.forEach((condition: any) => { - if (condition.type === "Running") { - vmDefinition.running = condition.status === "True"; - vmDefinition.runningConditionSince - = new Date(condition.lastTransitionTime); - } - }) - } else { - vmDefinition = {}; - } - api.vmDefinition = vmDefinition; - }); - -JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmaccess.VmAccess", - "openConsole", function(_conletId: string, data: string) { - let target = document.getElementById( - "org.jdrupes.vmoperator.vmaccess.VmAccess.target"); - if (!target) { - target = document.createElement("iframe"); - target.id = "org.jdrupes.vmoperator.vmaccess.VmAccess.target"; - target.setAttribute("name", target.id); - target.setAttribute("style", "display: none;"); - document.querySelector("body")!.append(target); - } - const url = "data:application/x-virt-viewer;base64," - + window.btoa(data); - window.open(url, target.id); - }); - -window.orgJDrupesVmOperatorVmAccess.initEdit = (dialogDom: HTMLElement, - isUpdate: boolean) => { - if (isUpdate) { - return; - } - const app = createApp({ - setup() { - const formId = (dialogDom - .closest("*[data-conlet-id]")!).id + "-form"; - - const localize = (key: string) => { - return JGConsole.localize( - l10nBundles, JGWC.lang()!, key); - }; - - const resource = ref("vm"); - const vmNameInput = ref(""); - const poolNameInput = ref(""); - - watch(resource, (resource: string) => { - if (resource === "vm") { - poolNameInput.value = ""; - } - if (resource === "pool") - vmNameInput.value = ""; - }); - - const conletId = (dialogDom.closest( - "[data-conlet-id]")!).dataset["conletId"]!; - const conlet = JGConsole.findConletPreview(conletId); - if (conlet) { - const api = getApi(conlet.element().querySelector( - ":scope .jdrupes-vmoperator-vmaccess-preview"))!; - if (api.poolName) { - resource.value = "pool"; - } - vmNameInput.value = api.vmName; - poolNameInput.value = api.poolName; - } - - provideApi(dialogDom, { resource: () => resource.value, - name: () => resource.value === "vm" - ? vmNameInput.value : poolNameInput.value }); - - return { formId, localize, resource, vmNameInput, poolNameInput }; - } - }); - app.use(JgwcPlugin); - app.mount(dialogDom); -} - -window.orgJDrupesVmOperatorVmAccess.applyEdit = - (dialogDom: HTMLElement, apply: boolean) => { - if (!apply) { - return; - } - const conletId = (dialogDom.closest("[data-conlet-id]")!) - .dataset["conletId"]!; - const editApi = getApi>(dialogDom!)!; - JGConsole.notifyConletModel(conletId, "selectedResource", editApi.resource(), - editApi.name()); -} - -window.orgJDrupesVmOperatorVmAccess.confirmReset = - (conletType: string, conletId: string) => { - JGConsole.instance.closeModalDialog(conletType, conletId); - JGConsole.notifyConletModel(conletId, "resetConfirmed"); -} \ No newline at end of file diff --git a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/browser/VmAccess-style.scss b/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/browser/VmAccess-style.scss deleted file mode 100644 index 3a291dd..0000000 --- a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/browser/VmAccess-style.scss +++ /dev/null @@ -1,124 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -/* - * Conlet specific styles. - */ -.jdrupes-vmoperator-vmaccess { - - span[role="button"].svg-icon { - display: inline-block; - line-height: 1; - - /* Align with forkawesome */ - font-size: 14px; - fill: var(--primary); - - &[aria-disabled="true"], &[aria-disabled=""] { - fill: var(--disabled); - } - - svg { - height: 2ex; - width: 1em; - } - } - - [role=button] { - padding: 0.25rem; - - &:not([aria-disabled]):hover, &[aria-disabled='false']:hover { - box-shadow: var(--darkening); - } - } -} - -.jdrupes-vmoperator-vmaccess.jdrupes-vmoperator-vmaccess-preview { - - table { - border-spacing: 0; - } - - img { - display: block; - height: 3em; - padding: 0.25rem; - - &[aria-disabled=''], &[aria-disabled='true'] { - opacity: 0.4; - } - } - - .jdrupes-vmoperator-vmaccess-preview-action-list { - white-space: nowrap; - } - - span.busy::before { - font: normal normal normal 14px/1 ForkAwesome; - font-size: 1.125em; - content: "\f1ce"; - left: 1.45em; - top: 0.7em; - color: var(--info); - position: absolute; - animation: spin 2s linear infinite; - z-index: 100; - pointer-events: none; - } - - span.osicon { - width: 4.25em; - height: 3em; - padding: 0.25rem; - pointer-events: none; - - img { - display: block; - height: 1.75em; - margin: 0.2em auto 0; - pointer-events: none; - } - } -} - -.jdrupes-vmoperator-vmaccess.jdrupes-vmoperator-vmaccess-edit { - - fieldset ul li { - margin-top: 0.5em; - } - - select { - width: 15em; - } -} - -.jdrupes-vmoperator-vmaccess.jdrupes-vmoperator-vmaccess-confirm-reset { - p { - text-align: center; - } - - span[role="button"].svg-icon { - fill: var(--danger); - - svg { - width: 2.5em; - height: 2.5em; - } - } - -} diff --git a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/package-info.java b/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/package-info.java deleted file mode 100644 index 745ded7..0000000 --- a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/package-info.java +++ /dev/null @@ -1,19 +0,0 @@ -/* - * 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.vmaccess; diff --git a/org.jdrupes.vmoperator.vmaccess/.checkstyle b/org.jdrupes.vmoperator.vmconlet/.checkstyle similarity index 100% rename from org.jdrupes.vmoperator.vmaccess/.checkstyle rename to org.jdrupes.vmoperator.vmconlet/.checkstyle diff --git a/spice-squid/.eclipse-pmd b/org.jdrupes.vmoperator.vmconlet/.eclipse-pmd similarity index 76% rename from spice-squid/.eclipse-pmd rename to org.jdrupes.vmoperator.vmconlet/.eclipse-pmd index 5d69caa..8b394f8 100644 --- a/spice-squid/.eclipse-pmd +++ b/org.jdrupes.vmoperator.vmconlet/.eclipse-pmd @@ -2,6 +2,6 @@ - + diff --git a/org.jdrupes.vmoperator.vmaccess/.gitignore b/org.jdrupes.vmoperator.vmconlet/.gitignore similarity index 100% rename from org.jdrupes.vmoperator.vmaccess/.gitignore rename to org.jdrupes.vmoperator.vmconlet/.gitignore diff --git a/org.jdrupes.vmoperator.vmaccess/.settings/org.eclipse.buildship.core.prefs b/org.jdrupes.vmoperator.vmconlet/.settings/org.eclipse.buildship.core.prefs similarity index 100% rename from org.jdrupes.vmoperator.vmaccess/.settings/org.eclipse.buildship.core.prefs rename to org.jdrupes.vmoperator.vmconlet/.settings/org.eclipse.buildship.core.prefs diff --git a/org.jdrupes.vmoperator.vmaccess/.settings/org.eclipse.core.resources.prefs b/org.jdrupes.vmoperator.vmconlet/.settings/org.eclipse.core.resources.prefs similarity index 100% rename from org.jdrupes.vmoperator.vmaccess/.settings/org.eclipse.core.resources.prefs rename to org.jdrupes.vmoperator.vmconlet/.settings/org.eclipse.core.resources.prefs diff --git a/org.jdrupes.vmoperator.vmaccess/.settings/org.eclipse.core.runtime.prefs b/org.jdrupes.vmoperator.vmconlet/.settings/org.eclipse.core.runtime.prefs similarity index 100% rename from org.jdrupes.vmoperator.vmaccess/.settings/org.eclipse.core.runtime.prefs rename to org.jdrupes.vmoperator.vmconlet/.settings/org.eclipse.core.runtime.prefs diff --git a/org.jdrupes.vmoperator.vmaccess/.settings/org.eclipse.jdt.ui.prefs b/org.jdrupes.vmoperator.vmconlet/.settings/org.eclipse.jdt.ui.prefs similarity index 100% rename from org.jdrupes.vmoperator.vmaccess/.settings/org.eclipse.jdt.ui.prefs rename to org.jdrupes.vmoperator.vmconlet/.settings/org.eclipse.jdt.ui.prefs diff --git a/org.jdrupes.vmoperator.vmmgmt/build.gradle b/org.jdrupes.vmoperator.vmconlet/build.gradle similarity index 86% rename from org.jdrupes.vmoperator.vmmgmt/build.gradle rename to org.jdrupes.vmoperator.vmconlet/build.gradle index 606c6cd..2031651 100644 --- a/org.jdrupes.vmoperator.vmmgmt/build.gradle +++ b/org.jdrupes.vmoperator.vmconlet/build.gradle @@ -5,11 +5,9 @@ plugins { dependencies { implementation project(':org.jdrupes.vmoperator.manager.events') - implementation 'org.jgrapes:org.jgrapes.webconsole.base:[2.1.0,3)' + implementation 'org.jgrapes:org.jgrapes.webconsole.base:[1.2.0,2)' implementation 'org.jgrapes:org.jgrapes.webconsole.provider.vue:[1,2)' - implementation 'org.jgrapes:org.jgrapes.webconsole.provider.jgwcvuecomponents:[1.2,2)' - implementation 'org.jgrapes:org.jgrapes.webconsole.provider.chartjs:[1.2,2)' - + implementation 'org.jgrapes:org.jgrapes.webconsole.provider.jgwcvuecomponents:[1,2)' } apply plugin: 'com.github.node-gradle.node' @@ -24,7 +22,6 @@ task extractDependencies(type: Copy) { || it.name.contains('org.jgrapes.webconsole.base') } .collect{ zipTree (it) } - exclude '*.class' into 'build/unpacked' duplicatesStrategy 'include' } diff --git a/org.jdrupes.vmoperator.vmaccess/package.json b/org.jdrupes.vmoperator.vmconlet/package.json similarity index 100% rename from org.jdrupes.vmoperator.vmaccess/package.json rename to org.jdrupes.vmoperator.vmconlet/package.json diff --git a/org.jdrupes.vmoperator.vmconlet/resources/META-INF/services/org.jgrapes.webconsole.base.ConletComponentFactory b/org.jdrupes.vmoperator.vmconlet/resources/META-INF/services/org.jgrapes.webconsole.base.ConletComponentFactory new file mode 100644 index 0000000..5a22dc7 --- /dev/null +++ b/org.jdrupes.vmoperator.vmconlet/resources/META-INF/services/org.jgrapes.webconsole.base.ConletComponentFactory @@ -0,0 +1 @@ +org.jdrupes.vmoperator.vmconlet.VmConletFactory diff --git a/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/VmMgmt-l10nBundles.ftl.js b/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/VmConlet-l10nBundles.ftl.js similarity index 100% rename from org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/VmMgmt-l10nBundles.ftl.js rename to org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/VmConlet-l10nBundles.ftl.js diff --git a/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/VmConlet-preview.ftl.html b/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/VmConlet-preview.ftl.html new file mode 100644 index 0000000..7fa0a7f --- /dev/null +++ b/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/VmConlet-preview.ftl.html @@ -0,0 +1,5 @@ +
+
Preview
+
diff --git a/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/VmConlet-view.ftl.html b/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/VmConlet-view.ftl.html new file mode 100644 index 0000000..5e7d4b9 --- /dev/null +++ b/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/VmConlet-view.ftl.html @@ -0,0 +1,84 @@ +
+ + + + + + + + + + + +
+ {{ localize(controller.label(key)) }} + + {{ localize("vmActions") }} +
+
diff --git a/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/l10n.properties b/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/l10n.properties new file mode 100644 index 0000000..d77bf1a --- /dev/null +++ b/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/l10n.properties @@ -0,0 +1,11 @@ +conletName = VM Viewer + +currentCpus = Current CPUs +currentRam = Current RAM +maximumCpus = Maximum CPUs +maximumRam = Maximum RAM +requestedCpus = Requested CPUs +requestedRam = Requested RAM +running = Running +vmActions = Actions +vmname = Name diff --git a/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/l10n_de.properties b/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/l10n_de.properties new file mode 100644 index 0000000..5d8638a --- /dev/null +++ b/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/l10n_de.properties @@ -0,0 +1,17 @@ +conletName = VM Anzeige + +running = Gestartet +currentCpus = Aktuelle CPUs +currentRam = Akuelles RAM +maximumCpus = Maximale CPUs +maximumRam = Maximales RAM +requestedCpus = Angeforderte CPUs +requestedRam = Angefordertes RAM +vmActions = Aktionen +vmname = Name + +Start\ VM = VM Starten +Stop\ VM = VM Anhalten + +Yes = Ja +No = Nein diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/l10n_en.properties b/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/l10n_en.properties similarity index 100% rename from org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/l10n_en.properties rename to org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/l10n_en.properties diff --git a/org.jdrupes.vmoperator.vmaccess/rollup.config.mjs b/org.jdrupes.vmoperator.vmconlet/rollup.config.mjs similarity index 85% rename from org.jdrupes.vmoperator.vmaccess/rollup.config.mjs rename to org.jdrupes.vmoperator.vmconlet/rollup.config.mjs index ab1aae9..7565030 100644 --- a/org.jdrupes.vmoperator.vmaccess/rollup.config.mjs +++ b/org.jdrupes.vmoperator.vmconlet/rollup.config.mjs @@ -1,8 +1,8 @@ import typescript from 'rollup-plugin-typescript2'; import postcss from 'rollup-plugin-postcss'; -let packagePath = "org/jdrupes/vmoperator/vmaccess"; -let baseName = "VmAccess" +let packagePath = "org/jdrupes/vmoperator/vmconlet"; +let baseName = "VmConlet" let module = "build/generated/resources/" + packagePath + "/" + baseName + "-functions.js"; @@ -15,7 +15,7 @@ let pathsMap = { } export default { - external: ['aash-plugin', 'jgconsole', 'jgwc', 'l10nBundles', 'vue', 'chartjs'], + external: ['vue', 'aash-plugin', 'jgconsole', 'jgwc', 'l10nBundles'], input: "src/" + packagePath + "/browser/" + baseName + "-functions.ts", output: [ { diff --git a/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/VmConlet.java b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/VmConlet.java new file mode 100644 index 0000000..c9d5fde --- /dev/null +++ b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/VmConlet.java @@ -0,0 +1,247 @@ +/* + * 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.vmconlet; + +import com.google.gson.JsonObject; +import freemarker.core.ParseException; +import freemarker.template.MalformedTemplateNameException; +import freemarker.template.Template; +import freemarker.template.TemplateNotFoundException; +import io.kubernetes.client.custom.Quantity; +import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; +import java.io.IOException; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.jdrupes.json.JsonBeanDecoder; +import org.jdrupes.json.JsonDecodeException; +import org.jdrupes.vmoperator.manager.events.StartVm; +import org.jdrupes.vmoperator.manager.events.StopVm; +import org.jdrupes.vmoperator.manager.events.VmChannel; +import org.jdrupes.vmoperator.manager.events.VmDefChanged; +import org.jdrupes.vmoperator.manager.events.VmDefChanged.Type; +import org.jdrupes.vmoperator.util.GsonPtr; +import org.jgrapes.core.Channel; +import org.jgrapes.core.Event; +import org.jgrapes.core.Manager; +import org.jgrapes.core.NamedChannel; +import org.jgrapes.core.annotation.Handler; +import org.jgrapes.webconsole.base.Conlet.RenderMode; +import org.jgrapes.webconsole.base.ConletBaseModel; +import org.jgrapes.webconsole.base.ConsoleConnection; +import org.jgrapes.webconsole.base.events.AddConletRequest; +import org.jgrapes.webconsole.base.events.AddConletType; +import org.jgrapes.webconsole.base.events.AddPageResources.ScriptResource; +import org.jgrapes.webconsole.base.events.ConsoleReady; +import org.jgrapes.webconsole.base.events.NotifyConletModel; +import org.jgrapes.webconsole.base.events.NotifyConletView; +import org.jgrapes.webconsole.base.events.RenderConlet; +import org.jgrapes.webconsole.base.events.RenderConletRequestBase; +import org.jgrapes.webconsole.base.events.SetLocale; +import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet; + +/** + */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") +public class VmConlet extends FreeMarkerConlet { + + private static final Set MODES = RenderMode.asSet( + RenderMode.Preview, RenderMode.View); + private final Map vmInfos + = new ConcurrentHashMap<>(); + + /** + * Creates a new component with its channel set to the given channel. + * + * @param componentChannel the channel that the component's handlers listen + * on by default and that {@link Manager#fire(Event, Channel...)} + * sends the event to + */ + public VmConlet(Channel componentChannel) { + super(componentChannel); + } + + /** + * On {@link ConsoleReady}, fire the {@link AddConletType}. + * + * @param event the event + * @param channel the channel + * @throws TemplateNotFoundException the template not found exception + * @throws MalformedTemplateNameException the malformed template name + * exception + * @throws ParseException the parse exception + * @throws IOException Signals that an I/O exception has occurred. + */ + @Handler + public void onConsoleReady(ConsoleReady event, ConsoleConnection channel) + throws TemplateNotFoundException, MalformedTemplateNameException, + ParseException, IOException { + // Add conlet resources to page + channel.respond(new AddConletType(type()) + .setDisplayNames( + localizations(channel.supportedLocales(), "conletName")) + .addRenderMode(RenderMode.Preview) + .addScript(new ScriptResource().setScriptType("module") + .setScriptUri(event.renderSupport().conletResource( + type(), "VmConlet-functions.js")))); + } + + @Override + protected Optional createNewState(AddConletRequest event, + ConsoleConnection connection, String conletId) throws Exception { + return Optional.of(new VmsModel(conletId)); + } + + @Override + @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") + protected Set doRenderConlet(RenderConletRequestBase event, + ConsoleConnection channel, String conletId, VmsModel conletState) + throws Exception { + Set renderedAs = new HashSet<>(); + boolean sendData = false; + if (event.renderAs().contains(RenderMode.Preview)) { + Template tpl + = freemarkerConfig().getTemplate("VmConlet-preview.ftl.html"); + channel.respond(new RenderConlet(type(), conletId, + processTemplate(event, tpl, + fmModel(event, channel, conletId, conletState))) + .setRenderAs( + RenderMode.Preview.addModifiers(event.renderAs())) + .setSupportedModes(MODES)); + renderedAs.add(RenderMode.View); + sendData = true; + } + if (event.renderAs().contains(RenderMode.View)) { + Template tpl + = freemarkerConfig().getTemplate("VmConlet-view.ftl.html"); + channel.respond(new RenderConlet(type(), conletId, + processTemplate(event, tpl, + fmModel(event, channel, conletId, conletState))) + .setRenderAs( + RenderMode.View.addModifiers(event.renderAs())) + .setSupportedModes(MODES)); + renderedAs.add(RenderMode.View); + sendData = true; + } + if (sendData) { + for (var vmInfo : vmInfos.values()) { + var def = JsonBeanDecoder.create(vmInfo.getRaw().toString()) + .readObject(); + channel.respond(new NotifyConletView(type(), + conletId, "updateVm", def)); + } + } + + return renderedAs; + } + + /** + * Track the VM definitions. + * + * @param event the event + * @param channel the channel + * @throws JsonDecodeException + */ + @Handler(namedChannels = "manager") + @SuppressWarnings({ "PMD.ConfusingTernary", + "PMD.AvoidInstantiatingObjectsInLoops" }) + public void onVmDefChanged(VmDefChanged event, VmChannel channel) + throws JsonDecodeException { + if (event.type() == Type.DELETED) { + vmInfos.remove(event.vmDefinition().getMetadata().getName()); + for (var entry : conletIdsByConsoleConnection().entrySet()) { + for (String conletId : entry.getValue()) { + entry.getKey().respond(new NotifyConletView(type(), + conletId, "removeVm")); + } + } + } else { + var vmDef = new DynamicKubernetesObject( + event.vmDefinition().getRaw().deepCopy()); + GsonPtr.to(vmDef.getRaw()).to("metadata").get(JsonObject.class) + .remove("managedFields"); + var vmSpec = GsonPtr.to(vmDef.getRaw()).to("spec", "vm"); + vmSpec.set("maximumRam", Quantity.fromString( + vmSpec.getAsString("maximumRam").orElse("0")).getNumber() + .toBigInteger().toString()); + vmSpec.set("currentRam", Quantity.fromString( + vmSpec.getAsString("currentRam").orElse("0")).getNumber() + .toBigInteger().toString()); + var status = GsonPtr.to(vmDef.getRaw()).to("status"); + status.set("ram", Quantity.fromString( + status.getAsString("ram").orElse("0")).getNumber() + .toBigInteger().toString()); + String vmName = event.vmDefinition().getMetadata().getName(); + vmInfos.put(vmName, vmDef); + + // Extract running + var def = JsonBeanDecoder.create(vmDef.getRaw().toString()) + .readObject(); + for (var entry : conletIdsByConsoleConnection().entrySet()) { + for (String conletId : entry.getValue()) { + entry.getKey().respond(new NotifyConletView(type(), + conletId, "updateVm", def)); + } + } + } + } + + @Override + protected void doUpdateConletState(NotifyConletModel event, + ConsoleConnection channel, VmsModel conletState) + throws Exception { + event.stop(); + switch (event.method()) { + case "start": + fire(new StartVm(event.params().asString(0), + new NamedChannel("manager"))); + break; + case "stop": + fire(new StopVm(event.params().asString(0), + new NamedChannel("manager"))); + break; + default:// ignore + break; + } + } + + @Override + protected boolean doSetLocale(SetLocale event, ConsoleConnection channel, + String conletId) throws Exception { + return true; + } + + /** + * The Class VmsModel. + */ + public class VmsModel extends ConletBaseModel { + + /** + * Instantiates a new vms model. + * + * @param conletId the conlet id + */ + public VmsModel(String conletId) { + super(conletId); + } + + } +} diff --git a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/VmAccessFactory.java b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/VmConletFactory.java similarity index 85% rename from org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/VmAccessFactory.java rename to org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/VmConletFactory.java index 5140056..d77ceb6 100644 --- a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/VmAccessFactory.java +++ b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/VmConletFactory.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.jdrupes.vmoperator.vmaccess; +package org.jdrupes.vmoperator.vmconlet; import java.util.Map; import java.util.Optional; @@ -25,9 +25,9 @@ import org.jgrapes.core.ComponentType; import org.jgrapes.webconsole.base.ConletComponentFactory; /** - * The factory service for {@link VmAccess}s. + * The factory service for {@link VmConlet}s. */ -public class VmAccessFactory implements ConletComponentFactory { +public class VmConletFactory implements ConletComponentFactory { /* * (non-Javadoc) @@ -36,7 +36,7 @@ public class VmAccessFactory implements ConletComponentFactory { */ @Override public Class componentType() { - return VmAccess.class; + return VmConlet.class; } /* @@ -48,7 +48,7 @@ public class VmAccessFactory implements ConletComponentFactory { @Override public Optional create(Channel componentChannel, Map properties) { - return Optional.of(new VmAccess(componentChannel)); + return Optional.of(new VmConlet(componentChannel)); } } diff --git a/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/VmConlet-functions.ts b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/VmConlet-functions.ts new file mode 100644 index 0000000..f0f8919 --- /dev/null +++ b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/VmConlet-functions.ts @@ -0,0 +1,148 @@ +/* + * 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 . + */ + +import { reactive, ref, createApp, computed, onMounted } from "vue"; +import JGConsole from "jgconsole"; +import JgwcPlugin, { JGWC } from "jgwc"; +import { provideApi, getApi } from "aash-plugin"; +import l10nBundles from "l10nBundles"; + +import "./VmConlet-style.scss"; + +// +// Helpers +// +let unitMap = new Map(); +let unitMappings = new Array<{ key: string; value: bigint }>(); +let memorySize = /^\\s*(\\d+(\\.\\d+)?)\\s*([A-Za-z]*)\\s*/; + +// SI units and common abbreviations +let factor = BigInt("1"); +unitMap.set("", factor); +let scale = BigInt("1000"); +for (let unit of ["B", "kB", "MB", "GB", "TB", "PB", "EB"]) { + unitMap.set(unit, factor); + factor = factor * scale; +} +// Binary units +factor = BigInt("1024"); +scale = BigInt("1024"); +for (let unit of ["KiB", "MiB", "GiB", "TiB", "PiB", "EiB"]) { + unitMap.set(unit, factor); + factor = factor * scale; +} +unitMap.forEach((value: bigint, key: string) => { + unitMappings.push({ key, value }); +}); +unitMappings.sort((a, b) => a.value < b.value ? 1 : a.value > b.value ? -1 : 0); + +function formatMemory(size: bigint): string { + for (let mapping of unitMappings) { + if (size >= mapping.value + && (size % mapping.value) === BigInt("0")) { + return (size / mapping.value + " " + mapping.key).trim(); + } + } + return size.toString(); +} + +// For global access +declare global { + interface Window { + orgJDrupesVmOperatorVmConlet: any; + } +} + +window.orgJDrupesVmOperatorVmConlet = {}; + +let vmInfos = reactive(new Map()); + +window.orgJDrupesVmOperatorVmConlet.initPreview + = (previewDom: HTMLElement, isUpdate: boolean) => { + const app = createApp({}); + app.use(JgwcPlugin, []); + app.config.globalProperties.window = window; + app.mount(previewDom); + }; + +window.orgJDrupesVmOperatorVmConlet.initView = (viewDom: HTMLElement, + isUpdate: boolean) => { + const app = createApp({ + setup(_props: any) { + const conletId: string + = (viewDom.parentNode!).dataset["conletId"]!; + + const localize = (key: string) => { + return JGConsole.localize( + l10nBundles, JGWC.lang() || "en", key); + }; + + const controller = reactive(new JGConsole.TableController([ + ["name", "vmname"], + ["running", "running"], + ["currentCpus", "currentCpus"], + ["currentRam", "currentRam"] + ], { + sortKey: "name", + sortOrder: "up" + })); + + let filteredData = computed(() => { + let infos = Array.from(vmInfos.values()); + return controller.filter(infos); + }); + + const vmAction = (vmName: string, action: string) => { + JGConsole.notifyConletModel(conletId, action, vmName); + }; + + const idScope = JGWC.createIdScope(); + const detailsByName = reactive(new Set()); + + return { + controller, vmInfos, filteredData, detailsByName, + localize, formatMemory, vmAction, + scopedId: (id: string) => { return idScope.scopedId(id); } + } + } + }); + app.use(JgwcPlugin); + app.config.globalProperties.window = window; + app.mount(viewDom); +}; + +JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmconlet.VmConlet", + "updateVm", function(conletId: String, vmDefinition: any) { + // Add some short-cuts for table controller + vmDefinition.name = vmDefinition.metadata.name; + vmDefinition.currentCpus = vmDefinition.status.cpus; + vmDefinition.currentRam = vmDefinition.status.ram; + for (let condition of vmDefinition.status.conditions) { + if (condition.type === "Running") { + vmDefinition.running = condition.status === "True"; + break; + } + } + + vmInfos.set(vmDefinition.name, vmDefinition); + }); + +JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmconlet.VmConlet", + "removeVm", function(conletId: String, vmName: String) { + vmInfos.delete(vmName); + }); diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpReset.java b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/VmConlet-style.scss similarity index 54% rename from org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpReset.java rename to org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/VmConlet-style.scss index 5364811..6d3168b 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpReset.java +++ b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/VmConlet-style.scss @@ -16,27 +16,36 @@ * along with this program. If not, see . */ -package org.jdrupes.vmoperator.runner.qemu.commands; - -import com.fasterxml.jackson.databind.JsonNode; - -/** - * A {@link QmpCommand} that send a system_reset to the VM. +/* + * Conlet specific styles. */ -public class QmpReset extends QmpCommand { - - @SuppressWarnings({ "PMD.FieldNamingConventions" }) - private static final JsonNode jsonTemplate - = parseJson("{ \"execute\": \"system_reset\" }"); - - @Override - public JsonNode toJson() { - return jsonTemplate.deepCopy(); - } - - @Override - public String toString() { - return "QmpReset()"; - } - + +.jdrupes-vmoperator-vmconlet-view-search { + display: flex; + justify-content: flex-end } + +.jdrupes-vmoperator-vmconlet-view-search form { + white-space: nowrap; +} + +.jdrupes-vmoperator-vmconlet-view-action-list { + white-space: nowrap; +} + +.jdrupes-vmoperator-vmconlet-view-action-list [role=button]:not(:last-child) { + margin-right: 0.5em; +} + +.jdrupes-vmoperator-vmconlet-view td { + vertical-align: top; +} + +.jdrupes-vmoperator-vmconlet-view td:not([colspan]):first-child { + white-space: nowrap; +} + +.jdrupes-vmoperator-vmconlet-view table td.details { + padding-left: 1em; +} + diff --git a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/browser/l10nBundles-stub.d.ts b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/l10nBundles-stub.d.ts similarity index 100% rename from org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/browser/l10nBundles-stub.d.ts rename to org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/l10nBundles-stub.d.ts diff --git a/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/package-info.java b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/package-info.java similarity index 94% rename from org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/package-info.java rename to org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/package-info.java index c39c193..2cbbfa7 100644 --- a/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/package-info.java +++ b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/package-info.java @@ -16,4 +16,4 @@ * along with this program. If not, see . */ -package org.jdrupes.vmoperator.vmmgmt; \ No newline at end of file +package org.jdrupes.vmoperator.vmconlet; \ No newline at end of file diff --git a/org.jdrupes.vmoperator.vmaccess/tsconfig.json b/org.jdrupes.vmoperator.vmconlet/tsconfig.json similarity index 92% rename from org.jdrupes.vmoperator.vmaccess/tsconfig.json rename to org.jdrupes.vmoperator.vmconlet/tsconfig.json index d9dbb3f..906e474 100644 --- a/org.jdrupes.vmoperator.vmaccess/tsconfig.json +++ b/org.jdrupes.vmoperator.vmconlet/tsconfig.json @@ -14,7 +14,7 @@ "aash-plugin": ["./build/unpacked/org/jgrapes/webconsole/provider/jgwcvuecomponents/aash-vue-components/lib/AashPlugin"], "jgconsole": ["./build/unpacked/org/jgrapes/webconsole/base/JGConsole"], "jgwc": ["./build/unpacked/org/jgrapes/webconsole/provider/jgwcvuecomponents/jgwc-vue-components/jgwc-components"], - "l10nBundles": ["./src/org/jdrupes/vmoperator/vmaccess/browser/l10nBundles-stub"], + "l10nBundles": ["./src/org/jdrupes/vmoperator/vmconlet/browser/l10nBundles-stub"], "vue": ["./build/unpacked/org/jgrapes/webconsole/provider/vue/vue/vue"] } }, diff --git a/org.jdrupes.vmoperator.vmmgmt/.checkstyle b/org.jdrupes.vmoperator.vmmgmt/.checkstyle deleted file mode 100644 index 7f2c604..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/.checkstyle +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/org.jdrupes.vmoperator.vmmgmt/.eclipse-pmd b/org.jdrupes.vmoperator.vmmgmt/.eclipse-pmd deleted file mode 100644 index 5d69caa..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/.eclipse-pmd +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/org.jdrupes.vmoperator.vmmgmt/.eslintignore b/org.jdrupes.vmoperator.vmmgmt/.eslintignore deleted file mode 100644 index 139d3ee..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -rollup.config.mjs diff --git a/org.jdrupes.vmoperator.vmmgmt/.eslintrc.json b/org.jdrupes.vmoperator.vmmgmt/.eslintrc.json deleted file mode 100644 index e4f80f1..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/.eslintrc.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { "project": ["./tsconfig.json"] }, - "plugins": [ - "@typescript-eslint" - ], - "rules": { - "constructor-super": "off" - } -} - diff --git a/org.jdrupes.vmoperator.vmmgmt/.gitignore b/org.jdrupes.vmoperator.vmmgmt/.gitignore deleted file mode 100644 index a53e74c..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -/bin/ -/bin_test/ -/generated/ -/build/ diff --git a/org.jdrupes.vmoperator.vmmgmt/.settings/org.eclipse.buildship.core.prefs b/org.jdrupes.vmoperator.vmmgmt/.settings/org.eclipse.buildship.core.prefs deleted file mode 100644 index 641c156..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/.settings/org.eclipse.buildship.core.prefs +++ /dev/null @@ -1,10 +0,0 @@ -build.commands=org.eclipse.jdt.core.javabuilder -connection.arguments= -connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) -connection.java.home=null -connection.jvm.arguments= -connection.project.dir=.. -derived.resources=.gradle,generated -eclipse.preferences.version=1 -natures=org.eclipse.jdt.groovy.core.groovyNature,org.eclipse.jdt.core.javanature -project.path=\:org.jgrapes.osgi.conlets.services diff --git a/org.jdrupes.vmoperator.vmmgmt/.settings/org.eclipse.core.resources.prefs b/org.jdrupes.vmoperator.vmmgmt/.settings/org.eclipse.core.resources.prefs deleted file mode 100644 index 99f26c0..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/.settings/org.eclipse.core.resources.prefs +++ /dev/null @@ -1,2 +0,0 @@ -eclipse.preferences.version=1 -encoding/=UTF-8 diff --git a/org.jdrupes.vmoperator.vmmgmt/.settings/org.eclipse.core.runtime.prefs b/org.jdrupes.vmoperator.vmmgmt/.settings/org.eclipse.core.runtime.prefs deleted file mode 100644 index 5a0ad22..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/.settings/org.eclipse.core.runtime.prefs +++ /dev/null @@ -1,2 +0,0 @@ -eclipse.preferences.version=1 -line.separator=\n diff --git a/org.jdrupes.vmoperator.vmmgmt/.settings/org.eclipse.jdt.ui.prefs b/org.jdrupes.vmoperator.vmmgmt/.settings/org.eclipse.jdt.ui.prefs deleted file mode 100644 index 784d01f..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/.settings/org.eclipse.jdt.ui.prefs +++ /dev/null @@ -1,63 +0,0 @@ -eclipse.preferences.version=1 -editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true -formatter_profile=_JGrapes -formatter_settings_version=13 -sp_cleanup.add_default_serial_version_id=true -sp_cleanup.add_generated_serial_version_id=false -sp_cleanup.add_missing_annotations=true -sp_cleanup.add_missing_deprecated_annotations=true -sp_cleanup.add_missing_methods=false -sp_cleanup.add_missing_nls_tags=false -sp_cleanup.add_missing_override_annotations=true -sp_cleanup.add_missing_override_annotations_interface_methods=true -sp_cleanup.add_serial_version_id=false -sp_cleanup.always_use_blocks=true -sp_cleanup.always_use_parentheses_in_expressions=false -sp_cleanup.always_use_this_for_non_static_field_access=false -sp_cleanup.always_use_this_for_non_static_method_access=false -sp_cleanup.convert_functional_interfaces=false -sp_cleanup.convert_to_enhanced_for_loop=false -sp_cleanup.correct_indentation=false -sp_cleanup.format_source_code=true -sp_cleanup.format_source_code_changes_only=false -sp_cleanup.insert_inferred_type_arguments=false -sp_cleanup.make_local_variable_final=true -sp_cleanup.make_parameters_final=false -sp_cleanup.make_private_fields_final=true -sp_cleanup.make_type_abstract_if_missing_method=false -sp_cleanup.make_variable_declarations_final=false -sp_cleanup.never_use_blocks=false -sp_cleanup.never_use_parentheses_in_expressions=true -sp_cleanup.on_save_use_additional_actions=false -sp_cleanup.organize_imports=false -sp_cleanup.qualify_static_field_accesses_with_declaring_class=false -sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true -sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true -sp_cleanup.qualify_static_member_accesses_with_declaring_class=false -sp_cleanup.qualify_static_method_accesses_with_declaring_class=false -sp_cleanup.remove_private_constructors=true -sp_cleanup.remove_redundant_type_arguments=false -sp_cleanup.remove_trailing_whitespaces=false -sp_cleanup.remove_trailing_whitespaces_all=true -sp_cleanup.remove_trailing_whitespaces_ignore_empty=false -sp_cleanup.remove_unnecessary_casts=true -sp_cleanup.remove_unnecessary_nls_tags=false -sp_cleanup.remove_unused_imports=false -sp_cleanup.remove_unused_local_variables=false -sp_cleanup.remove_unused_private_fields=true -sp_cleanup.remove_unused_private_members=false -sp_cleanup.remove_unused_private_methods=true -sp_cleanup.remove_unused_private_types=true -sp_cleanup.sort_members=false -sp_cleanup.sort_members_all=false -sp_cleanup.use_anonymous_class_creation=false -sp_cleanup.use_blocks=false -sp_cleanup.use_blocks_only_for_return_and_throw=false -sp_cleanup.use_lambda=true -sp_cleanup.use_parentheses_in_expressions=false -sp_cleanup.use_this_for_non_static_field_access=false -sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true -sp_cleanup.use_this_for_non_static_method_access=false -sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true -sp_jautodoc.cleanup.add_header=false -sp_jautodoc.cleanup.replace_header=false diff --git a/org.jdrupes.vmoperator.vmmgmt/package.json b/org.jdrupes.vmoperator.vmmgmt/package.json deleted file mode 100644 index 0967ef4..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/package.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/org.jdrupes.vmoperator.vmmgmt/resources/META-INF/services/org.jgrapes.webconsole.base.ConletComponentFactory b/org.jdrupes.vmoperator.vmmgmt/resources/META-INF/services/org.jgrapes.webconsole.base.ConletComponentFactory deleted file mode 100644 index d7d7c8d..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/resources/META-INF/services/org.jgrapes.webconsole.base.ConletComponentFactory +++ /dev/null @@ -1 +0,0 @@ -org.jdrupes.vmoperator.vmmgmt.VmMgmtFactory diff --git a/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/VmMgmt-confirmReset.ftl.html b/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/VmMgmt-confirmReset.ftl.html deleted file mode 100644 index d174707..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/VmMgmt-confirmReset.ftl.html +++ /dev/null @@ -1,13 +0,0 @@ -
-

${_("confirmResetMsg")}

-

- - - - - - -

-
\ No newline at end of file diff --git a/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/VmMgmt-preview.ftl.html b/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/VmMgmt-preview.ftl.html deleted file mode 100644 index 8c9970a..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/VmMgmt-preview.ftl.html +++ /dev/null @@ -1,46 +0,0 @@ -
- -
-
- {{ localize("Period") }}: -
    -
  • - -
  • -
  • - -
  • -
-
-
- - - - - - - - - - - - - - - - -
{{ localize("VMsSummary") }}:{{ vmSummary.runningVms }} / {{ vmSummary.totalVms }}
{{ localize("currentCpus") }}:{{ vmSummary.usedCpus }}
{{ localize("currentRam") }}:{{ formatMemory(Number(vmSummary.usedRam)) }}
-
- -
- -
diff --git a/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/VmMgmt-view.ftl.html b/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/VmMgmt-view.ftl.html deleted file mode 100644 index 3197440..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/VmMgmt-view.ftl.html +++ /dev/null @@ -1,145 +0,0 @@ -
- - - - - - - - - - - -
- {{ localize(controller.label(key)) }} - - {{ localize("vmActions") }} -
-
diff --git a/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/computer-in-use.svg b/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/computer-in-use.svg deleted file mode 100644 index 00e4cc0..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/computer-in-use.svg +++ /dev/null @@ -1,90 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/computer-off.svg b/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/computer-off.svg deleted file mode 100644 index 27c11ae..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/computer-off.svg +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - diff --git a/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/computer.svg b/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/computer.svg deleted file mode 100644 index f7a6b94..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/computer.svg +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - diff --git a/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/l10n.properties b/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/l10n.properties deleted file mode 100644 index 95cb839..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/l10n.properties +++ /dev/null @@ -1,26 +0,0 @@ -conletName = VM Management - -VMsSummary = VMs (running/total) - -assignedTo = Assigned to -currentCpus = Current vCPUs -currentRam = Current vRAM -guestOs = Guest OS -maximumCpus = Maximum vCPUs -maximumRam = Maximum vRAM -notInUse = Currently closed -nodeName = Node -requestedCpus = Requested vCPUs -requestedRam = Requested vRAM -runnerVersion = Runner version -running = Running -since = Since -usedBy = Used by -usedFrom = Used from -vmActions = Actions -vmname = Name - -confirmResetTitle = Confirm reset -confirmResetMsg = Resetting the VM may cause loss of data. \ - Please confirm to continue. -consoleTakenNotification = Console access is locked by another user. diff --git a/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/l10n_de.properties b/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/l10n_de.properties deleted file mode 100644 index abe0d46..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/l10n_de.properties +++ /dev/null @@ -1,40 +0,0 @@ -conletName = VM-Management - -VMsSummary = VMs (gestartet/gesamt) - -Period = Zeitraum -Last\ hour = Letzte Stunde -Last\ day = Letzter Tag - -assignedTo = Zugewiesen an -currentCpus = Aktuelle vCPUs -currentRam = Akuelles vRAM -guestOs = Gast BS -maximumCpus = Maximale vCPUs -maximumRam = Maximales vRAM -nodeName = Knoten -notInUse = Derzeit geschlossen -requestedCpus = Angeforderte vCPUs -requestedRam = Angefordertes vRAM -runnerVersion = Runner-Version -running = Gestartet -since = Seit -usedBy = Benutzt durch -usedFrom = Benutzt von -vmActions = Aktionen -vmname = Name -Value\ is\ above\ maximum = Wert ist zu groß -Illegal\ format = Ungültiges Format - -confirmResetTitle = Zurücksetzen bestätigen -confirmResetMsg = Zurücksetzen der VM kann zu Datenverlust führen. \ - Bitte bestätigen um fortzufahren. -consoleTakenNotification = Die Konsole wird von einem anderen Benutzer verwendet. - -Open\ console = Konsole anzeigen -Start\ VM = VM Starten -Stop\ VM = VM Anhalten -Reset\ VM = VM zurücksetzen - -Yes = Ja -No = Nein diff --git a/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/l10n_en.properties b/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/l10n_en.properties deleted file mode 100644 index e69de29..0000000 diff --git a/org.jdrupes.vmoperator.vmmgmt/rollup.config.mjs b/org.jdrupes.vmoperator.vmmgmt/rollup.config.mjs deleted file mode 100644 index 59aff08..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/rollup.config.mjs +++ /dev/null @@ -1,36 +0,0 @@ -import typescript from 'rollup-plugin-typescript2'; -import postcss from 'rollup-plugin-postcss'; - -let packagePath = "org/jdrupes/vmoperator/vmmgmt"; -let baseName = "VmMgmt" -let module = "build/generated/resources/" + packagePath - + "/" + baseName + "-functions.js"; - -let pathsMap = { - "aash-plugin": "../../page-resource/aash-vue-components/lib/aash-vue-components.js", - "jgconsole": "../../console-base-resource/jgconsole.js", - "jgwc": "../../page-resource/jgwc-vue-components/jgwc-components.js", - "l10nBundles": "./" + baseName + "-l10nBundles.ftl.js", - "vue": "../../page-resource/vue/vue.esm-browser.js", - "chartjs": "../../page-resource/chart.js/auto.js" -} - -export default { - external: ['aash-plugin', 'jgconsole', 'jgwc', 'l10nBundles', 'vue', 'chartjs'], - input: "src/" + packagePath + "/browser/" + baseName + "-functions.ts", - output: [ - { - format: "esm", - file: module, - sourcemap: true, - sourcemapPathTransform: (relativeSourcePath, _sourcemapPath) => { - return relativeSourcePath.replace(/^([^/]*\/){12}/, "./"); - }, - paths: pathsMap - } - ], - plugins: [ - typescript(), - postcss() - ] -}; diff --git a/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/TimeSeries.java b/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/TimeSeries.java deleted file mode 100644 index 7e1f39e..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/TimeSeries.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.vmmgmt; - -import java.time.Duration; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; - -/** - * The Class TimeSeries. - */ -public class TimeSeries { - - @SuppressWarnings("PMD.LooseCoupling") - private final LinkedList data = new LinkedList<>(); - private final Duration period; - - /** - * Instantiates a new time series. - * - * @param period the period - */ - public TimeSeries(Duration period) { - this.period = period; - } - - /** - * Adds data to the series. - * - * @param time the time - * @param numbers the numbers - * @return the time series - */ - @SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition", - "PMD.AvoidSynchronizedStatement" }) - public TimeSeries add(Instant time, Number... numbers) { - var newEntry = new Entry(time, numbers); - boolean nothingNew = false; - synchronized (data) { - if (data.size() >= 2) { - var lastEntry = data.get(data.size() - 1); - var lastButOneEntry = data.get(data.size() - 2); - nothingNew = lastEntry.valuesEqual(lastButOneEntry) - && lastEntry.valuesEqual(newEntry); - } - if (nothingNew) { - data.removeLast(); - } - data.add(new Entry(time, numbers)); - - // Purge - Instant limit = time.minus(period); - while (data.size() > 2 - && data.get(0).getTime().isBefore(limit) - && data.get(1).getTime().isBefore(limit)) { - data.removeFirst(); - } - } - return this; - } - - /** - * Returns the entries. - * - * @return the list - */ - @SuppressWarnings("PMD.AvoidSynchronizedStatement") - public List entries() { - synchronized (data) { - return new ArrayList<>(data); - } - } - - /** - * The Class Entry. - */ - public static class Entry { - private final Instant timestamp; - private final Number[] values; - - /** - * Instantiates a new entry. - * - * @param time the time - * @param numbers the numbers - */ - @SuppressWarnings("PMD.ArrayIsStoredDirectly") - public Entry(Instant time, Number... numbers) { - timestamp = time; - values = numbers; - } - - /** - * Returns the entry's time. - * - * @return the instant - */ - public Instant getTime() { - return timestamp; - } - - /** - * Returns the values. - * - * @return the number[] - */ - @SuppressWarnings("PMD.MethodReturnsInternalArray") - public Number[] getValues() { - return values; - } - - /** - * Returns `true` if both entries have the same values. - * - * @param other the other - * @return true, if successful - */ - public boolean valuesEqual(Entry other) { - return Arrays.equals(values, other.values); - } - } -} diff --git a/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/VmMgmt.java b/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/VmMgmt.java deleted file mode 100644 index e4380ba..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/VmMgmt.java +++ /dev/null @@ -1,516 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.vmmgmt; - -import freemarker.core.ParseException; -import freemarker.template.MalformedTemplateNameException; -import freemarker.template.Template; -import freemarker.template.TemplateNotFoundException; -import io.kubernetes.client.custom.Quantity; -import io.kubernetes.client.custom.Quantity.Format; -import java.io.IOException; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.net.Inet4Address; -import java.net.Inet6Address; -import java.time.Duration; -import java.time.Instant; -import java.util.Collections; -import java.util.EnumSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.ResourceBundle; -import java.util.Set; -import org.jdrupes.vmoperator.common.Constants.Status; -import org.jdrupes.vmoperator.common.K8sObserver; -import org.jdrupes.vmoperator.common.VmDefinition; -import org.jdrupes.vmoperator.common.VmDefinition.Permission; -import org.jdrupes.vmoperator.manager.events.ChannelTracker; -import org.jdrupes.vmoperator.manager.events.GetDisplaySecret; -import org.jdrupes.vmoperator.manager.events.ModifyVm; -import org.jdrupes.vmoperator.manager.events.ResetVm; -import org.jdrupes.vmoperator.manager.events.VmChannel; -import org.jdrupes.vmoperator.manager.events.VmResourceChanged; -import org.jdrupes.vmoperator.util.DataPath; -import org.jgrapes.core.Channel; -import org.jgrapes.core.Event; -import org.jgrapes.core.Manager; -import org.jgrapes.core.annotation.Handler; -import org.jgrapes.util.events.ConfigurationUpdate; -import org.jgrapes.webconsole.base.Conlet.RenderMode; -import org.jgrapes.webconsole.base.ConletBaseModel; -import org.jgrapes.webconsole.base.ConsoleConnection; -import org.jgrapes.webconsole.base.ConsoleRole; -import org.jgrapes.webconsole.base.ConsoleUser; -import org.jgrapes.webconsole.base.WebConsoleUtils; -import org.jgrapes.webconsole.base.events.AddConletType; -import org.jgrapes.webconsole.base.events.AddPageResources.ScriptResource; -import org.jgrapes.webconsole.base.events.ConsoleReady; -import org.jgrapes.webconsole.base.events.DisplayNotification; -import org.jgrapes.webconsole.base.events.NotifyConletModel; -import org.jgrapes.webconsole.base.events.NotifyConletView; -import org.jgrapes.webconsole.base.events.OpenModalDialog; -import org.jgrapes.webconsole.base.events.RenderConlet; -import org.jgrapes.webconsole.base.events.RenderConletRequestBase; -import org.jgrapes.webconsole.base.events.SetLocale; -import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet; - -/** - * The Class {@link VmMgmt}. - */ -@SuppressWarnings({ "PMD.CouplingBetweenObjects", "PMD.ExcessiveImports" }) -public class VmMgmt extends FreeMarkerConlet { - - private Class preferredIpVersion = Inet4Address.class; - private boolean deleteConnectionFile = true; - private static final Set MODES = RenderMode.asSet( - RenderMode.Preview, RenderMode.View); - private final ChannelTracker channelTracker = new ChannelTracker<>(); - private final TimeSeries summarySeries = new TimeSeries(Duration.ofDays(1)); - private Summary cachedSummary; - - /** - * The periodically generated update event. - */ - public static class Update extends Event { - } - - /** - * Creates a new component with its channel set to the given channel. - * - * @param componentChannel the channel that the component's handlers listen - * on by default and that {@link Manager#fire(Event, Channel...)} - * sends the event to - */ - @SuppressWarnings("PMD.ConstructorCallsOverridableMethod") - public VmMgmt(Channel componentChannel) { - super(componentChannel); - setPeriodicRefresh(Duration.ofMinutes(1), () -> new Update()); - } - - /** - * Configure the component. - * - * @param event the event - */ - @SuppressWarnings({ "unchecked" }) - @Handler - public void onConfigurationUpdate(ConfigurationUpdate event) { - event.structured("/Manager/GuiHttpServer" - + "/ConsoleWeblet/WebConsole/ComponentCollector/VmAccess") - .ifPresent(c -> { - try { - var dispRes = (Map) c - .getOrDefault("displayResource", - Collections.emptyMap()); - switch ((String) dispRes.getOrDefault("preferredIpVersion", - "")) { - case "ipv6": - preferredIpVersion = Inet6Address.class; - break; - case "ipv4": - default: - preferredIpVersion = Inet4Address.class; - break; - } - - // Delete connection file - deleteConnectionFile - = Optional.ofNullable(c.get("deleteConnectionFile")) - .filter(v -> v instanceof String) - .map(v -> (String) v) - .map(Boolean::parseBoolean).orElse(true); - } catch (ClassCastException e) { - logger.config("Malformed configuration: " + e.getMessage()); - } - }); - } - - /** - * On {@link ConsoleReady}, fire the {@link AddConletType}. - * - * @param event the event - * @param channel the channel - * @throws TemplateNotFoundException the template not found exception - * @throws MalformedTemplateNameException the malformed template name - * exception - * @throws ParseException the parse exception - * @throws IOException Signals that an I/O exception has occurred. - */ - @Handler - public void onConsoleReady(ConsoleReady event, ConsoleConnection channel) - throws TemplateNotFoundException, MalformedTemplateNameException, - ParseException, IOException { - // Add conlet resources to page - channel.respond(new AddConletType(type()) - .setDisplayNames( - localizations(channel.supportedLocales(), "conletName")) - .addRenderMode(RenderMode.Preview) - .addScript(new ScriptResource().setScriptType("module") - .setScriptUri(event.renderSupport().conletResource( - type(), "VmMgmt-functions.js")))); - } - - @Override - protected Optional createStateRepresentation(Event event, - ConsoleConnection connection, String conletId) throws Exception { - return Optional.of(new VmsModel(conletId)); - } - - @Override - protected Set doRenderConlet(RenderConletRequestBase event, - ConsoleConnection channel, String conletId, VmsModel conletState) - throws Exception { - Set renderedAs = EnumSet.noneOf(RenderMode.class); - boolean sendVmInfos = false; - if (event.renderAs().contains(RenderMode.Preview)) { - Template tpl - = freemarkerConfig().getTemplate("VmMgmt-preview.ftl.html"); - channel.respond(new RenderConlet(type(), conletId, - processTemplate(event, tpl, - fmModel(event, channel, conletId, conletState))) - .setRenderAs( - RenderMode.Preview.addModifiers(event.renderAs())) - .setSupportedModes(MODES)); - renderedAs.add(RenderMode.Preview); - channel.respond(new NotifyConletView(type(), - conletId, "summarySeries", summarySeries.entries())); - var summary = evaluateSummary(false); - channel.respond(new NotifyConletView(type(), - conletId, "updateSummary", summary)); - sendVmInfos = true; - } - if (event.renderAs().contains(RenderMode.View)) { - Template tpl - = freemarkerConfig().getTemplate("VmMgmt-view.ftl.html"); - channel.respond(new RenderConlet(type(), conletId, - processTemplate(event, tpl, - fmModel(event, channel, conletId, conletState))) - .setRenderAs( - RenderMode.View.addModifiers(event.renderAs())) - .setSupportedModes(MODES)); - renderedAs.add(RenderMode.View); - sendVmInfos = true; - } - if (sendVmInfos) { - for (var item : channelTracker.values()) { - updateVm(channel, conletId, item.associated()); - } - } - return renderedAs; - } - - private void updateVm(ConsoleConnection channel, String conletId, - VmDefinition vmDef) { - var user = WebConsoleUtils.userFromSession(channel.session()) - .map(ConsoleUser::getName).orElse(null); - var roles = WebConsoleUtils.rolesFromSession(channel.session()) - .stream().map(ConsoleRole::getName).toList(); - channel.respond(new NotifyConletView(type(), conletId, "updateVm", - simplifiedVmDefinition(vmDef, user, roles))); - } - - private Map simplifiedVmDefinition(VmDefinition vmDef, - String user, List roles) { - // Convert RAM sizes to unitless numbers - var spec = DataPath.deepCopy(vmDef.spec()); - spec.remove("cloudInit"); - var vmSpec = DataPath.> get(spec, "vm").get(); - vmSpec.remove("networks"); - vmSpec.remove("disks"); - vmSpec.put("maximumRam", Quantity.fromString( - DataPath. get(vmSpec, "maximumRam").orElse("0")).getNumber() - .toBigInteger()); - vmSpec.put("currentRam", Quantity.fromString( - DataPath. get(vmSpec, "currentRam").orElse("0")).getNumber() - .toBigInteger()); - var status = DataPath.deepCopy(vmDef.status()); - status.put(Status.RAM, Quantity.fromString( - DataPath. get(status, Status.RAM).orElse("0")).getNumber() - .toBigInteger()); - - // Build result - var perms = vmDef.permissionsFor(user, roles); - return Map.of("metadata", - Map.of("namespace", vmDef.namespace(), - "name", vmDef.name()), - "spec", spec, - "status", status, - "nodeName", vmDef.extra().nodeName(), - "consoleAccessible", vmDef.consoleAccessible(user, perms), - "permissions", perms); - } - - /** - * Track the VM definitions. - * - * @param event the event - * @param channel the channel - * @throws IOException - */ - @Handler(namedChannels = "manager") - @SuppressWarnings({ "PMD.CognitiveComplexity", - "PMD.AvoidInstantiatingObjectsInLoops" }) - public void onVmResourceChanged(VmResourceChanged event, VmChannel channel) - throws IOException { - var vmName = event.vmDefinition().name(); - if (event.type() == K8sObserver.ResponseType.DELETED) { - channelTracker.remove(vmName); - for (var entry : conletIdsByConsoleConnection().entrySet()) { - for (String conletId : entry.getValue()) { - entry.getKey().respond(new NotifyConletView(type(), - conletId, "removeVm", vmName)); - } - } - } else { - var vmDef = event.vmDefinition(); - channelTracker.put(vmName, channel, vmDef); - for (var entry : conletIdsByConsoleConnection().entrySet()) { - for (String conletId : entry.getValue()) { - updateVm(entry.getKey(), conletId, vmDef); - } - } - } - - var summary = evaluateSummary(true); - summarySeries.add(Instant.now(), summary.usedCpus, summary.usedRam); - for (var entry : conletIdsByConsoleConnection().entrySet()) { - for (String conletId : entry.getValue()) { - entry.getKey().respond(new NotifyConletView(type(), - conletId, "updateSummary", summary)); - } - } - } - - /** - * Handle the periodic update event by sending {@link NotifyConletView} - * events. - * - * @param event the event - * @param connection the console connection - */ - @Handler - @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") - public void onUpdate(Update event, ConsoleConnection connection) { - var summary = evaluateSummary(false); - summarySeries.add(Instant.now(), summary.usedCpus, summary.usedRam); - for (String conletId : conletIds(connection)) { - connection.respond(new NotifyConletView(type(), - conletId, "updateSummary", summary)); - } - } - - /** - * The Class Summary. - */ - @SuppressWarnings("PMD.DataClass") - public static class Summary { - - /** The total vms. */ - public int totalVms; - - /** The running vms. */ - public long runningVms; - - /** The used cpus. */ - public long usedCpus; - - /** The used ram. */ - public BigInteger usedRam = BigInteger.ZERO; - - /** - * Gets the total vms. - * - * @return the totalVms - */ - public int getTotalVms() { - return totalVms; - } - - /** - * Gets the running vms. - * - * @return the runningVms - */ - public long getRunningVms() { - return runningVms; - } - - /** - * Gets the used cpus. - * - * @return the usedCpus - */ - public long getUsedCpus() { - return usedCpus; - } - - /** - * Gets the used ram. Returned as String for Json rendering. - * - * @return the usedRam - */ - public String getUsedRam() { - return usedRam.toString(); - } - - } - - private Summary evaluateSummary(boolean force) { - if (!force && cachedSummary != null) { - return cachedSummary; - } - Summary summary = new Summary(); - for (var vmDef : channelTracker.associated()) { - summary.totalVms += 1; - summary.usedCpus += vmDef. fromStatus(Status.CPUS) - .map(Number::intValue).orElse(0); - summary.usedRam = summary.usedRam - .add(vmDef. fromStatus(Status.RAM) - .map(r -> Quantity.fromString(r).getNumber().toBigInteger()) - .orElse(BigInteger.ZERO)); - if (vmDef.conditionStatus("Running").orElse(false)) { - summary.runningVms += 1; - } - } - cachedSummary = summary; - return summary; - } - - @Override - @SuppressWarnings({ "PMD.NcssCount" }) - protected void doUpdateConletState(NotifyConletModel event, - ConsoleConnection channel, VmsModel model) throws Exception { - event.stop(); - String vmName = event.param(0); - var value = channelTracker.value(vmName); - var vmChannel = value.map(v -> v.channel()).orElse(null); - var vmDef = value.map(v -> v.associated()).orElse(null); - if (vmDef == null) { - return; - } - var user = WebConsoleUtils.userFromSession(channel.session()) - .map(ConsoleUser::getName).orElse(""); - var roles = WebConsoleUtils.rolesFromSession(channel.session()) - .stream().map(ConsoleRole::getName).toList(); - var perms = vmDef.permissionsFor(user, roles); - switch (event.method()) { - case "start": - if (perms.contains(VmDefinition.Permission.START)) { - vmChannel.fire(new ModifyVm(vmName, "state", "Running")); - } - break; - case "stop": - if (perms.contains(VmDefinition.Permission.STOP)) { - vmChannel.fire(new ModifyVm(vmName, "state", "Stopped")); - } - break; - case "reset": - if (perms.contains(VmDefinition.Permission.RESET)) { - confirmReset(event, channel, model, vmName); - } - break; - case "resetConfirmed": - if (perms.contains(VmDefinition.Permission.RESET)) { - vmChannel.fire(new ResetVm(vmName)); - } - break; - case "openConsole": - openConsole(channel, model, vmChannel, vmDef, user, perms); - break; - case "cpus": - vmChannel.fire(new ModifyVm(vmName, "currentCpus", - new BigDecimal(event.param(1).toString()).toBigInteger())); - break; - case "ram": - vmChannel.fire(new ModifyVm(vmName, "currentRam", - new Quantity(new BigDecimal(event.param(1).toString()), - Format.BINARY_SI).toSuffixedString())); - break; - default:// ignore - break; - } - } - - private void confirmReset(NotifyConletModel event, - ConsoleConnection channel, VmsModel model, String vmName) - throws TemplateNotFoundException, - MalformedTemplateNameException, ParseException, IOException { - Template tpl = freemarkerConfig() - .getTemplate("VmMgmt-confirmReset.ftl.html"); - ResourceBundle resourceBundle = resourceBundle(channel.locale()); - var fmModel = fmModel(event, channel, model.getConletId(), model); - fmModel.put("vmName", vmName); - channel.respond(new OpenModalDialog(type(), model.getConletId(), - processTemplate(event, tpl, fmModel)) - .addOption("cancelable", true).addOption("closeLabel", "") - .addOption("title", - resourceBundle.getString("confirmResetTitle"))); - } - - private void openConsole(ConsoleConnection channel, VmsModel model, - VmChannel vmChannel, VmDefinition vmDef, String user, - Set perms) { - ResourceBundle resourceBundle = resourceBundle(channel.locale()); - if (!vmDef.consoleAccessible(user, perms)) { - channel.respond(new DisplayNotification( - resourceBundle.getString("consoleTakenNotification"), - Map.of("autoClose", 5_000, "type", "Warning"))); - return; - } - var pwQuery = Event.onCompletion(new GetDisplaySecret(vmDef, user), - e -> gotPassword(channel, model, vmDef, e)); - vmChannel.fire(pwQuery); - } - - private void gotPassword(ConsoleConnection channel, VmsModel model, - VmDefinition vmDef, GetDisplaySecret event) { - if (!event.secretAvailable()) { - return; - } - vmDef.extra().connectionFile(event.secret(), - preferredIpVersion, deleteConnectionFile).ifPresent( - cf -> channel.respond(new NotifyConletView(type(), - model.getConletId(), "openConsole", cf))); - } - - @Override - protected boolean doSetLocale(SetLocale event, ConsoleConnection channel, - String conletId) throws Exception { - return true; - } - - /** - * The Class VmsModel. - */ - public class VmsModel extends ConletBaseModel { - - /** - * Instantiates a new vms model. - * - * @param conletId the conlet id - */ - public VmsModel(String conletId) { - super(conletId); - } - - } -} diff --git a/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/VmMgmtFactory.java b/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/VmMgmtFactory.java deleted file mode 100644 index 922f938..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/VmMgmtFactory.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package org.jdrupes.vmoperator.vmmgmt; - -import java.util.Map; -import java.util.Optional; -import org.jgrapes.core.Channel; -import org.jgrapes.core.ComponentType; -import org.jgrapes.webconsole.base.ConletComponentFactory; - -/** - * The factory service for {@link VmMgmt}s. - */ -public class VmMgmtFactory implements ConletComponentFactory { - - /* - * (non-Javadoc) - * - * @see org.jgrapes.core.ComponentFactory#componentType() - */ - @Override - public Class componentType() { - return VmMgmt.class; - } - - /* - * (non-Javadoc) - * - * @see org.jgrapes.core.ComponentFactory#create(org.jgrapes.core.Channel, - * java.util.Map) - */ - @Override - public Optional create(Channel componentChannel, - Map properties) { - return Optional.of(new VmMgmt(componentChannel)); - } - -} diff --git a/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/browser/ConditionalInputController.ts b/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/browser/ConditionalInputController.ts deleted file mode 100644 index 4f3d8a0..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/browser/ConditionalInputController.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import { ref, nextTick } from "vue"; - -/** - * A controller for conditionally shown inputs. "Conditionally shown" - * means that the value is usually shown using some display element - * (e.g. `span`). Only when that elements gets the focus, it is replaced - * with an input element for editing the value. - */ -export default class ConditionlInputController { - - private submitCallback: (selected: string, value: number | null) - => string | null; - private readonly inputKey = ref(""); - private startValue: string | null = null; - private inputElement: HTMLInputElement | null = null; - private errorMessage = ref(""); - - /** - * Creates a new controller. - */ - constructor(submitCallback: (selected: string, value: number | null) - => string | null) { - // this.inputRef = inputRef; - this.submitCallback = submitCallback; - } - - get key() { - return this.inputKey.value; - } - - get error() { - return this.errorMessage.value; - } - - set input(element: HTMLInputElement) { - this.inputElement = element; - } - - startEdit (key: string, value: string) { - if (this.inputKey.value != "") { - return; - } - this.startValue = value; - this.errorMessage.value = ""; - this.inputKey.value = key; - nextTick(() => { - this.inputElement!.value = value; - this.inputElement!.focus(); - }); - } - - endEdit (converter?: (value: string) => number | null) : boolean { - if (typeof converter === 'undefined') { - this.inputKey.value = ""; - return false; - } - const newValue = converter(this.inputElement!.value); - if (newValue === this.startValue) { - this.inputKey.value = ""; - return false; - } - const submitResult = this.submitCallback (this.inputKey.value, newValue); - if (submitResult !== null) { - this.errorMessage.value = submitResult; - // Neither doing it directly nor doing it with nextTick works. - setTimeout(() => this.inputElement!.focus(), 10); - } else { - this.inputKey.value = ""; - } - - // In case it is called by form action - return false; - } - - get parseNumber() { - return (value: string): number | null => { - if (value.match(/^\d+$/)) { - return Number(value); - } - return null; - } - } - -} \ No newline at end of file diff --git a/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/browser/CpuRamChart.ts b/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/browser/CpuRamChart.ts deleted file mode 100644 index d2bf26b..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/browser/CpuRamChart.ts +++ /dev/null @@ -1,140 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import { Chart } from "chartjs"; -import TimeSeries from "./TimeSeries"; -import { formatMemory } from "./MemorySize"; -import JGConsole from "jgconsole"; -import l10nBundles from "l10nBundles"; -import { JGWC } from "jgwc"; -/* eslint-disable @typescript-eslint/no-explicit-any */ - -export default class CpuRamChart extends Chart { - - private period = 24 * 3600 * 1000; - - constructor(canvas: HTMLCanvasElement, series: TimeSeries) { - super(canvas.getContext('2d')!, { - // The type of chart we want to create - type: 'line', - - // The data for our datasets - data: { - labels: series.getTimes(), - datasets: [{ - // See localize - data: series.getSeries(0), - yAxisID: 'cpus' - }, { - // See localize - data: series.getSeries(1), - yAxisID: 'ram' - }] - }, - - // Configuration options go here - options: { - animation: false, - maintainAspectRatio: false, - scales: { - x: { - type: 'time', - time: { minUnit: 'minute' }, - adapters: { - date: { - // See localize - } - } - }, - cpus: { - type: 'linear', - display: true, - position: 'left', - min: 0 - }, - ram: { - type: 'linear', - display: true, - position: 'right', - min: 0, - grid: { drawOnChartArea: false }, - ticks: { - stepSize: 1024 * 1024 * 1024, - callback: function(value, _index, _values) { - return formatMemory(Math.round(Number(value))); - } - } - } - } - } - }); - - const css = getComputedStyle(canvas); - this.setPropValue("options.plugins.legend.labels.font.family", css.fontFamily); - this.setPropValue("options.plugins.legend.labels.color", css.color); - this.setPropValue("options.scales.x.ticks.font.family", css.fontFamily); - this.setPropValue("options.scales.x.ticks.color", css.color); - this.setPropValue("options.scales.cpus.ticks.font.family", css.fontFamily); - this.setPropValue("options.scales.cpus.ticks.color", css.color); - this.setPropValue("options.scales.ram.ticks.font.family", css.fontFamily); - this.setPropValue("options.scales.ram.ticks.color", css.color); - - this.localizeChart(); - } - - setPeriod(period: number) { - this.period = period; - this.update(); - } - - setPropValue(path: string, value: any) { - // eslint-disable-next-line @typescript-eslint/no-this-alias - let ptr: any = this; - const segs = path.split("."); - const lastSeg = segs.pop()!; - for (const seg of segs) { - const cur = ptr[seg]; - if (!cur) { - ptr[seg] = {}; - } - // ptr[seg] = ptr[seg] || {} - ptr = ptr[seg]; - } - ptr[lastSeg] = value; - } - - localizeChart() { - (this.options.scales?.x).adapters.date.locale = JGWC.lang(); - this.data.datasets[0].label - = JGConsole.localize(l10nBundles, JGWC.lang(), "Used CPUs") - this.data.datasets[1].label - = JGConsole.localize(l10nBundles, JGWC.lang(), "Used RAM") - this.update(); - } - - shift() { - this.setPropValue("options.scales.x.max", Date.now()); - this.update(); - } - - update() { - this.setPropValue("options.scales.x.min", Date.now() - this.period); - super.update(); - } -} - diff --git a/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/browser/MemorySize.ts b/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/browser/MemorySize.ts deleted file mode 100644 index 162da1d..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/browser/MemorySize.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -const unitMap = new Map(); -const unitMappings = new Array<{ key: string; value: number }>(); -const memorySize = /^(\d+(\.\d+)?)\s*(B|kB|MB|GB|TB|PB|EB|KiB|MiB|GiB|TiB|PiB|EiB)?$/; - -// SI units and common abbreviations -let factor = 1; -unitMap.set("", factor); -let scale = 1000; -for (const unit of ["B", "kB", "MB", "GB", "TB", "PB", "EB"]) { - unitMap.set(unit, factor); - factor = factor * scale; -} - -// Binary units -factor = 1024; -scale = 1024; -for (const unit of ["KiB", "MiB", "GiB", "TiB", "PiB", "EiB"]) { - unitMap.set(unit, factor); - factor = factor * scale; -} -unitMap.forEach((value: number, key: string) => { - unitMappings.push({ key, value }); -}); -unitMappings.sort((a, b) => a.value < b.value ? 1 : a.value > b.value ? -1 : 0); - -export function formatMemory(size: number): string { - for (const mapping of unitMappings) { - if (size >= mapping.value - && (size % mapping.value) === 0) { - return (size / mapping.value + " " + mapping.key).trim(); - } - } - return size.toString(); -} - -export function parseMemory(value: string): number | null { - const match = value.match(memorySize); - if (!match) { - return null; - } - - let unit = 1; - if (match[3]) { - unit = unitMap.get(match[3])!; - } - return Number(match[1]) * unit; -} diff --git a/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/browser/TimeSeries.ts b/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/browser/TimeSeries.ts deleted file mode 100644 index 53a1aa7..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/browser/TimeSeries.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -type OnChangeCallback = ((ts: TimeSeries) => void) | null; - -export default class TimeSeries { - private timestamps: Date[] = []; - private series: number[][]; - private period: number; - private onChange: OnChangeCallback; - - constructor(nbOfSeries: number, period = 24 * 3600 * 1000, - onChange: OnChangeCallback = null) { - this.period = period; - this.onChange = onChange; - this.series = []; - while (this.series.length < nbOfSeries) { - this.series.push([]); - } - } - - clear() { - this.timestamps.length = 0; - for (const values of this.series) { - values.length = 0; - } - if (this.onChange) { - this.onChange(this); - } - } - - push(time: Date, ...values: number[]) { - let adjust = false; - if (this.timestamps.length >= 2) { - adjust = true; - for (let i = 0; i < values.length; i++) { - if (values[i] !== this.series[i][this.series[i].length - 1] - || values[i] !== this.series[i][this.series[i].length - 2]) { - adjust = false; - break; - } - } - } - if (adjust) { - this.timestamps[this.timestamps.length - 1] = time; - } else { - this.timestamps.push(time); - for (let i = 0; i < values.length; i++) { - this.series[i].push(values[i]); - } - } - - // Purge - const limit = time.getTime() - this.period; - while (this.timestamps.length > 2 - && this.timestamps[0].getTime() < limit - && this.timestamps[1].getTime() < limit) { - this.timestamps.shift(); - for (const values of this.series) { - values.shift(); - } - } - if (this.onChange) { - this.onChange(this); - } - } - - getTimes(): Date[] { - return this.timestamps; - } - - getSeries(n: number): number[] { - return this.series[n]; - } -} - diff --git a/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/browser/VmMgmt-functions.ts b/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/browser/VmMgmt-functions.ts deleted file mode 100644 index f0407b7..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/browser/VmMgmt-functions.ts +++ /dev/null @@ -1,245 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import { - reactive, ref, Ref, createApp, computed, onMounted, watch -} from "vue"; -import JGConsole from "jgconsole"; -import JgwcPlugin, { JGWC } from "jgwc"; -import l10nBundles from "l10nBundles"; -import TimeSeries from "./TimeSeries"; -import { formatMemory, parseMemory } from "./MemorySize"; -import CpuRamChart from "./CpuRamChart"; -import ConditionlInputController from "./ConditionalInputController"; - -import "./VmMgmt-style.scss"; - -// For global access -declare global { - interface Window { - orgJDrupesVmOperatorVmMgmt: { - initPreview?: (previewDom: HTMLElement, isUpdate: boolean) => void, - initView?: (viewDom: HTMLElement, isUpdate: boolean) => void, - confirmReset?: (conletType: string, conletId: string, - vmName: string) => void - } - } -} - -window.orgJDrupesVmOperatorVmMgmt = {}; - -const vmInfos = reactive(new Map()); -const vmSummary = reactive({ - totalVms: 0, - runningVms: 0, - usedCpus: 0, - usedRam: "" -}); - -const localize = (key: string) => { - return JGConsole.localize( - l10nBundles, JGWC.lang(), key); -}; - -const shortDateTime = (time: Date) => { - // https://stackoverflow.com/questions/63958875/why-do-i-get-rangeerror-date-value-is-not-finite-in-datetimeformat-format-w - return new Intl.DateTimeFormat(JGWC.lang(), - { dateStyle: "short", timeStyle: "short" }).format(new Date(time)); -}; - -// Cannot be reactive, leads to infinite recursion. -const chartData = new TimeSeries(2); -const chartDateUpdate = ref(null); - -window.orgJDrupesVmOperatorVmMgmt.initPreview = (previewDom: HTMLElement, - _isUpdate: boolean) => { - const app = createApp({ - setup(_props: object) { - let chart: CpuRamChart | null = null; - onMounted(() => { - const canvas: HTMLCanvasElement - = previewDom.querySelector(":scope .vmsChart")!; - chart = new CpuRamChart(canvas, chartData); - }) - - watch(chartDateUpdate, (_: never) => { - chart?.update(); - }) - - watch(JGWC.langRef(), (_: never) => { - chart?.localizeChart(); - }) - - const period: Ref = ref("day"); - - watch(period, (_: never) => { - const hours = (period.value === "day") ? 24 : 1; - chart?.setPeriod(hours * 3600 * 1000); - }); - - return { localize, formatMemory, vmSummary, period }; - } - }); - app.use(JgwcPlugin, []); - app.config.globalProperties.window = window; - app.mount(previewDom); -}; - -window.orgJDrupesVmOperatorVmMgmt.initView = (viewDom: HTMLElement, - _isUpdate: boolean) => { - const app = createApp({ - setup(_props: object) { - const conletId: string - = (viewDom.parentNode!).dataset["conletId"]!; - const resourceBase = (viewDom).dataset.conletResourceBase; - - const controller = reactive(new JGConsole.TableController([ - ["name", "vmname"], - ["running", "running"], - ["runningConditionSince", "since"], - ["currentCpus", "currentCpus"], - ["currentRam", "currentRam"], - ["nodeName", "nodeName"], - ["usedBy", "usedBy"], - ["assignedTo", "assignedTo"] - ], { - sortKey: "name", - sortOrder: "up" - })); - - const filteredData = computed(() => { - const infos = Array.from(vmInfos.values()); - return controller.filter(infos); - }); - - const vmAction = (vmName: string, action: string) => { - JGConsole.notifyConletModel(conletId, action, vmName); - }; - - const idScope = JGWC.createIdScope(); - const detailsByName = reactive(new Set()); - - const submitCallback = (selected: string, value: number | null) => { - if (value === null) { - return localize("Illegal format"); - } - const vmName = selected.substring(0, selected.lastIndexOf(":")); - const property = selected.substring(selected.lastIndexOf(":") + 1); - const vmDef = vmInfos.get(vmName); - const maxValue = vmDef.spec.vm["maximum" - + property.substring(0, 1).toUpperCase() + property.substring(1)]; - if (value > maxValue) { - return localize("Value is above maximum"); - } - JGConsole.notifyConletModel(conletId, property, vmName, value); - return null; - } - - const cic = new ConditionlInputController(submitCallback); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const maximumCpus = (vmDef: any) => { - if (vmDef.spec.vm["maximumCpus"]) { - return vmDef.spec.vm.maximumCpus; - } - const topo = vmDef.spec.vm.cpuTopology; - return Math.max(1, topo.coresPerDie) - * Math.max(1, topo.diesPerSocket) - * Math.max(1, topo.sockets) - * Math.max(1, topo.threadsPerCore); - } - - return { - controller, vmInfos, filteredData, detailsByName, - resourceBase, localize, shortDateTime, formatMemory, - vmAction, cic, parseMemory, maximumCpus, - scopedId: (id: string) => { return idScope.scopedId(id); } - }; - } - }); - app.use(JgwcPlugin); - app.config.globalProperties.window = window; - app.mount(viewDom); -}; - -JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmmgmt.VmMgmt", - // eslint-disable-next-line @typescript-eslint/no-explicit-any - "updateVm", function(_conletId: string, vmDefinition: any) { - // Add some short-cuts for table controller - vmDefinition.name = vmDefinition.metadata.name; - vmDefinition.currentCpus = vmDefinition.status.cpus; - vmDefinition.currentRam = Number(vmDefinition.status.ram); - vmDefinition.usedFrom = vmDefinition.status.consoleClient || ""; - vmDefinition.usedBy = vmDefinition.status.consoleUser || ""; - vmDefinition.assignedTo = vmDefinition.status.assignment?.user || ""; - for (const condition of vmDefinition.status.conditions) { - if (condition.type === "Running") { - vmDefinition.running = condition.status === "True"; - vmDefinition.runningConditionSince - = new Date(condition.lastTransitionTime); - break; - } - } - vmInfos.set(vmDefinition.name, vmDefinition); - }); - -JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmmgmt.VmMgmt", - "removeVm", function(_conletId: string, vmName: string) { - vmInfos.delete(vmName); - }); - -JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmmgmt.VmMgmt", - // eslint-disable-next-line @typescript-eslint/no-explicit-any - "summarySeries", function(_conletId: string, series: any[]) { - chartData.clear(); - for (const entry of series) { - chartData.push(new Date(entry.time * 1000), - entry.values[0], entry.values[1]); - } - chartDateUpdate.value = new Date(); -}); - -JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmmgmt.VmMgmt", - // eslint-disable-next-line @typescript-eslint/no-explicit-any - "updateSummary", function(_conletId: string, summary: any) { - chartData.push(new Date(), summary.usedCpus, Number(summary.usedRam)); - chartDateUpdate.value = new Date(); - Object.assign(vmSummary, summary); -}); - -JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmmgmt.VmMgmt", - "openConsole", function(_conletId: string, data: string) { - let target = document.getElementById( - "org.jdrupes.vmoperator.vmmgt.VmMgmt.target"); - if (!target) { - target = document.createElement("iframe"); - target.id = "org.jdrupes.vmoperator.vmmgt.VmMgmt.target"; - target.setAttribute("name", target.id); - target.setAttribute("style", "display: none;"); - document.querySelector("body")!.append(target); - } - const url = "data:application/x-virt-viewer;base64," - + window.btoa(data); - window.open(url, target.id); - }); - -window.orgJDrupesVmOperatorVmMgmt.confirmReset = - (conletType: string, conletId: string, vmName: string) => { - JGConsole.instance.closeModalDialog(conletType, conletId); - JGConsole.notifyConletModel(conletId, "resetConfirmed", vmName); -} diff --git a/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/browser/VmMgmt-style.scss b/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/browser/VmMgmt-style.scss deleted file mode 100644 index eb1b556..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/browser/VmMgmt-style.scss +++ /dev/null @@ -1,183 +0,0 @@ -/* - * VM-Operator - * Copyright (C) 2023 Michael N. Lipp - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -/* - * Conlet specific styles. - */ - -.jdrupes-vmoperator-vmmgmt-preview { - form { - float: right; - padding: 0.15em 0.3em; - border: 1px solid var(--panel-border); - border-radius: var(--corner-radius); - } - - table { - margin-bottom: 1em; - } - - .vmsChart-wrapper { - height: 12em; - } -} - -.jdrupes-vmoperator-vmmgmt-view-search { - display: flex; - justify-content: flex-end; - - form { - white-space: nowrap; - } -} - -.jdrupes-vmoperator-vmmgmt-view-table { - td { - vertical-align: top; - - &[tabindex] { - outline: 1px solid var(--primary); - cursor: text; - } - - &:not([colspan]):first-child { - white-space: nowrap; - } - - &.column-running { - text-align: center; - - span { - &.fa-check { - color: var(--success); - } - - &.fa-close { - color: var(--danger); - } - } - } - - .console-conection-closed { - color: var(--disabled); - } - } - - td.details { - padding-left: 0; - - table { - display: inline-block; - - td:nth-child(2) { - min-width: 7em; - - input { - max-width: 5em; - } - } - - input~span { - margin-left: 0.5em; - color: var(--danger); - } - } - - p { - display: inline-block; - margin: 0.25rem 0.5rem 0.25rem 0.5rem; - vertical-align: top; - } - } -} - -.jdrupes-vmoperator-vmmgmt-view-action-list { - white-space: nowrap; - - & > * + * { - margin-left: 0.5em; - } - - [role=button] { - padding: 0.25rem; - - &:not([aria-disabled]):hover, &[aria-disabled='false']:hover { - box-shadow: var(--darkening); - } - } - - span[role="button"].svg-icon { - display: inline-block; - line-height: 1; - - /* Align with forkawesome */ - font-size: 14px; - fill: var(--primary); - - &[aria-disabled="true"], &[aria-disabled=""] { - fill: var(--disabled); - } - - svg { - height: 2ex; - width: 1em; - } - } - - img { - display: inline; - height: 1.5em; - vertical-align: top; - - &[aria-disabled=''], &[aria-disabled='true'] { - opacity: 0.4; - } - } -} - -.jdrupes-vmoperator-vmmgmt.jdrupes-vmoperator-vmmgmt-confirm-reset { - - [role=button] { - padding: 0.25rem; - - &:not([aria-disabled]):hover, &[aria-disabled='false']:hover { - box-shadow: var(--darkening); - } - } - - span[role="button"].svg-icon { - display: inline-block; - line-height: 1; - /* Align with forkawesome */ - font-size: 14px; - fill: var(--danger); - - &[aria-disabled="true"], &[aria-disabled=""] { - fill: var(--disabled); - } - - svg { - width: 2.5em; - height: 2.5em; - } - } - - p { - text-align: center; - } -} diff --git a/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/browser/l10nBundles-stub.d.ts b/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/browser/l10nBundles-stub.d.ts deleted file mode 100644 index 8ca03f3..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/browser/l10nBundles-stub.d.ts +++ /dev/null @@ -1 +0,0 @@ -export default new Map>(); diff --git a/org.jdrupes.vmoperator.vmmgmt/tsconfig.json b/org.jdrupes.vmoperator.vmmgmt/tsconfig.json deleted file mode 100644 index 96c4072..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/tsconfig.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - "target": "es2015", - "module": "es2015", - "sourceMap": true, - "inlineSources": true, - "declaration": true, - "importHelpers": true, - "strict": true, - "moduleResolution": "node", - "experimentalDecorators": true, - "lib": ["DOM", "ES2020"], - "paths": { - "aash-plugin": ["./build/unpacked/org/jgrapes/webconsole/provider/jgwcvuecomponents/aash-vue-components/lib/AashPlugin"], - "jgconsole": ["./build/unpacked/org/jgrapes/webconsole/base/JGConsole"], - "jgwc": ["./build/unpacked/org/jgrapes/webconsole/provider/jgwcvuecomponents/jgwc-vue-components/jgwc-components"], - "l10nBundles": ["./src/org/jdrupes/vmoperator/vmmgmt/browser/l10nBundles-stub"], - "vue": ["./build/unpacked/org/jgrapes/webconsole/provider/vue/vue/vue"], - "chartjs": ["./build/unpacked/org/jgrapes/webconsole/provider/chartjs/chart.js/auto/auto"] - } - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "l10nBundles-stub.ts"] -} diff --git a/overview.md b/overview.md index e263b6a..0677d51 100644 --- a/overview.md +++ b/overview.md @@ -3,8 +3,5 @@ A Kubernetes operator for running VMs as pods. VM-Operator =========== -The VM-operator enables you to easily run Qemu based VMs as pods -in Kubernetes. It is built on the -[JGrapes](https://mnlipp.github.io/jgrapes/) event driven framework. - -See the project's [home page](https://vm-operator.jdrupes.org/) for details. +The VM-operator is built on the [JGrapes](https://mnlipp.github.io/jgrapes/) +event driven framework. diff --git a/package-lock.json b/package-lock.json index 4bfe990..4eb8aaf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,21 +4,16 @@ "requires": true, "packages": { "": { - "dependencies": { - "markdownlint-cli": "^0.44.0" - }, "devDependencies": { "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-replace": "^5.0.2", "@rollup/plugin-terser": "^0.4.0", - "@typescript-eslint/eslint-plugin": "^6.9.1", "documentation": "^14.0.1", "install": "^0.13.0", "jsdoc": "^4.0.2", - "markdownlint": "^0.37.4", "node-sass": "^9.0.0", "npm": "^8.11.0", - "rollup": "^4.1.5", + "rollup": "^3.17.2", "rollup-plugin-peer-deps-external": "^2.2.3", "rollup-plugin-postcss": "^4.0.2", "rollup-plugin-typescript2": "^0.36.0", @@ -32,56 +27,118 @@ } }, "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", "dev": true, "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dev": true, "dependencies": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/compat-data": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", - "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.2.tgz", + "integrity": "sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", - "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz", + "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helpers": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/template": "^7.24.7", - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.23.0", + "@babel/helpers": "^7.23.2", + "@babel/parser": "^7.23.0", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -96,24 +153,15 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/generator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", - "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dev": true, "dependencies": { - "@babel/types": "^7.24.7", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" }, "engines": { @@ -121,14 +169,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", - "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", + "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.24.7", - "@babel/helper-validator-option": "^7.24.7", - "browserslist": "^4.22.2", + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.15", + "browserslist": "^4.21.9", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -136,76 +184,63 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", - "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, - "dependencies": { - "@babel/types": "^7.24.7" - }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", - "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", - "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "dependencies": { - "@babel/types": "^7.24.7" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", - "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", "dev": true, "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", - "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz", + "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7" + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -215,80 +250,79 @@ } }, "node_modules/@babel/helper-simple-access": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", - "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", "dev": true, "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", - "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "dependencies": { - "@babel/types": "^7.24.7" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", - "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", - "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", + "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", - "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", + "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", "dev": true, "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", + "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "js-tokens": "^4.0.0" }, "engines": { "node": ">=6.9.0" @@ -357,9 +391,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", - "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -369,34 +403,34 @@ } }, "node_modules/@babel/template": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", - "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", - "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-hoist-variables": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7", - "debug": "^4.3.1", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "debug": "^4.1.0", "globals": "^11.1.0" }, "engines": { @@ -404,347 +438,65 @@ } }, "node_modules/@babel/types": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", - "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz", - "integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==", - "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "peer": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "peer": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "peer": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "peer": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/eslintrc/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", - "dev": true, - "peer": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "dev": true }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "deprecated": "Use @eslint/config-array instead", - "dev": true, - "peer": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "peer": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", - "dev": true, - "peer": true - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", "dev": true, "dependencies": { - "@jridgewell/set-array": "^1.2.1", + "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" + "@jridgewell/trace-mapping": "^0.3.9" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", "dev": true, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", "dev": true, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", "dev": true, "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" } }, "node_modules/@jridgewell/sourcemap-codec": { @@ -754,9 +506,9 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", + "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -764,9 +516,9 @@ } }, "node_modules/@jsdoc/salty": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.8.tgz", - "integrity": "sha512-5e+SFVavj1ORKlKaKr2BmTOekmXbelU7dC0cDkQLqag7xfuTPuGMUFx7KWJuv4bYZrTsoL2Z18VVCOKYxzoHcg==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.5.tgz", + "integrity": "sha512-TfRP53RqunNe2HBobVBJ0VLhK1HbfvBYeTC1ahnN64PWvyYyGebmMiPkuwvD9fpw2ZbkoPb8Q7mwy0aR8Z9rvw==", "dev": true, "dependencies": { "lodash": "^4.17.21" @@ -775,118 +527,6 @@ "node": ">=v12.0.0" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@npmcli/agent": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz", - "integrity": "sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "lru-cache": "^10.0.1", - "socks-proxy-agent": "^8.0.3" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/agent/node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dev": true, - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@npmcli/agent/node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@npmcli/agent/node_modules/https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", - "dev": true, - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", - "dev": true, - "engines": { - "node": "14 || >=16.14" - } - }, - "node_modules/@npmcli/agent/node_modules/socks-proxy-agent": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.3.tgz", - "integrity": "sha512-VNegTZKhuGq5vSD6XNKlbqWhyt/40CgoEw8XxD6dhnm8Jq9IEa3nIa4HwnM8XOqU0CdB0BwWVXusqiFXfHB3+A==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.1", - "debug": "^4.3.4", - "socks": "^2.7.1" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/@npmcli/fs": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", @@ -900,6 +540,39 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/@npmcli/fs/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/fs/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/fs/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/@npmcli/move-file": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", @@ -914,16 +587,6 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@rollup/plugin-node-resolve": { "version": "15.2.3", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", @@ -950,13 +613,13 @@ } }, "node_modules/@rollup/plugin-replace": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.7.tgz", - "integrity": "sha512-PqxSfuorkHz/SPpyngLyg5GCEkOcee9M1bkxiVDr41Pd61mqP1PLOoDPbpl44SB2mQGKwV/In74gqQmGITOhEQ==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.3.tgz", + "integrity": "sha512-je7fu05B800IrMlWjb2wzJcdXzHYW46iTipfChnBDbIbDXhASZs27W1B58T2Yf45jZtJUONegpbce+9Ut2Ti/Q==", "dev": true, "dependencies": { "@rollup/pluginutils": "^5.0.1", - "magic-string": "^0.30.3" + "magic-string": "^0.27.0" }, "engines": { "node": ">=14.0.0" @@ -993,9 +656,9 @@ } }, "node_modules/@rollup/pluginutils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", - "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.5.tgz", + "integrity": "sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==", "dev": true, "dependencies": { "@types/estree": "^1.0.0", @@ -1014,214 +677,6 @@ } } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", - "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", - "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", - "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", - "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", - "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", - "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", - "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", - "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", - "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", - "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", - "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", - "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", - "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", - "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", - "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", - "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -1241,92 +696,82 @@ } }, "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.9.tgz", + "integrity": "sha512-8Hz50m2eoS56ldRlepxSBa6PWEVCtzUo/92HgLc2qTMnotJNIm7xP+UZhyWoYsyOdd5dxZ+NZLb24rsKyFs2ow==", + "dev": true, "dependencies": { "@types/ms": "*" } }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.2.tgz", + "integrity": "sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==", "dev": true }, "node_modules/@types/extend": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/extend/-/extend-3.0.4.tgz", - "integrity": "sha512-ArMouDUTJEz1SQRpFsT2rIw7DeqICFv5aaVzLSIYMYQSLcwcGOfT3VyglQs/p7K3F7fT4zxr0NWxYZIdifD6dA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-CqDQhn7jxaN9zw7zAu926zIx51ZzMaX8U8Wa4jGpKI6jeBr9ejFE68AQ+h+ztfrNJD+leo7K1cLbvMjpHfZSRg==", "dev": true }, "node_modules/@types/hast": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", - "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.6.tgz", + "integrity": "sha512-47rJE80oqPmFdVDCD7IheXBrVdwuBgsYwoczFvKmwfo2Mzsnt+V9OONsYauFmICb6lQPpCuXYJWejBNs4pDJRg==", "dev": true, "dependencies": { "@types/unist": "^2" } }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true - }, - "node_modules/@types/katex": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.7.tgz", - "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==", - "license": "MIT" - }, "node_modules/@types/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-pTjcqY9E4nOI55Wgpz7eiI8+LzdYnw3qxXCfHyBDdPbYvbyLgWLJGh8EdPvqawwMK1Uo1794AUkkR38Fr0g+2g==", "dev": true }, "node_modules/@types/markdown-it": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.1.tgz", - "integrity": "sha512-4NpsnpYl2Gt1ljyBGrKMxFYAYvpqbnnkgP/i/g+NLpjEUa3obn1XJCur9YbEXKDAkaXqsR1LbDnGEJ0MmKFxfg==", + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", "dev": true, "dependencies": { - "@types/linkify-it": "^5", - "@types/mdurl": "^2" + "@types/linkify-it": "*", + "@types/mdurl": "*" } }, "node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.13.tgz", + "integrity": "sha512-HjiGiWedR0DVFkeNljpa6Lv4/IZU1+30VY5d747K7lBudFc3R0Ibr6yJ9lN3BE28VnZyDfLF/VB1Ql1ZIbKrmg==", "dev": true, "dependencies": { "@types/unist": "^2" } }, "node_modules/@types/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.3.tgz", + "integrity": "sha512-T5k6kTXak79gwmIOaDF2UUQXFbnBE0zBUzF20pz7wDYu0RQMzWg+Ml/Pz50214NsFHBITkoi5VtdjFZnJ2ijjA==", "dev": true }, "node_modules/@types/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.3.tgz", + "integrity": "sha512-ZYFzrvyWUNhaPomn80dsMNgMeXxNWZBdkuG/hWlUvXvbdUH8ZERNBGXnU87McuGcWDsyzX2aChCv/SVN348k3A==", "dev": true }, "node_modules/@types/ms": { - "version": "0.7.34", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", - "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" + "version": "0.7.32", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.32.tgz", + "integrity": "sha512-xPSg0jm4mqgEkNhowKgZFBNtwoEwF6gJ4Dhww+GFpm3IgtNseHQZ5IqdNwnquZEoANxyDAKDRAdVo4Z72VvD/g==", + "dev": true }, "node_modules/@types/normalize-package-data": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.2.tgz", + "integrity": "sha512-lqa4UEhhv/2sjjIQgjX8B+RBjj47eo0mzGasklVJ78UKGQY1r0VpB9XHDaZZO9qzEFDdy4MrXLuEaSmPrPSe/A==", "dev": true }, "node_modules/@types/parse5": { @@ -1341,290 +786,121 @@ "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "dev": true }, - "node_modules/@types/semver": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", - "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", - "dev": true - }, "node_modules/@types/supports-color": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/@types/supports-color/-/supports-color-8.1.3.tgz", - "integrity": "sha512-Hy6UMpxhE3j1tLpl27exp1XqHD7n8chAiNPzWfz16LPZoMMoSc4dzLl6w9qijkEb/r5O1ozdu1CWGA2L83ZeZg==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@types/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-dPWnWsf+kzIG140B8z2w3fr5D03TLWbOAFQl45xUpI3vcizeXriNR5VYkWZ+WTMsUHqZ9Xlt3hrxGNANFyNQfw==", "dev": true }, "node_modules/@types/unist": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", - "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", - "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", - "dev": true, - "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/type-utils": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.4", - "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", - "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", - "dev": true, - "peer": true, - "dependencies": { - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", - "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", - "dev": true, - "dependencies": { - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", - "dev": true, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", - "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "semver": "^7.5.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true, - "peer": true + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.8.tgz", + "integrity": "sha512-d0XxK3YTObnWVp6rZuev3c49+j4Lo8g4L1ZRm9z5L0xpoZycUPshHgczK5gsUMaZOstjVYYi09p5gYvUtfChYw==", + "dev": true }, "node_modules/@vue/compiler-core": { - "version": "3.4.29", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.29.tgz", - "integrity": "sha512-TFKiRkKKsRCKvg/jTSSKK7mYLJEQdUiUfykbG49rubC9SfDyvT2JrzTReopWlz2MxqeLyxh9UZhvxEIBgAhtrg==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.4.tgz", + "integrity": "sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==", "dev": true, "dependencies": { - "@babel/parser": "^7.24.7", - "@vue/shared": "3.4.29", - "entities": "^4.5.0", + "@babel/parser": "^7.21.3", + "@vue/shared": "3.3.4", "estree-walker": "^2.0.2", - "source-map-js": "^1.2.0" + "source-map-js": "^1.0.2" } }, "node_modules/@vue/compiler-dom": { - "version": "3.4.29", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.29.tgz", - "integrity": "sha512-A6+iZ2fKIEGnfPJejdB7b1FlJzgiD+Y/sxxKwJWg1EbJu6ZPgzaPQQ51ESGNv0CP6jm6Z7/pO6Ia8Ze6IKrX7w==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz", + "integrity": "sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==", "dev": true, "dependencies": { - "@vue/compiler-core": "3.4.29", - "@vue/shared": "3.4.29" + "@vue/compiler-core": "3.3.4", + "@vue/shared": "3.3.4" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.4.29", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.29.tgz", - "integrity": "sha512-zygDcEtn8ZimDlrEQyLUovoWgKQic6aEQqRXce2WXBvSeHbEbcAsXyCk9oG33ZkyWH4sl9D3tkYc1idoOkdqZQ==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz", + "integrity": "sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==", "dev": true, "dependencies": { - "@babel/parser": "^7.24.7", - "@vue/compiler-core": "3.4.29", - "@vue/compiler-dom": "3.4.29", - "@vue/compiler-ssr": "3.4.29", - "@vue/shared": "3.4.29", + "@babel/parser": "^7.20.15", + "@vue/compiler-core": "3.3.4", + "@vue/compiler-dom": "3.3.4", + "@vue/compiler-ssr": "3.3.4", + "@vue/reactivity-transform": "3.3.4", + "@vue/shared": "3.3.4", "estree-walker": "^2.0.2", - "magic-string": "^0.30.10", - "postcss": "^8.4.38", - "source-map-js": "^1.2.0" + "magic-string": "^0.30.0", + "postcss": "^8.1.10", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/magic-string": { + "version": "0.30.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", + "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.4.29", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.29.tgz", - "integrity": "sha512-rFbwCmxJ16tDp3N8XCx5xSQzjhidYjXllvEcqX/lopkoznlNPz3jyy0WGJCyhAaVQK677WWFt3YO/WUEkMMUFQ==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz", + "integrity": "sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==", "dev": true, "dependencies": { - "@vue/compiler-dom": "3.4.29", - "@vue/shared": "3.4.29" + "@vue/compiler-dom": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "node_modules/@vue/reactivity-transform": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz", + "integrity": "sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.15", + "@vue/compiler-core": "3.3.4", + "@vue/shared": "3.3.4", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.0" + } + }, + "node_modules/@vue/reactivity-transform/node_modules/magic-string": { + "version": "0.30.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", + "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" } }, "node_modules/@vue/shared": { - "version": "3.4.29", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", - "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz", + "integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==", "dev": true }, "node_modules/abbrev": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", - "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true }, "node_modules/acorn": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", - "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -1633,16 +909,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "peer": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -1680,23 +946,6 @@ "node": ">=8" } }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -1740,19 +989,30 @@ "node": ">= 8" } }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "dev": true + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "dev": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "engines": { - "node": ">=8" - } + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, "node_modules/arrify": { "version": "1.0.1", @@ -1785,18 +1045,16 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true, "engines": { "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/bluebird": { @@ -1815,26 +1073,27 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "dev": true, "dependencies": { - "fill-range": "^7.1.1" + "fill-range": "^7.0.1" }, "engines": { "node": ">=8" } }, "node_modules/browserslist": { - "version": "4.23.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", - "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", "dev": true, "funding": [ { @@ -1851,10 +1110,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001629", - "electron-to-chromium": "^1.4.796", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.16" + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" }, "bin": { "browserslist": "cli.js" @@ -1919,16 +1178,6 @@ "node": ">=12" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6" - } - }, "node_modules/camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -1968,9 +1217,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001634", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001634.tgz", - "integrity": "sha512-fbBYXQ9q3+yp1q1gBk86tOFs4pyn/yxFm5ZNP18OXJDfA3txImOY9PhfxVggZ4vRHDqoU8NrKU81eN0OtzOgRA==", + "version": "1.0.30001547", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001547.tgz", + "integrity": "sha512-W7CrtIModMAxobGhz8iXmDfuJiiKg1WADMO/9x7/CLNin5cpSbuBjooyoIUVB5eyCc36QuTVlkVa1iB2S5+/eA==", "dev": true, "funding": [ { @@ -2025,6 +1274,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "dev": true, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -2044,26 +1294,23 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-reference-invalid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", - "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", - "license": "MIT", + "dev": true, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -2076,9 +1323,6 @@ "engines": { "node": ">= 8.10.0" }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, "optionalDependencies": { "fsevents": "~2.3.2" } @@ -2133,6 +1377,15 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "bin": { + "color-support": "bin.js" + } + }, "node_modules/colord": { "version": "2.9.3", "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", @@ -2179,6 +1432,12 @@ "source-map": "^0.6.1" } }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2366,9 +1625,10 @@ "optional": true }, "node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -2419,6 +1679,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", + "dev": true, "dependencies": { "character-entities": "^2.0.0" }, @@ -2427,22 +1688,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "peer": true - }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -2452,61 +1697,30 @@ "node": ">=0.10.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, "engines": { "node": ">=6" } }, - "node_modules/devlop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", "dev": true, "engines": { "node": ">=0.3.1" } }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "peer": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/doctrine-temporary-fork": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine-temporary-fork/-/doctrine-temporary-fork-2.1.0.tgz", @@ -2520,9 +1734,9 @@ } }, "node_modules/documentation": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/documentation/-/documentation-14.0.3.tgz", - "integrity": "sha512-B7cAviVKN9Rw7Ofd+9grhVuxiHwly6Ieh+d/ceMw8UdBOv/irkuwnDEJP8tq0wgdLJDUVuIkovV+AX9mTrZFxg==", + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/documentation/-/documentation-14.0.2.tgz", + "integrity": "sha512-hWoTf8/u4pOjib02L7w94hwmhPfcSwyJNGtlPdGVe8GFyq8HkzcFzQQltaaikKunHEp0YSwDAbwBAO7nxrWIfA==", "dev": true, "dependencies": { "@babel/core": "^7.18.10", @@ -2588,15 +1802,6 @@ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, - "node_modules/dom-serializer/node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/domelementtype": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", @@ -2645,9 +1850,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.803", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.803.tgz", - "integrity": "sha512-61H9mLzGOCLLVsnLiRzCbc63uldP0AniRYPV3hbGVtONA1pI7qSGILdbofR7A8TMbOypDocEAjH/e+9k1QIe3g==", + "version": "1.4.553", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.553.tgz", + "integrity": "sha512-HiRdtyKS2+VhiXvjhMvvxiMC33FJJqTA5EB2YHgFZW6v7HkK4Q9Ahv2V7O2ZPgAjw+MyCJVMQvigj13H8t+wvA==", "dev": true }, "node_modules/emoji-regex": { @@ -2667,12 +1872,10 @@ } }, "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "engines": { - "node": ">=0.12" - }, + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "dev": true, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" } @@ -2702,270 +1905,21 @@ } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", "dev": true, "engines": { "node": ">=6" } }, "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", "dev": true, - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", - "dev": true, - "peer": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "peer": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "peer": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "peer": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/eslint/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "peer": true, - "dependencies": { - "type-fest": "^0.20.2" - }, "engines": { "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "peer": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "peer": true, - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, - "peer": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "peer": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4.0" } }, "node_modules/estree-walker": { @@ -2989,81 +1943,16 @@ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "dev": true }, - "node_modules/exponential-backoff": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", - "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", - "dev": true - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "dev": true }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "peer": true - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "peer": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "peer": true - }, - "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "peer": true, - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -3090,60 +1979,21 @@ } }, "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", "dev": true, - "peer": true, "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" }, "engines": { - "node": ">=10" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "peer": true, - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true, - "peer": true - }, - "node_modules/foreground-child": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", - "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -3173,7 +2023,8 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true }, "node_modules/fsevents": { "version": "2.3.3", @@ -3189,13 +2040,23 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/gaze": { @@ -3257,9 +2118,9 @@ } }, "node_modules/git-url-parse": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-13.1.1.tgz", - "integrity": "sha512-PCFJyeSSdtnbfhSNRw9Wk96dDCNx+sogTe4YNXeXSJxt7xz5hvXekuRn9JX7m+Mf4OscCu8h+mtAl3+h5Fo8lQ==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-13.1.0.tgz", + "integrity": "sha512-5FvPJP/70WkIprlUZ33bm4UAaFdjcLkJLpWft1BeZKqwR0uhhNGoKwlUaPtVb4LxCSQ++erHapRak9kWGj+FCA==", "dev": true, "dependencies": { "git-up": "^7.0.0" @@ -3272,17 +2133,19 @@ "dev": true }, "node_modules/glob": { - "version": "9.3.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", - "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, "dependencies": { "fs.realpath": "^1.0.0", - "minimatch": "^8.0.2", - "minipass": "^4.2.4", - "path-scurry": "^1.6.1" + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -3300,28 +2163,6 @@ "node": ">= 6" } }, - "node_modules/glob/node_modules/minimatch": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz", - "integrity": "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob/node_modules/minipass": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", - "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", - "engines": { - "node": ">=8" - } - }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -3337,26 +2178,6 @@ "integrity": "sha512-qpPnUKkWnz8NESjrCvnlGklsgiQzlq+rcCxoG5uNQ+dNA7cFMCmn231slLAwS2N/PlkzZ3COL8CcS10jXmLHqg==", "dev": true }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/globule": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.4.tgz", @@ -3381,6 +2202,26 @@ "concat-map": "0.0.1" } }, + "node_modules/globule/node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globule/node_modules/minimatch": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", @@ -3399,12 +2240,6 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, "node_modules/hard-rejection": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", @@ -3414,6 +2249,15 @@ "node": ">=6" } }, + "node_modules/has": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", + "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -3423,24 +2267,18 @@ "node": ">=4" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "dev": true + }, "node_modules/hash-sum": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-2.0.0.tgz", "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==", "dev": true }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/hast-util-from-parse5": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-7.1.2.tgz", @@ -3709,19 +2547,10 @@ "postcss": "^8.1.0" } }, - "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, "node_modules/immutable": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", - "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz", + "integrity": "sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==", "dev": true }, "node_modules/import-cwd": { @@ -3736,23 +2565,6 @@ "node": ">=8" } }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "peer": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/import-from": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz", @@ -3765,15 +2577,6 @@ "node": ">=8" } }, - "node_modules/import-from/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -3798,6 +2601,16 @@ "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", "dev": true }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -3822,18 +2635,11 @@ "node": ">= 0.10" } }, - "node_modules/ip-address": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", - "dev": true, - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, - "engines": { - "node": ">= 12" - } + "node_modules/ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", + "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", + "dev": true }, "node_modules/is-absolute": { "version": "1.0.0", @@ -3848,30 +2654,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-alphabetical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", - "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-alphanumerical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", - "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", - "license": "MIT", - "dependencies": { - "is-alphabetical": "^2.0.0", - "is-decimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -3929,27 +2711,17 @@ } }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", "dev": true, "dependencies": { - "hasown": "^2.0.0" + "has": "^1.0.3" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-decimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", - "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3980,16 +2752,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-hexadecimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", - "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/is-lambda": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", @@ -4011,16 +2773,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, "node_modules/is-plain-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", @@ -4084,24 +2836,6 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, - "node_modules/jackspeak": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.0.tgz", - "integrity": "sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==", - "dev": true, - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/js-base64": { "version": "2.6.4", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", @@ -4118,6 +2852,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -4134,28 +2869,22 @@ "xmlcreate": "^2.0.4" } }, - "node_modules/jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", - "dev": true - }, "node_modules/jsdoc": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.3.tgz", - "integrity": "sha512-Nu7Sf35kXJ1MWDZIMAuATRQTg1iIPdzh7tqJ6jjvaU/GfDf+qi5UV8zJR3Mo+/pYFvm8mzay4+6O5EWigaQBQw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.2.tgz", + "integrity": "sha512-e8cIg2z62InH7azBBi3EsSEqrKx+nUtAS5bBcYTSpZFA+vhNPyhv8PTFZ0WsjOPDj04/dOLlm08EDcQJDqaGQg==", "dev": true, "dependencies": { "@babel/parser": "^7.20.15", "@jsdoc/salty": "^0.2.1", - "@types/markdown-it": "^14.1.1", + "@types/markdown-it": "^12.2.3", "bluebird": "^3.7.2", "catharsis": "^0.9.0", "escape-string-regexp": "^2.0.0", "js2xmlparser": "^4.0.2", "klaw": "^3.0.0", - "markdown-it": "^14.1.0", - "markdown-it-anchor": "^8.6.7", + "markdown-it": "^12.3.2", + "markdown-it-anchor": "^8.4.1", "marked": "^4.0.10", "mkdirp": "^1.0.4", "requizzle": "^0.2.3", @@ -4169,15 +2898,6 @@ "node": ">=12.0.0" } }, - "node_modules/jsdoc/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/jsdoc/node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -4202,33 +2922,12 @@ "node": ">=4" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "peer": true - }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "peer": true - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "peer": true - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -4242,10 +2941,10 @@ } }, "node_modules/jsonc-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", - "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", - "license": "MIT" + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true }, "node_modules/jsonfile": { "version": "6.1.0", @@ -4259,50 +2958,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/jsonpointer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", - "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/katex": { - "version": "0.16.21", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz", - "integrity": "sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==", - "funding": [ - "https://opencollective.com/katex", - "https://github.com/sponsors/katex" - ], - "license": "MIT", - "dependencies": { - "commander": "^8.3.0" - }, - "bin": { - "katex": "cli.js" - } - }, - "node_modules/katex/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "peer": true, - "dependencies": { - "json-buffer": "3.0.1" - } - }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -4340,20 +2995,6 @@ "@babel/traverse": "^7.10.5" } }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "peer": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -4370,33 +3011,33 @@ "dev": true }, "node_modules/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "dev": true, "dependencies": { - "uc.micro": "^2.0.0" + "uc.micro": "^1.0.1" } }, "node_modules/loader-utils": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", - "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", "dev": true, "engines": { "node": ">= 12.13.0" } }, "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", "dev": true, - "peer": true, "dependencies": { - "p-locate": "^5.0.0" + "p-locate": "^6.0.0" }, "engines": { - "node": ">=10" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -4420,13 +3061,6 @@ "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", "dev": true }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "peer": true - }, "node_modules/lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -4459,12 +3093,15 @@ "dev": true }, "node_modules/magic-string": { - "version": "0.30.10", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", - "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", "dev": true, "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" } }, "node_modules/make-dir": { @@ -4482,15 +3119,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/make-fetch-happen": { "version": "10.2.1", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", @@ -4549,19 +3177,19 @@ } }, "node_modules/markdown-it": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", - "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "dev": true, "dependencies": { "argparse": "^2.0.1", - "entities": "^4.4.0", - "linkify-it": "^5.0.0", - "mdurl": "^2.0.0", - "punycode.js": "^2.3.1", - "uc.micro": "^2.1.0" + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" }, "bin": { - "markdown-it": "bin/markdown-it.mjs" + "markdown-it": "bin/markdown-it.js" } }, "node_modules/markdown-it-anchor": { @@ -4584,559 +3212,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/markdownlint": { - "version": "0.37.4", - "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.37.4.tgz", - "integrity": "sha512-u00joA/syf3VhWh6/ybVFkib5Zpj2e5KB/cfCei8fkSRuums6nyisTWGqjTWIOFoFwuXoTBQQiqlB4qFKp8ncQ==", - "license": "MIT", - "dependencies": { - "markdown-it": "14.1.0", - "micromark": "4.0.1", - "micromark-core-commonmark": "2.0.2", - "micromark-extension-directive": "3.0.2", - "micromark-extension-gfm-autolink-literal": "2.1.0", - "micromark-extension-gfm-footnote": "2.1.0", - "micromark-extension-gfm-table": "2.1.0", - "micromark-extension-math": "3.1.0", - "micromark-util-types": "2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/DavidAnson" - } - }, - "node_modules/markdownlint-cli": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.44.0.tgz", - "integrity": "sha512-ZJTAONlvF9NkrIBltCdW15DxN9UTbPiKMEqAh2EU2gwIFlrCMavyCEPPO121cqfYOrLUJWW8/XKWongstmmTeQ==", - "license": "MIT", - "dependencies": { - "commander": "~13.1.0", - "glob": "~10.4.5", - "ignore": "~7.0.3", - "js-yaml": "~4.1.0", - "jsonc-parser": "~3.3.1", - "jsonpointer": "~5.0.1", - "markdownlint": "~0.37.4", - "minimatch": "~9.0.5", - "run-con": "~1.3.2", - "smol-toml": "~1.3.1" - }, - "bin": { - "markdownlint": "markdownlint.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/markdownlint-cli/node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/markdownlint-cli/node_modules/ignore": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.3.tgz", - "integrity": "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/markdownlint-cli/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/markdownlint/node_modules/micromark": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.1.tgz", - "integrity": "sha512-eBPdkcoCNvYcxQOAKAlceo5SNdzZWfF+FcSupREAzdAh9rRmE239CEQAiTwIgblwnoM8zzj35sZ5ZwvSEOF6Kw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/markdownlint/node_modules/micromark-core-commonmark": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.2.tgz", - "integrity": "sha512-FKjQKbxd1cibWMM1P9N+H8TwlgGgSkWZMmfuVucLCHaYqeSvJ0hFeHsIa65pA2nYbes0f8LDHPMrd9X7Ujxg9w==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-destination": "^2.0.0", - "micromark-factory-label": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-factory-title": "^2.0.0", - "micromark-factory-whitespace": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-html-tag-name": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/markdownlint/node_modules/micromark-extension-gfm-autolink-literal": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", - "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/markdownlint/node_modules/micromark-extension-gfm-footnote": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", - "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/markdownlint/node_modules/micromark-extension-gfm-table": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.0.tgz", - "integrity": "sha512-Ub2ncQv+fwD70/l4ou27b4YzfNaCJOvyX4HxXU15m7mpYY+rjuWzsLIPZHJL253Z643RpbcP1oeIJlQ/SKW67g==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/markdownlint/node_modules/micromark-factory-destination": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", - "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/markdownlint/node_modules/micromark-factory-label": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", - "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/markdownlint/node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/markdownlint/node_modules/micromark-factory-title": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", - "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/markdownlint/node_modules/micromark-factory-whitespace": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", - "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/markdownlint/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/markdownlint/node_modules/micromark-util-chunked": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", - "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/markdownlint/node_modules/micromark-util-classify-character": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", - "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/markdownlint/node_modules/micromark-util-combine-extensions": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", - "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/markdownlint/node_modules/micromark-util-decode-numeric-character-reference": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", - "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/markdownlint/node_modules/micromark-util-encode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", - "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/markdownlint/node_modules/micromark-util-html-tag-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", - "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/markdownlint/node_modules/micromark-util-normalize-identifier": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", - "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/markdownlint/node_modules/micromark-util-resolve-all": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", - "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/markdownlint/node_modules/micromark-util-sanitize-uri": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", - "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/markdownlint/node_modules/micromark-util-subtokenize": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", - "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/markdownlint/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/markdownlint/node_modules/micromark-util-types": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.1.tgz", - "integrity": "sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, "node_modules/marked": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", @@ -5454,9 +3529,10 @@ "dev": true }, "node_modules/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "dev": true }, "node_modules/meow": { "version": "9.0.0", @@ -5542,6 +3618,15 @@ "node": ">=8" } }, + "node_modules/meow/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/meow/node_modules/read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -5613,15 +3698,6 @@ "semver": "bin/semver" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, "node_modules/micromark": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", @@ -5691,119 +3767,6 @@ "uvu": "^0.5.0" } }, - "node_modules/micromark-extension-directive": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-3.0.2.tgz", - "integrity": "sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-factory-whitespace": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "parse-entities": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-directive/node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-directive/node_modules/micromark-factory-whitespace": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", - "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-directive/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-directive/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-extension-directive/node_modules/micromark-util-types": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", - "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, "node_modules/micromark-extension-gfm": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-2.0.3.tgz", @@ -5925,97 +3888,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/micromark-extension-math": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", - "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", - "license": "MIT", - "dependencies": { - "@types/katex": "^0.16.0", - "devlop": "^1.0.0", - "katex": "^0.16.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-math/node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-math/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-math/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-extension-math/node_modules/micromark-util-types": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", - "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, "node_modules/micromark-factory-destination": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", @@ -6389,19 +4261,6 @@ } ] }, - "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", - "dev": true, - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -6412,27 +4271,15 @@ } }, "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=10" } }, "node_modules/minimist-options": { @@ -6575,18 +4422,19 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, "node_modules/nan": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", - "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==", + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", + "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==", "dev": true }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", "dev": true, "funding": [ { @@ -6601,12 +4449,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -6617,359 +4459,152 @@ } }, "node_modules/node-gyp": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.1.0.tgz", - "integrity": "sha512-B4J5M1cABxPc5PwfjhbV5hoy2DP9p8lFXASnEN6hugXOa61416tnTZ29x9sSwAd0o99XNIcpvDDy1swAExsVKA==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", "dev": true, "dependencies": { "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "glob": "^10.3.10", + "glob": "^7.1.4", "graceful-fs": "^4.2.6", - "make-fetch-happen": "^13.0.0", - "nopt": "^7.0.0", - "proc-log": "^3.0.0", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", "semver": "^7.3.5", "tar": "^6.1.2", - "which": "^4.0.0" + "which": "^2.0.2" }, "bin": { "node-gyp": "bin/node-gyp.js" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": ">= 10.12.0" } }, "node_modules/node-gyp/node_modules/@npmcli/fs": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", - "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", "dev": true, "dependencies": { + "@gar/promisify": "^1.0.1", "semver": "^7.3.5" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/node-gyp/node_modules/cacache": { - "version": "18.0.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.3.tgz", - "integrity": "sha512-qXCd4rh6I07cnDqh8V48/94Tc/WSfj+o3Gn6NZ0aZovS255bUx8O13uKxRFd2eWG0xgsco7+YItQNPaa5E85hg==", + "node_modules/node-gyp/node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", "dev": true, "dependencies": { - "@npmcli/fs": "^3.1.0", - "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^10.0.1", - "minipass": "^7.0.3", - "minipass-collect": "^2.0.1", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^4.0.0", - "ssri": "^10.0.0", - "tar": "^6.1.11", - "unique-filename": "^3.0.0" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/node-gyp/node_modules/cacache/node_modules/glob": { - "version": "9.3.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", - "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "minimatch": "^8.0.2", - "minipass": "^4.2.4", - "path-scurry": "^1.6.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/node-gyp/node_modules/cacache/node_modules/glob/node_modules/minipass": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", - "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/node-gyp/node_modules/cacache/node_modules/minimatch": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz", - "integrity": "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/node-gyp/node_modules/fs-minipass": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", - "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", - "dev": true, - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/node-gyp/node_modules/glob": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", - "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/node-gyp/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, - "engines": { - "node": ">=16" - } - }, - "node_modules/node-gyp/node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", - "dev": true, - "engines": { - "node": "14 || >=16.14" - } - }, - "node_modules/node-gyp/node_modules/make-fetch-happen": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", - "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==", - "dev": true, - "dependencies": { - "@npmcli/agent": "^2.0.0", - "cacache": "^18.0.0", - "http-cache-semantics": "^4.1.1", - "is-lambda": "^1.0.1", - "minipass": "^7.0.2", - "minipass-fetch": "^3.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "proc-log": "^4.2.0", - "promise-retry": "^2.0.1", - "ssri": "^10.0.0" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/node-gyp/node_modules/make-fetch-happen/node_modules/proc-log": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", - "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/node-gyp/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/node-gyp/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/node-gyp/node_modules/minipass-collect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", - "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", - "dev": true, - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/node-gyp/node_modules/minipass-fetch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", - "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", - "dev": true, - "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/node-gyp/node_modules/ssri": { - "version": "10.0.6", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", - "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", - "dev": true, - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/node-gyp/node_modules/unique-filename": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", - "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", - "dev": true, - "dependencies": { - "unique-slug": "^4.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/node-gyp/node_modules/unique-slug": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", - "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", - "dev": true, - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/node-gyp/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", - "dev": true, - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^16.13.0 || >=18.0.0" - } - }, - "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "dev": true - }, - "node_modules/node-sass": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-9.0.0.tgz", - "integrity": "sha512-yltEuuLrfH6M7Pq2gAj5B6Zm7m+gdZoG66wTqG6mIZV/zijq3M2OO2HswtT6oBspPyFhHDcaxWpsBm0fRNDHPg==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "async-foreach": "^0.1.3", - "chalk": "^4.1.2", - "cross-spawn": "^7.0.3", - "gaze": "^1.0.0", - "get-stdin": "^4.0.1", - "glob": "^7.0.3", - "lodash": "^4.17.15", - "make-fetch-happen": "^10.0.4", - "meow": "^9.0.0", - "nan": "^2.17.0", - "node-gyp": "^10.1.0", - "sass-graph": "^4.0.1", - "stdout-stream": "^1.4.0", - "true-case-path": "^2.2.1" - }, - "bin": { - "node-sass": "bin/node-sass" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/node-sass/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" }, "engines": { "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/node-sass/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/node-gyp/node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", "dev": true, "engines": { - "node": ">=8" + "node": ">= 6" } }, - "node_modules/node-sass/node_modules/node-gyp/node_modules/make-fetch-happen": { + "node_modules/node-gyp/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/node-gyp/node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "dev": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/node-gyp/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/node-gyp/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp/node_modules/make-fetch-happen": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", - "extraneous": true, + "dev": true, "dependencies": { "agentkeepalive": "^4.1.3", "cacache": "^15.2.0", @@ -6992,6 +4627,202 @@ "node": ">= 10" } }, + "node_modules/node-gyp/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/node-gyp/node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "dev": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/node-gyp/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp/node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "dev": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/node-gyp/node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "dev": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/node-gyp/node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/node-gyp/node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/node-gyp/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, + "node_modules/node-sass": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-9.0.0.tgz", + "integrity": "sha512-yltEuuLrfH6M7Pq2gAj5B6Zm7m+gdZoG66wTqG6mIZV/zijq3M2OO2HswtT6oBspPyFhHDcaxWpsBm0fRNDHPg==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "async-foreach": "^0.1.3", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.3", + "gaze": "^1.0.0", + "get-stdin": "^4.0.1", + "glob": "^7.0.3", + "lodash": "^4.17.15", + "make-fetch-happen": "^10.0.4", + "meow": "^9.0.0", + "nan": "^2.17.0", + "node-gyp": "^8.4.1", + "sass-graph": "^4.0.1", + "stdout-stream": "^1.4.0", + "true-case-path": "^2.2.1" + }, + "bin": { + "node-sass": "bin/node-sass" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/node-sass/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/node-sass/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/node-sass/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-sass/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/node-sass/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/node-sass/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -7005,18 +4836,18 @@ } }, "node_modules/nopt": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", - "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", "dev": true, "dependencies": { - "abbrev": "^2.0.0" + "abbrev": "1" }, "bin": { "nopt": "bin/nopt.js" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=6" } }, "node_modules/normalize-package-data": { @@ -7034,6 +4865,39 @@ "node": ">=10" } }, + "node_modules/normalize-package-data/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-package-data/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -7135,11 +4999,6 @@ "write-file-atomic" ], "dev": true, - "workspaces": [ - "docs", - "smoke-tests", - "workspaces/*" - ], "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", "@npmcli/arborist": "^5.6.3", @@ -7184,7 +5043,7 @@ "mkdirp": "^1.0.4", "mkdirp-infer-owner": "^2.0.0", "ms": "^2.1.2", - "node-gyp": "^10.1.0", + "node-gyp": "^9.1.0", "nopt": "^6.0.0", "npm-audit-report": "^3.0.0", "npm-install-checks": "^5.0.0", @@ -9626,6 +7485,21 @@ "inBundle": true, "license": "ISC" }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "dev": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -9638,22 +7512,13 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, - "peer": true, "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" + "wrappy": "1" } }, "node_modules/p-finally": { @@ -9666,32 +7531,30 @@ } }, "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", "dev": true, - "peer": true, "dependencies": { - "yocto-queue": "^0.1.0" + "yocto-queue": "^1.0.0" }, "engines": { - "node": ">=10" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", "dev": true, - "peer": true, "dependencies": { - "p-limit": "^3.0.2" + "p-limit": "^4.0.0" }, "engines": { - "node": ">=10" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -9749,38 +7612,6 @@ "node": ">=6" } }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "peer": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-entities": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", - "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "character-entities-legacy": "^3.0.0", - "character-reference-invalid": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "is-alphanumerical": "^2.0.0", - "is-decimal": "^2.0.0", - "is-hexadecimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/parse-filepath": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", @@ -9838,12 +7669,21 @@ "dev": true }, "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", "dev": true, "engines": { - "node": ">=8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" } }, "node_modules/path-key": { @@ -9882,50 +7722,10 @@ "node": ">=0.10.0" } }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", - "engines": { - "node": "14 || >=16.14" - } - }, - "node_modules/path-scurry/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", "dev": true }, "node_modules/picomatch": { @@ -10016,10 +7816,19 @@ "node": ">=8" } }, + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "funding": [ { @@ -10036,9 +7845,9 @@ } ], "dependencies": { - "nanoid": "^3.3.7", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" + "source-map-js": "^1.0.2" }, "engines": { "node": "^10 || ^12 || >=14" @@ -10286,9 +8095,9 @@ } }, "node_modules/postcss-modules-extract-imports": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", - "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", "dev": true, "engines": { "node": "^10 || ^12 || >= 14" @@ -10298,9 +8107,9 @@ } }, "node_modules/postcss-modules-local-by-default": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", - "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz", + "integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==", "dev": true, "dependencies": { "icss-utils": "^5.0.0", @@ -10315,9 +8124,9 @@ } }, "node_modules/postcss-modules-scope": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", - "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", "dev": true, "dependencies": { "postcss-selector-parser": "^6.0.4" @@ -10526,9 +8335,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz", - "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==", + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", "dev": true, "dependencies": { "cssesc": "^3.0.0", @@ -10575,25 +8384,6 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "peer": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/proc-log": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", - "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -10629,9 +8419,9 @@ } }, "node_modules/property-information": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", - "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.3.0.tgz", + "integrity": "sha512-gVNZ74nqhRMiIUYWGQdosYetaKc83x8oT41a0LlV3AAFCAZwCpg4vmGkq8t34+cUhp3cnM4XDiU/7xlgK7HGrg==", "dev": true, "funding": { "type": "github", @@ -10644,44 +8434,6 @@ "integrity": "sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==", "dev": true }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/punycode.js": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", - "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/quick-lru": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", @@ -10735,76 +8487,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/read-pkg-up/node_modules/find-up": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", - "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", - "dev": true, - "dependencies": { - "locate-path": "^7.1.0", - "path-exists": "^5.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/locate-path": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", - "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", - "dev": true, - "dependencies": { - "p-locate": "^6.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/p-locate": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", - "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", - "dev": true, - "dependencies": { - "p-limit": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/path-exists": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", - "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, "node_modules/read-pkg-up/node_modules/type-fest": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", @@ -10817,18 +8499,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/read-pkg-up/node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/read-pkg/node_modules/type-fest": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", @@ -10841,6 +8511,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -11011,13 +8695,12 @@ } }, "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, - "peer": true, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/retry": { @@ -11029,21 +8712,10 @@ "node": ">= 4" } }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "dependencies": { "glob": "^7.1.3" @@ -11055,38 +8727,61 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rollup": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", - "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "dependencies": { - "@types/estree": "1.0.5" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "dev": true, "bin": { "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=18.0.0", + "node": ">=14.18.0", "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.18.0", - "@rollup/rollup-android-arm64": "4.18.0", - "@rollup/rollup-darwin-arm64": "4.18.0", - "@rollup/rollup-darwin-x64": "4.18.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.18.0", - "@rollup/rollup-linux-arm-musleabihf": "4.18.0", - "@rollup/rollup-linux-arm64-gnu": "4.18.0", - "@rollup/rollup-linux-arm64-musl": "4.18.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0", - "@rollup/rollup-linux-riscv64-gnu": "4.18.0", - "@rollup/rollup-linux-s390x-gnu": "4.18.0", - "@rollup/rollup-linux-x64-gnu": "4.18.0", - "@rollup/rollup-linux-x64-musl": "4.18.0", - "@rollup/rollup-win32-arm64-msvc": "4.18.0", - "@rollup/rollup-win32-ia32-msvc": "4.18.0", - "@rollup/rollup-win32-x64-msvc": "4.18.0", "fsevents": "~2.3.2" } }, @@ -11205,6 +8900,39 @@ "node": ">= 8.0.0" } }, + "node_modules/rollup-plugin-typescript2/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/rollup-plugin-typescript2/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/rollup-plugin-typescript2/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/rollup-plugin-vue": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/rollup-plugin-vue/-/rollup-plugin-vue-6.0.0.tgz", @@ -11234,65 +8962,6 @@ "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", "dev": true }, - "node_modules/run-con": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/run-con/-/run-con-1.3.2.tgz", - "integrity": "sha512-CcfE+mYiTcKEzg0IqS08+efdnH0oJ3zV0wSUFBNrMHMuxCtXvBCLzCJHatwuXDcu/RlhjTziTo/a1ruQik6/Yg==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~4.1.0", - "minimist": "^1.2.8", - "strip-json-comments": "~3.1.1" - }, - "bin": { - "run-con": "cli.js" - } - }, - "node_modules/run-con/node_modules/ini": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", - "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/run-con/node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/sade": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", @@ -11339,9 +9008,9 @@ "optional": true }, "node_modules/sass": { - "version": "1.77.5", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.5.tgz", - "integrity": "sha512-oDfX1mukIlxacPdQqNb6mV2tVCrnE+P3nVYioy72V5tlk56CPNcO4TCuFcaCRKKfJ1M3lH95CleRS+dVKL2qMg==", + "version": "1.69.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.3.tgz", + "integrity": "sha512-X99+a2iGdXkdWn1akFPs0ZmelUzyAQfvqYc2P/MPTrJRuIRoTffGzT9W9nFqG00S+c8hXzVmgxhUuHFdrwxkhQ==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -11373,6 +9042,48 @@ "node": ">=12" } }, + "node_modules/sass-graph/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/sass-graph/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sass-graph/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/scss-tokenizer": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.4.3.tgz", @@ -11393,26 +9104,29 @@ } }, "node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" } }, "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", "dev": true, "dependencies": { "randombytes": "^2.1.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -11435,9 +9149,9 @@ } }, "node_modules/shiki": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.7.tgz", - "integrity": "sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==", + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.5.tgz", + "integrity": "sha512-1gCAYOcmCFONmErGTrS1fjzJLA7MGZmKzrBNX7apqSwhyITJg2O102uFzXUeBxNnEkDA9vHIKLyeKq0V083vIw==", "dev": true, "dependencies": { "ansi-sequence-parser": "^1.1.0", @@ -11447,25 +9161,10 @@ } }, "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true }, "node_modules/smart-buffer": { "version": "4.2.0", @@ -11478,34 +9177,22 @@ } }, "node_modules/smob": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", - "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.4.1.tgz", + "integrity": "sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==", "dev": true }, - "node_modules/smol-toml": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.3.1.tgz", - "integrity": "sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">= 18" - }, - "funding": { - "url": "https://github.com/sponsors/cyyynthia" - } - }, "node_modules/socks": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", - "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", + "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", "dev": true, "dependencies": { - "ip-address": "^9.0.5", + "ip": "^2.0.0", "smart-buffer": "^4.2.0" }, "engines": { - "node": ">= 10.0.0", + "node": ">= 10.13.0", "npm": ">= 3.0.0" } }, @@ -11533,9 +9220,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", "dev": true, "engines": { "node": ">=0.10.0" @@ -11572,9 +9259,9 @@ } }, "node_modules/spdx-exceptions": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", "dev": true }, "node_modules/spdx-expression-parse": { @@ -11588,15 +9275,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.18", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", - "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==", - "dev": true - }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", + "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", "dev": true }, "node_modules/ssri": { @@ -11657,6 +9338,15 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-hash": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", @@ -11677,25 +9367,10 @@ "node": ">=8" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/stringify-entities": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", - "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.3.tgz", + "integrity": "sha512-BP9nNHMhhfcMbiuQKCqMjhDP5yBCAxsPu4pHFFzJ6Alo9dZgY4VLDPutXqIjpRiMoKdp7Av85Gr73Q5uH9k7+g==", "dev": true, "dependencies": { "character-entities-html4": "^2.0.0", @@ -11718,19 +9393,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -11823,9 +9485,9 @@ } }, "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", + "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", "dev": true, "dependencies": { "chownr": "^2.0.0", @@ -11855,9 +9517,9 @@ "dev": true }, "node_modules/terser": { - "version": "5.31.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.1.tgz", - "integrity": "sha512-37upzU1+viGvuFtBo9NPufCb9dwM0+l9hMxYyWfBA+fbwrPqNJAhbZ6W47bBFnZHKHTUBnMvi87434qq+qnxOg==", + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.21.0.tgz", + "integrity": "sha512-WtnFKrxu9kaoXuiZFSGrcAvvBqAdmKx0SFNmVNYdJamMu9yyN3I/QF0FbH4QcqJQ+y1CJnzxGIKH0cSj+FGYRw==", "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -11878,13 +9540,6 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "peer": true - }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -11926,9 +9581,9 @@ } }, "node_modules/trough": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", - "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz", + "integrity": "sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==", "dev": true, "funding": { "type": "github", @@ -11941,37 +9596,12 @@ "integrity": "sha512-0z3j8R7MCjy10kc/g+qg7Ln3alJTodw9aDuVWZa3uiWqfuBMKeAeP2ocWcxoyM3D73yz3Jt/Pu4qPr4wHSdB/Q==", "dev": true }, - "node_modules/ts-api-utils": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", - "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", - "dev": true, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, "node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "peer": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/type-fest": { "version": "0.18.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", @@ -11985,15 +9615,15 @@ } }, "node_modules/typedoc": { - "version": "0.25.13", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.13.tgz", - "integrity": "sha512-pQqiwiJ+Z4pigfOnnysObszLiU3mVLWAExSPf+Mu06G/qsc3wzbuM56SZQvONhHLncLUhYzOVkjFFpFfL5AzhQ==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.2.tgz", + "integrity": "sha512-286F7BeATBiWe/qC4PCOCKlSTwfnsLbC/4cZ68oGBbvAqb9vV33quEOXx7q176OXotD+JdEerdQ1OZGJ818lnA==", "dev": true, "dependencies": { "lunr": "^2.3.9", "marked": "^4.3.0", "minimatch": "^9.0.3", - "shiki": "^0.14.7" + "shiki": "^0.14.1" }, "bin": { "typedoc": "bin/typedoc" @@ -12002,22 +9632,37 @@ "node": ">= 16" }, "peerDependencies": { - "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x" + "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x" } }, "node_modules/typedoc-plugin-missing-exports": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/typedoc-plugin-missing-exports/-/typedoc-plugin-missing-exports-2.3.0.tgz", - "integrity": "sha512-iI9ITNNLlbsLCBBeYDyu0Qqp3GN/9AGyWNKg8bctRXuZEPT7G1L+0+MNWG9MsHcf/BFmNbXL0nQ8mC/tXRicog==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/typedoc-plugin-missing-exports/-/typedoc-plugin-missing-exports-2.1.0.tgz", + "integrity": "sha512-+1DhqZCEu7Vu5APnrqpPwl31D+hXpt1fV0Le9ycCRL1eLVdatdl6KVt4SEVwPxnEpKwgOn2dNX6I9+0F1aO2aA==", "dev": true, "peerDependencies": { "typedoc": "0.24.x || 0.25.x" } }, + "node_modules/typedoc/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -12028,9 +9673,10 @@ } }, "node_modules/uc.micro": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==" + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "dev": true }, "node_modules/unc-path-regex": { "version": "0.1.2", @@ -12194,18 +9840,18 @@ } }, "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", "dev": true, "engines": { "node": ">= 10.0.0" } }, "node_modules/update-browserslist-db": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", - "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", "dev": true, "funding": [ { @@ -12222,8 +9868,8 @@ } ], "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.1.1", + "picocolors": "^1.0.0" }, "bin": { "update-browserslist-db": "cli.js" @@ -12232,16 +9878,6 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "peer": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -12431,9 +10067,9 @@ "dev": true }, "node_modules/vue-template-compiler": { - "version": "2.7.16", - "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", - "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", + "version": "2.7.14", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz", + "integrity": "sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==", "dev": true, "optional": true, "dependencies": { @@ -12466,14 +10102,13 @@ "node": ">= 8" } }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" } }, "node_modules/wrap-ansi": { @@ -12493,23 +10128,11 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true }, "node_modules/xmlcreate": { "version": "2.0.4", @@ -12578,13 +10201,12 @@ } }, "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", "dev": true, - "peer": true, "engines": { - "node": ">=10" + "node": ">=12.20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" diff --git a/package.json b/package.json index 1f72d6f..42bf2fa 100644 --- a/package.json +++ b/package.json @@ -3,14 +3,12 @@ "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-replace": "^5.0.2", "@rollup/plugin-terser": "^0.4.0", - "@typescript-eslint/eslint-plugin": "^6.9.1", "documentation": "^14.0.1", "install": "^0.13.0", "jsdoc": "^4.0.2", - "markdownlint": "^0.37.4", "node-sass": "^9.0.0", "npm": "^8.11.0", - "rollup": "^4.1.5", + "rollup": "^3.17.2", "rollup-plugin-peer-deps-external": "^2.2.3", "rollup-plugin-postcss": "^4.0.2", "rollup-plugin-typescript2": "^0.36.0", @@ -22,10 +20,6 @@ "typedoc-plugin-missing-exports": "^2.1.0", "typescript": "^5.2.2" }, - "overrides": { - "node-gyp": "^10.1.0", - "glob": "^9.0.0" - }, "eslintConfig": { "root": true, "extends": "eslint:recommended", @@ -35,8 +29,5 @@ }, "eslintIgnore": [ "node_modules/**" - ], - "dependencies": { - "markdownlint-cli": "^0.44.0" - } + ] } diff --git a/ruleset.xml b/ruleset.xml index 365c409..b362eb8 100644 --- a/ruleset.xml +++ b/ruleset.xml @@ -1,9 +1,9 @@ + xmlns="http://pmd.sourceforge.net/ruleset/2.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.net/ruleset_2_0_0.xsd"> JGrapes rules @@ -27,16 +27,14 @@ JGrapes rules - - + - @@ -47,11 +45,6 @@ JGrapes rules value="Avoid variables with short names like id"/> - - - - - @@ -97,6 +90,11 @@ JGrapes rules + + + + + diff --git a/settings.gradle b/settings.gradle index 4a3bfc8..cb613b6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -12,9 +12,7 @@ rootProject.name = 'VM-Operator' include 'org.jdrupes.vmoperator.manager' include 'org.jdrupes.vmoperator.manager.events' -include 'org.jdrupes.vmoperator.vmaccess' -include 'org.jdrupes.vmoperator.vmmgmt' +include 'org.jdrupes.vmoperator.vmconlet' include 'org.jdrupes.vmoperator.runner.qemu' include 'org.jdrupes.vmoperator.common' include 'org.jdrupes.vmoperator.util' -include 'spice-squid' diff --git a/spice-squid/.checkstyle b/spice-squid/.checkstyle deleted file mode 100644 index 7f2c604..0000000 --- a/spice-squid/.checkstyle +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/spice-squid/.settings/net.sf.jautodoc.prefs b/spice-squid/.settings/net.sf.jautodoc.prefs deleted file mode 100644 index 03e8200..0000000 --- a/spice-squid/.settings/net.sf.jautodoc.prefs +++ /dev/null @@ -1,8 +0,0 @@ -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 -replacements=\n\n\nReturns the\nSets the\nAdds the\nEdits the\nRemoves the\nInits the\nParses the\nCreates the\nBuilds the\nChecks if is\nPrints the\nChecks for\n\n\n -visibility_package=false -visibility_private=false -visibility_protected=false diff --git a/spice-squid/.settings/org.eclipse.buildship.core.prefs b/spice-squid/.settings/org.eclipse.buildship.core.prefs deleted file mode 100644 index 258eb47..0000000 --- a/spice-squid/.settings/org.eclipse.buildship.core.prefs +++ /dev/null @@ -1,13 +0,0 @@ -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/spice-squid/.settings/org.eclipse.core.resources.prefs b/spice-squid/.settings/org.eclipse.core.resources.prefs deleted file mode 100644 index 99f26c0..0000000 --- a/spice-squid/.settings/org.eclipse.core.resources.prefs +++ /dev/null @@ -1,2 +0,0 @@ -eclipse.preferences.version=1 -encoding/=UTF-8 diff --git a/spice-squid/.settings/org.eclipse.core.runtime.prefs b/spice-squid/.settings/org.eclipse.core.runtime.prefs deleted file mode 100644 index 5a0ad22..0000000 --- a/spice-squid/.settings/org.eclipse.core.runtime.prefs +++ /dev/null @@ -1,2 +0,0 @@ -eclipse.preferences.version=1 -line.separator=\n diff --git a/spice-squid/Containerfile b/spice-squid/Containerfile deleted file mode 100644 index 5c94829..0000000 --- a/spice-squid/Containerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM docker.io/alpine:3.19 - -RUN apk update &&\ - apk add --no-cache inotify-tools &&\ - apk add --no-cache squid - -COPY run.sh /usr/local/bin/run-squid.sh - -CMD ["/usr/local/bin/run-squid.sh"] - -EXPOSE 3128 diff --git a/spice-squid/build.gradle b/spice-squid/build.gradle deleted file mode 100644 index 5278098..0000000 --- a/spice-squid/build.gradle +++ /dev/null @@ -1,68 +0,0 @@ -plugins { - id 'org.jdrupes.vmoperator.java-application-conventions' -} - -dependencies { -} - -project.ext.gitBranch = grgit.branch.current.name.replace('/', '-') -def registry = "${project.rootProject.properties['docker.registry']}" -def rootVersion = rootProject.version - -task buildImage(type: Exec) { - inputs.files 'Containerfile' - - commandLine 'podman', 'build', '--pull', - '-t', "${project.name}:${project.gitBranch}",\ - '-f', 'Containerfile', '.' -} - -task pushImage(type: Exec) { - dependsOn buildImage - - commandLine 'podman', 'push', '--tls-verify=false', \ - "${project.name}:${project.gitBranch}", \ - "${registry}/${project.name}:${project.gitBranch}" -} -task tagWithVersion(type: Exec) { - dependsOn pushImage - - enabled = !rootVersion.contains("SNAPSHOT") - - commandLine 'podman', 'push', \ - "${project.name}:${project.gitBranch}",\ - "${registry}/${project.name}:${project.version}" -} - -task tagAsLatest(type: Exec) { - dependsOn tagWithVersion - - enabled = !rootVersion.contains("SNAPSHOT") - && !rootVersion.contains("alpha") \ - && !rootVersion.contains("beta") \ - || project.rootProject.properties['docker.testRegistry'] \ - && project.rootProject.properties['docker.registry'] \ - == project.rootProject.properties['docker.testRegistry'] - - commandLine 'podman', 'push', \ - "${project.name}:${project.gitBranch}",\ - "${registry}/${project.name}:latest" -} - -task publishImage { - dependsOn pushImage - dependsOn tagWithVersion - dependsOn tagAsLatest -} -test { - enabled = project.hasProperty("k8s.testCluster") - - useJUnitPlatform() - - testLogging { - showStandardStreams = true - } - - systemProperty "k8s.testCluster", project.hasProperty("k8s.testCluster") - ? project.getProperty("k8s.testCluster") : null -} diff --git a/spice-squid/run.sh b/spice-squid/run.sh deleted file mode 100755 index eddea39..0000000 --- a/spice-squid/run.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh - -CONF_OPT="-f /run/etc/squid/squid.conf" -/usr/sbin/squid $CONF_OPT - -inotifywait -m -e create -r /run/etc/squid | - while read file_path file_event file_name; do - if [ "$file_event" != "CREATE" ]; then - continue - fi - if [ -r /run/squid/squid.pid ]; then - echo "Reconfiguring squid" - /usr/sbin/squid $CONF_OPT -k reconfigure - else - echo "Restarting squid" - /usr/sbin/squid $CONF_OPT - fi - echo "Processed event" - done diff --git a/spice-squid/squid.conf b/spice-squid/squid.conf deleted file mode 100644 index 724b0df..0000000 --- a/spice-squid/squid.conf +++ /dev/null @@ -1,4 +0,0 @@ -http_access deny all - -# Squid normally listens to port 3128 -http_port 3128 diff --git a/webpages/.gitignore b/webpages/.gitignore deleted file mode 100644 index 7615a9d..0000000 --- a/webpages/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -_site -Gemfile.lock -.bundle -.jekyll-cache \ No newline at end of file diff --git a/webpages/.readthedocs.yaml b/webpages/.readthedocs.yaml deleted file mode 100644 index 546c09f..0000000 --- a/webpages/.readthedocs.yaml +++ /dev/null @@ -1,18 +0,0 @@ -# Read the Docs configuration file -# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details - -# Required -version: 2 -# Set the OS, Python version, and other tools you might need -build: - os: ubuntu-24.04 - tools: - ruby: "3.3" - commands: - # Install dependencies - - cd webpages && gem install bundle - - cd webpages && bundle install - # Build the site and save generated files into Read the Docs directory - - cd webpages && jekyll build --destination $READTHEDOCS_OUTPUT/html - - cp webpages/robots-readthedocs.txt $READTHEDOCS_OUTPUT/html/robots.txt - \ No newline at end of file diff --git a/webpages/02_2_operator.png b/webpages/02_2_operator.png deleted file mode 100644 index d3909d4..0000000 Binary files a/webpages/02_2_operator.png and /dev/null differ diff --git a/webpages/BingSiteAuth.xml b/webpages/BingSiteAuth.xml deleted file mode 100644 index b0cf39a..0000000 --- a/webpages/BingSiteAuth.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - 0309051EFD625D32489366E1BE8189EF - \ No newline at end of file diff --git a/webpages/ConfigAccess-preview.png b/webpages/ConfigAccess-preview.png deleted file mode 100644 index e7523f9..0000000 Binary files a/webpages/ConfigAccess-preview.png and /dev/null differ diff --git a/webpages/Gemfile b/webpages/Gemfile deleted file mode 100644 index ecbbb7d..0000000 --- a/webpages/Gemfile +++ /dev/null @@ -1,5 +0,0 @@ -source 'https://rubygems.org' -# gem 'github-pages', group: :jekyll_plugins -gem "jekyll", "~> 4.0" -gem "jekyll-seo-tag" -gem 'webrick', '~> 1.3', '>= 1.3.1' diff --git a/webpages/PoolAccess-preview.png b/webpages/PoolAccess-preview.png deleted file mode 100644 index 34db8cd..0000000 Binary files a/webpages/PoolAccess-preview.png and /dev/null differ diff --git a/webpages/VM-Operator-GUI-preview.png b/webpages/VM-Operator-GUI-preview.png deleted file mode 100644 index b5293d7..0000000 Binary files a/webpages/VM-Operator-GUI-preview.png and /dev/null differ diff --git a/webpages/VM-Operator-GUI-view.png b/webpages/VM-Operator-GUI-view.png deleted file mode 100644 index dbda800..0000000 Binary files a/webpages/VM-Operator-GUI-view.png and /dev/null differ diff --git a/webpages/VM-Operator-with-font.svg b/webpages/VM-Operator-with-font.svg deleted file mode 100644 index 6240969..0000000 --- a/webpages/VM-Operator-with-font.svg +++ /dev/null @@ -1,173 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - - - - - VM - - - - - - - - - - - - - - - - - diff --git a/webpages/VM-Operator.svg b/webpages/VM-Operator.svg deleted file mode 100644 index 30c1ed2..0000000 --- a/webpages/VM-Operator.svg +++ /dev/null @@ -1,184 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/webpages/VmAccess-preview.png b/webpages/VmAccess-preview.png deleted file mode 100644 index a97f7e1..0000000 Binary files a/webpages/VmAccess-preview.png and /dev/null differ diff --git a/webpages/_config.yml b/webpages/_config.yml deleted file mode 100644 index a2162f7..0000000 --- a/webpages/_config.yml +++ /dev/null @@ -1,14 +0,0 @@ -plugins: - - jekyll-seo-tag - -url: "https://vm-operator.jdrupes.org" - -author: Michael N. Lipp - -logo: VM-Operator.svg - -tagline: VM-Operator by mnlipp - -description: >- - A Kubernetes operator for running virtual machines (notably Qemu VMs) - as pods. diff --git a/webpages/_includes/matomo.html b/webpages/_includes/matomo.html deleted file mode 100644 index adb7c30..0000000 --- a/webpages/_includes/matomo.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - diff --git a/webpages/_includes/toc.html b/webpages/_includes/toc.html deleted file mode 100644 index 56ac8e4..0000000 --- a/webpages/_includes/toc.html +++ /dev/null @@ -1,96 +0,0 @@ -{% capture tocWorkspace %} - {% comment %} - Version 1.0.10 - https://github.com/allejo/jekyll-toc - - "...like all things liquid - where there's a will, and ~36 hours to spare, there's usually a/some way" ~jaybe - - Usage: - {% include toc.html html=content sanitize=true class="inline_toc" id="my_toc" h_min=2 h_max=3 %} - - Parameters: - * html (string) - the HTML of compiled markdown generated by kramdown in Jekyll - - Optional Parameters: - * sanitize (bool) : false - when set to true, the headers will be stripped of any HTML in the TOC - * class (string) : '' - a CSS class assigned to the TOC - * id (string) : '' - an ID to assigned to the TOC - * h_min (int) : 1 - the minimum TOC header level to use; any header lower than this value will be ignored - * h_max (int) : 6 - the maximum TOC header level to use; any header greater than this value will be ignored - * ordered (bool) : false - when set to true, an ordered list will be outputted instead of an unordered list - * item_class (string) : '' - add custom class(es) for each list item; has support for '%level%' placeholder, which is the current heading level - * baseurl (string) : '' - add a base url to the TOC links for when your TOC is on another page than the actual content - * anchor_class (string) : '' - add custom class(es) for each anchor element - - Output: - An ordered or unordered list representing the table of contents of a markdown block. This snippet will only - generate the table of contents and will NOT output the markdown given to it - {% endcomment %} - - {% capture my_toc %}{% endcapture %} - {% assign orderedList = include.ordered | default: false %} - {% assign minHeader = include.h_min | default: 1 %} - {% assign maxHeader = include.h_max | default: 6 %} - {% assign nodes = include.html | split: ' maxHeader %} - {% continue %} - {% endif %} - - {% if firstHeader %} - {% assign firstHeader = false %} - {% assign minHeader = headerLevel %} - {% endif %} - - {% assign indentAmount = headerLevel | minus: minHeader %} - {% assign _workspace = node | split: '' | first }}>{% endcapture %} - {% assign header = _workspace[0] | replace: _hAttrToStrip, '' %} - - {% assign space = '' %} - {% for i in (1..indentAmount) %} - {% assign space = space | prepend: ' ' %} - {% endfor %} - - {% if include.item_class and include.item_class != blank %} - {% capture listItemClass %}{:.{{ include.item_class | replace: '%level%', headerLevel }}}{% endcapture %} - {% endif %} - - {% capture heading_body %}{% if include.sanitize %}{{ header | strip_html }}{% else %}{{ header }}{% endif %}{% endcapture %} - {% capture my_toc %}{{ my_toc }} -{{ space }}{{ listModifier }} {{ listItemClass }} [{{ heading_body | replace: "|", "\|" }}]({% if include.baseurl %}{{ include.baseurl }}{% endif %}#{{ html_id }}){% if include.anchor_class %}{:.{{ include.anchor_class }}}{% endif %}{% endcapture %} - {% endfor %} - - {% if include.class and include.class != blank %} - {% capture my_toc %}{:.{{ include.class }}} -{{ my_toc | lstrip }}{% endcapture %} - {% endif %} - - {% if include.id %} - {% capture my_toc %}{: #{{ include.id }}} -{{ my_toc | lstrip }}{% endcapture %} - {% endif %} -{% endcapture %}{% assign tocWorkspace = '' %}{{ my_toc | markdownify | strip }} diff --git a/webpages/_includes/umami.html b/webpages/_includes/umami.html deleted file mode 100644 index 8066278..0000000 --- a/webpages/_includes/umami.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/webpages/_layouts/vm-operator.html b/webpages/_layouts/vm-operator.html deleted file mode 100644 index 40bdff7..0000000 --- a/webpages/_layouts/vm-operator.html +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - - - - - - {% include umami.html %} - - - - - {% seo %} - - -
- -
-
- -
- VM-Operator Logo -
-
-
-

- -

View GitHub Project

- -

- -

Overview

-

The Runner

-

The Manager

- -

Web interface

- -

Advanced

- -

Hints

-

Upgrading

-

Javadoc

- -
-
- - {% if page.tocTitle %} -

{{ page.tocTitle }}

- {% include toc.html html=content %} - {% endif %} - - {{ content }} -
-
- - - - -
-
- - {% include matomo.html %} - - - diff --git a/webpages/admin-gui.md b/webpages/admin-gui.md deleted file mode 100644 index 325c227..0000000 --- a/webpages/admin-gui.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -title: "VM-Operator: Administrator View — Provides an overview of running VMs" -description: >- - Information about the administrator view of the VM-Operator, which provides - an overview of the defined VMs, their state and resource consumptions and - actions for starting, stopping and accessing the VMs. -layout: vm-operator ---- - -# Administrator view - -An overview display shows the current CPU and RAM usage and a graph -with recent changes. - -![VM-Operator admin GUI preview](VM-Operator-GUI-preview.png) - -The detail display lists all VMs. From here you can start and stop -the VMs and adjust the CPU and RAM usages (modifies the definition -in kubernetes). - -![VM-Operator admin GUI view](VM-Operator-GUI-view.png) diff --git a/webpages/auto-login.md b/webpages/auto-login.md deleted file mode 100644 index 66f0edf..0000000 --- a/webpages/auto-login.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -title: "VM-Operator: Auto login — Login users automatically on the guest" -layout: vm-operator ---- - -# Auto Login - -*Since 4.0.0* - -When users log into the web GUI, they have already authenticated with the -VM-Operator. In some environments, requiring an additional login on the -guest OS can be annoying. To enhance the user experience, the VM-Operator -supports automatic login on the guest operating system, thus eliminating -the need for multiple logins. However, this feature requires specific -support from the guest OS. - -## Prepare the VM - -Automatic login requires an agent running inside the guest OS. Similar -to QEMU's standard guest agent, the VM-Operator agent communicates with -the host via a tty device (provided in the guest as -`/dev/virtio-ports/org.jdrupes.vmop_agent.0`). On modern Linux systems, `udev` can -detect this device and trigger the start of an associated systemd service. - -Sample configuration files for a VM-Operator agent are available -[here](https://github.com/mnlipp/VM-Operator/tree/main/dev-example/vmop-agent). -Copy - - * `99-vmop-agent.rules` → `/usr/local/lib/udev/rules.d/99-vmop-agent.rules`, - * `vmop-agent` → `/usr/local/libexec/vmop-agent` and - * `vmop-agent.service` → `/usr/local/lib/systemd/system/vmop-agent.service`. - -Some of these target directories may not exist by default and must be -created manually. If your system uses SELinux, run `restorecon` to apply -the correct security contexts. - -Enable the agent: - -```console -# systemctl daemon-reload -# systemctl enable vmop-agent -# udevadm control --reload-rules -# udevadm trigger - ``` - -## The VM operator agent - -Communication with the VM-Operator agent follows the pattern established by -protocols such as SMTP and FTP. The agent must handle the commands -"`login `" and "`logout`" on its input. In response to -these commands, the agent sends back lines that start with a three -digit number. The first digit determines the type of message: "1" for -informational, "2" for success and "4" or "5" for errors. The second -digit provides information about the category that a response relates -to. The third digit is specific to the command. - -While this describes the general pattern, the [runner](runner.html) -only evaluates the following codes: - -| Code | Meaning | -| ---- | ------- | -| 220 | Sent by the agent on startup | -| 201 | Login command executed successfully | -| 202 | Logout command executed successfully | - -The provided sample script is written for the gnome desktop environment. -It assumes that GDM is running as a service by default. When the agent -receives a login command, it stops GDM and starts a gnome-session for -the specified user. Upon receiving the logout command, it terminates -the session and starts GDM again. - -No attempt has been made to make the script configurable. There are too -many possible options. The script should therefore be considered as a -starting point that you may need to adapt to your specific needs. - -In addition to starting the desktop for the logged in user, the sample -script automatically creates user accounts if they do not already exist. -The idea behind this behavior is further explained in the -[section about pools](pools.html#vm-pools). - -## Enable auto login for a VM - -To enable auto login for a VM, specify the user to be logged in in the VM's -definition with "`spec.vm.display.loggedInUser: user-name`". If everything has been -set up correctly, you should be able to open the console and observe the -transition from GDM's login screen to the user's desktop when updating the -VM's spec. diff --git a/webpages/controller.md b/webpages/controller.md deleted file mode 100644 index c91e2d4..0000000 --- a/webpages/controller.md +++ /dev/null @@ -1,238 +0,0 @@ ---- -title: "VM-Operator: Controller — Reconciles the VM CRs" -description: >- - Information about the VM Operator's controller component its - configuration options and the CRD used to define VMs. -layout: vm-operator ---- - -# The Controller - -The controller component (which is part of the manager) monitors -custom resources of kind `VirtualMachine`. It creates or modifies -other resources in the cluster as required to get the VM defined -by the CR up and running. - -Here is the sample definition of a VM from the -["local-path" example](https://github.com/mnlipp/VM-Operator/tree/main/example/local-path): - -```yaml -apiVersion: "vmoperator.jdrupes.org/v1" -kind: VirtualMachine -metadata: - namespace: vmop-demo - name: test-vm -spec: - guestShutdownStops: false - - vm: - state: Running - maximumCpus: 4 - currentCpus: 2 - maximumRam: 8Gi - currentRam: 4Gi - - networks: - - user: {} - - disks: - - volumeClaimTemplate: - metadata: - name: system - spec: - storageClassName: "" - selector: - matchLabels: - app.kubernetes.io/name: vmrunner - app.kubernetes.io/instance: test-vm - vmrunner.jdrupes.org/disk: system - resources: - requests: - storage: 40Gi - - cdrom: - image: "" - # image: https://download.fedoraproject.org/pub/fedora/linux/releases/38/Workstation/x86_64/iso/Fedora-Workstation-Live-x86_64-38-1.6.iso - # image: "Fedora-Workstation-Live-x86_64-38-1.6.iso" - - display: - spice: - port: 5910 - # Since 3.0.0: - # generateSecret: false -``` - -## Pod management - -The central resource created by the controller is a -[`Pod`](https://kubernetes.io/docs/concepts/workloads/pods/) -with the same name as the VM (`metadata.name`). The pod is created only -if `spec.vm.state` is "Running" (default is "Stopped" which deletes the -pod)[^oldSts]. - -Property `spec.guestShutdownStops` (since 2.2.0) controls the effect of a -shutdown initiated by the guest. If set to `false` (default) the pod -and thus the VM is automatically restarted. If set to `true`, the -VM's state is set to "Stopped" when the VM terminates and the pod is -deleted. - -[^oldSts]: Before version 3.4, the operator created a - [stateful set](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/) - that in turn created the pod and the PVCs (see below). - -## Defining the basics - -How to define the number of CPUs and the size of the RAM of the VM -should be obvious from the example. Note that changes of the current -number of CPUs and the current RAM size will be propagated to -running VMs. - -## Defining disks - -Maybe the most interesting part is the definition of the VM's disks. -This is done by adding one or more `volumeClaimTemplate`s to the -list of disks. As its name suggests, such a template is used by the -controller to generate a -[`PVC`](https://kubernetes.io/docs/concepts/storage/persistent-volumes/). - -The example template does not define any storage. Rather it references -some PV that you must have created first. This may be your first approach -if you have existing storage from running the VM outside Kubernetes -(e.g. with libvirtd). - -If you have ceph or some other full fledged storage provider installed -and create a new VM, provisioning a disk can happen automatically -as shown in this example: - -```yaml - disks: - - volumeClaimTemplate: - metadata: - name: system - spec: - storageClassName: rook-ceph-block - resources: - requests: - storage: 40Gi -``` - -The disk will be available as "/dev/*name*-disk" in the VM, -using the string from `.volumeClaimTemplate.metadata.name` as *name*. -If no name is defined in the metadata, then "/dev/disk-*n*" -is used instead, with *n* being the index of the volume claim -template in the list of disks. - -The name of the generated PVC is the VM's name with "-*name*-disk" -(or the generated name) appended: "*vmName*-*name*-disk" -(or "*vmName*-disk-*n*"). The definition of the PVC is simply a copy -of the information from the `volumeClaimTemplate` (with some additional -labels, see below)[^oldStsDisks]. - -[^oldStsDisks]: Before version 3.4 the `volumeClaimTemplate`s were - copied in the definition of the stateful set. As a stateful set - appends the started pod's name to the name of the volume claim - templates when it creates the PVCs, the PVCs' name were - "*name*-disk-*vmName*-0" (or "disk-*n*-*vmName*-0"). - -PVCs are never removed automatically. Usually, you do not want your -VMs disks to be removed when you (maybe accidentally) remove the CR -for the VM. To simplify the lookup for an eventual (manual) removal, -all PVCs are labeled with "app.kubernetes.io/name: vm-runner", -"app.kubernetes.io/instance: *vmName*", and -"app.kubernetes.io/managed-by: vm-operator", making it easy to select -the PVCs by label in a delete command. - -## Choosing an image for the runner - -The image used for the runner can be configured with -[`spec.image`](https://github.com/mnlipp/VM-Operator/blob/7e094e720b7b59a5e50f4a9a4ad29a6000ec76e6/deploy/crds/vms-crd.yaml#L19). -This is a mapping with either a single key `source` or a detailed -configuration using the keys `repository`, `path` etc. - -Currently two runner images are maintained. One that is based on -Arch Linux (`ghcr.io/mnlipp/org.jdrupes.vmoperator.runner.qemu-arch`) and a -second one based on Alpine (`ghcr.io/mnlipp/org.jdrupes.vmoperator.runner.qemu-alpine`). - -Starting with release 1.0, all versions of runner images and managers -that have the same major release number are guaranteed to be compatible. - -## Generating cloud-init data - -*Since: 2.2.0* - -The optional object `.spec.cloudInit` with sub-objects `.cloudInit.metaData`, -`.cloudInit.userData` and `.cloudInit.networkConfig` can be used to provide -data for -[cloud-init](https://cloudinit.readthedocs.io/en/latest/index.html). -The data from the CRD will be made available to the VM by the runner -as a vfat formatted disk (see the description of -[NoCloud](https://cloudinit.readthedocs.io/en/latest/reference/datasources/nocloud.html)). - -If `.metaData.instance-id` is not defined, the controller automatically -generates it from the CRD's `resourceVersion`. If `.metaData.local-hostname` -is not defined, the controller adds this property using the value from -`metadata.name`. - -Note that there are no schema definitions available for `.userData` -and `.networkConfig`. Whatever is defined in the CRD is copied to -the corresponding cloud-init file without any checks. (The introductory -comment `#cloud-config` required at the beginning of `.userData` is -generated automatically by the runner.) - -## Display secret/password - -*Since: 2.3.0* - -You can define a display password using a Kubernetes secret. -When you start a VM, the controller checks if there is a secret -with labels "app.kubernetes.io/name: vm-runner, -app.kubernetes.io/component: display-secret, -app.kubernetes.io/instance: *vmname*" in the namespace of the -VM definition. The name of the secret can be chosen freely. - -```yaml -kind: Secret -apiVersion: v1 -metadata: - name: test-vm-display-secret - namespace: vmop-demo - labels: - app.kubernetes.io/name: vm-runner - app.kubernetes.io/instance: test-vm - app.kubernetes.io/component: display-secret -type: Opaque -data: - display-password: dGVzdC12bQ== - # Since 3.0.0: - # password-expiry: bmV2ZXI= -``` - -If such a secret for the VM is found, the VM is configured to use -the display password specified. The display password in the secret -can be updated while the VM runs[^delay]. Activating/deactivating -the display password while a VM runs is not supported by QEMU and -therefore requires stopping the VM, adding/removing the secret and -restarting the VM. - -[^delay]: Be aware of the possible delay, see e.g. - [here](https://web.archive.org/web/20240223073838/https://ahmet.im/blog/kubernetes-secret-volumes-delay/). - -*Since: 3.0.0* - -The secret's `data` can have an additional property `data.password-expiry` which -specifies a (base64 encoded) expiry date for the password. Supported -values are those defined by QEMU (`+n` seconds from now, `n` Unix -timestamp, `never` and `now`). - -Unless `spec.vm.display.spice.generateSecret` is set to `false` in the VM -definition (CRD), the controller creates a secret for the display -password automatically if none is found. The secret is created -with a random password that expires immediately, which makes the -display effectively inaccessible until the secret is modified. -Note that a password set manually may be overwritten by components -of the manager unless the password-expiry is set to "never" or -some time in the future. - -## Further reading - -For a detailed description of the available configuration options see the -[CRD](https://github.com/mnlipp/VM-Operator/blob/main/deploy/crds/vms-crd.yaml). diff --git a/webpages/favicon.svg b/webpages/favicon.svg deleted file mode 100644 index c8616d5..0000000 --- a/webpages/favicon.svg +++ /dev/null @@ -1,184 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/webpages/hints.md b/webpages/hints.md deleted file mode 100644 index 1f896a4..0000000 --- a/webpages/hints.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: "VM-Operator: Hints — Miscellaneous hints for using VM-Operator" -layout: vm-operator ---- - -# Hints - -## Disable suspend and hibernate - -Suspend and hibernate are poorly supported in VMs and usually do not -work as expected. To disable these on systemd based systems, use the -following command: - -```console -# systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target -``` diff --git a/webpages/index-pic.svg b/webpages/index-pic.svg deleted file mode 100644 index d6b0ef9..0000000 --- a/webpages/index-pic.svg +++ /dev/null @@ -1,7329 +0,0 @@ - - - - diff --git a/webpages/index.md b/webpages/index.md deleted file mode 100644 index 6dc3c10..0000000 --- a/webpages/index.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: "VM-Operator: Easy to use kubernetes operator for QEM/KVM VMs" -description: >- - A solution for running VMs on Kubernetes with a web interface for - admins and users. Focuses on running QEMU/KVM virtual machines and - using SPICE as display protocol. -layout: vm-operator ---- - -# Welcome to VM-Operator - -![VM-Operator summary picture](index-pic.svg) - -This project provides an easy to use and flexible solution for -running QEMU/KVM based virtual machines (VMs) in Kubernetes pods. - -The image used for the VM pods combines QEMU and a control program -for starting and managing the QEMU process. This application is called -"[the runner](runner.html)". - -While you can deploy a runner manually (or with the help of some -helm templates), the preferred way is to deploy "[the manager](manager.html)" -application which acts as a Kubernetes operator for runners -and thus the VMs. - -If you just want to try out things, you can skip the remainder of this -page and proceed to "[the manager](manager.html)". - -## Motivation - -The project was triggered by a remark in the discussion about RedHat -[dropping SPICE support](https://bugzilla.redhat.com/show_bug.cgi?id=2030592) -from the RHEL packages. Which means that you have to run QEMU in a -container on RHEL and derivatives if you want to continue using Spice. -So KubeVirt comes to mind. But -[one comment](https://bugzilla.redhat.com/show_bug.cgi?id=2030592#c4) -mentioned that the [KubeVirt](https://kubevirt.io/) project isn't -interested in supporting SPICE either. - -Time to have a look at alternatives. Libvirt has become a common -tool to configure and run QEMU. But some of its functionality, notably -the management of storage for the VMs and networking is already provided -by Kubernetes. Therefore this project takes a fresh approach of -running QEMU in a pod using a simple, lightweight manager called "runner". -Providing resources to the VM is left to Kubernetes mechanisms as -much as possible. - -## VMs and Pods - -VMs are not the typical workload managed by Kubernetes. You can neither -have replicas nor can the containers simply be restarted without a major -impact on the "application". So there are many features for managing -pods that we cannot make use of. QEMU in its container can only be -deployed as a pod or using a stateful set with replica 1, which is rather -close to simply deploying the pod (you get the restart and some PVC -management "for free"). - -A second look, however, reveals that Kubernetes has more to offer. - - * It has a well defined API for managing resources. - * It provides access to different kinds of managed storage for the VMs. - * Its managing features *are* useful for running the component that - manages the pods with the VMs. - -And if you use Kubernetes anyway, well then the VMs within Kubernetes -provide you with a unified view of all (or most of) your workloads, -which simplifies the maintenance of your platform. diff --git a/webpages/manager.md b/webpages/manager.md deleted file mode 100644 index 8748ad8..0000000 --- a/webpages/manager.md +++ /dev/null @@ -1,154 +0,0 @@ ---- -title: "VM-Operator: The Manager — Provides the controller and a Web UI" -description: >- - Information about the installation and configuration of the - VM Operator. -layout: vm-operator ---- - -# The Manager - -The Manager is the program that provides the controller from the -[operator pattern](https://github.com/cncf/tag-app-delivery/blob/eece8f7307f2970f46f100f51932db106db46968/operator-wg/whitepaper/Operator-WhitePaper_v1-0.md#operator-components-in-kubernetes) -together with a web user interface. It should be run in a container in the cluster. - -## Installation - -A manager instance manages the VMs in its own namespace. The only -common (and therefore cluster scoped) resource used by all instances -is the CRD. It is available -[here](https://github.com/mnlipp/VM-Operator/raw/main/deploy/crds/vms-crd.yaml) -and must be created first. - -```sh -kubectl apply -f https://github.com/mnlipp/VM-Operator/raw/main/deploy/crds/vms-crd.yaml -``` - -The example above uses the CRD from the main branch. This is okay if -you apply it once. If you want to preserve the link for automatic -upgrades, you should use a link that points to one of the release branches. - -The next step is to create a namespace for the manager and the VMs, e.g. -`vmop-demo`. - -```sh -kubectl create namespace vmop-demo -``` - -Finally you have to create an account, the role, the binding etc. The -default files for creating these resources using the default namespace -can be found in the -[deploy](https://github.com/mnlipp/VM-Operator/tree/main/deploy) -directory. I recommend to use -[kustomize](https://kubernetes.io/docs/tasks/manage-kubernetes-objects/kustomization/) -to create your own configuration. - -## Initial Configuration - -Use one of the `kustomize.yaml` files from the -[example](https://github.com/mnlipp/VM-Operator/tree/main/example) directory -as a starting point. The directory contains two examples. Here's the file -from subdirectory `local-path`: - -```yaml -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: -# Again, I recommend to use the deploy directory from a -# release branch for anything but test environments. -- https://github.com/mnlipp/VM-Operator/deploy - -namespace: vmop-demo - -patches: -- patch: |- - kind: PersistentVolumeClaim - apiVersion: v1 - metadata: - name: vmop-image-repository - spec: - # Default is ReadOnlyMany - accessModes: - - ReadWriteOnce - resources: - requests: - # Default is 100Gi - storage: 10Gi - # Default is to use the default storage class - storageClassName: local-path - -- patch: |- - kind: ConfigMap - apiVersion: v1 - metadata: - name: vm-operator - data: - config.yaml: | - "/Manager": - # "/GuiHttpServer": - # See section about the GUI - "/Controller": - "/Reconciler": - runnerDataPvc: - # Default is to use the default storage class - storageClassName: local-path -``` - -The sample file adds a namespace (`vmop-demo`) to all resource -definitions and patches the PVC `vmop-image-repository`. This is a volume -that is mounted into all pods that run a VM. The volume is intended -to be used as a common repository for CDROM images. The PVC must exist -and it must be bound before any pods can run. - -The second patch affects the small volume that is created for each -runner and contains the VM's configuration data such as the EFI vars. -The manager's default configuration causes the PVC for this volume -to be created with no storage class (which causes the default storage -class to be used). The patch provides a new configuration file for -the manager that makes the reconciler use local-path as storage -class for this PVC. Details about the manager configuration can be -found in the next section. - -Note that you need none of the patches if you are fine with using your -cluster's default storage class and this class supports ReadOnlyMany as -access mode. - -Check that the pod with the manager is running: - -```sh -kubectl -n vmop-demo get pods -l app.kubernetes.io/name=vm-operator -``` - -Proceed to the description of [the controller](controller.html) -for creating your first VM. - -## Configuration Details - -The [config map](https://github.com/mnlipp/VM-Operator/blob/main/deploy/vmop-config-map.yaml) -for the manager may provide a configuration file (`config.yaml`) and -a file with logging properties (`logging.properties`). Both files are mounted -into the container that runs the manager and are evaluated by the manager -on startup. If no files are provided, the manager uses built-in defaults. - -The configuration file for the Manager follows the conventions of -the [JGrapes](https://jgrapes.org/) component framework. -The keys that start with a slash select the component within the -application's component hierarchy. The mapping associated with the -selected component configures this component's properties. - -The available configuration options for the components can be found -in their respective JavaDocs (e.g. -[here](latest-release/javadoc/org/jdrupes/vmoperator/manager/Reconciler.html) -for the Reconciler). - -## Development Configuration - -The [dev-example](https://github.com/mnlipp/VM-Operator/tree/main/dev-example) -directory contains a `kustomize.yaml` that uses the development namespace -`vmop-dev` and creates a deployment for the manager with 0 replicas. - -This environment can be used for running the manager in the IDE. As the -namespace to manage cannot be detected from the environment, you must use - `-c ../dev-example/config.yaml` as argument when starting the manager. This -configures it to use the namespace `vmop-dev`. diff --git a/webpages/pools.md b/webpages/pools.md deleted file mode 100644 index c84264f..0000000 --- a/webpages/pools.md +++ /dev/null @@ -1,115 +0,0 @@ ---- -title: "VM-Operator: VM pools — assigning VMs to users dynamically" -layout: vm-operator ---- - -# VM Pools - -*Since 4.0.0* - -Not all VMs are defined as replacements for carefully maintained -individual PCs. In many workplaces, a standardardized VM configuration -can be used where all user-specific data is stored in each user's home -directory. By using a shared file system for home directories, users -can login on any VM and find themselves in their personal -environment. - -If only a subset of users require access simultaneously, this makes it -possible to define a pool of standardardized VMs and dynamically assign -them to users as needed, eliminating the need to define a dedicated VM -for each user. - -## Pool definitions - -The VM-operator supports this use case with a CRD for pools. - -```yaml -apiVersion: "vmoperator.jdrupes.org/v1" -kind: VmPool -metadata: - namespace: vmop-dev - name: test-vms -spec: - retention: "PT4h" - loginOnAssignment: true - permissions: - - user: admin - may: - - accessConsole - - start - - role: user - may: - - accessConsole - - start -``` - -The `retention` specifies how long the assignment of a VM from the pool to -a user remains valid after the user closes the console. This ensures that -a user can resume work within this timeframe without the risk of another -user taking over the VM. The time is specified as an -[ISO 8601 duration](https://en.wikipedia.org/wiki/ISO_8601#Durations). -Specifying an ISO 8601 time is also supported, but if you consider -using an absolute time, check again whether a dedicated VM for the user -isn't the more appropriate choice. - -Setting `loginOnAssignment` to `true` (defaults to `false`) triggers automatic -login of the user (as described in [section auto login](auto-login.html)) -when the VM is assigned. The `permissions` property specifies the actions -that users or roles can perform on assigned VMs. The `may` property defaults -to `[accessConsole]` if not specified. - -VMs become members of one (or more) pools by adding the pool name to -the `spec.pools` array in the VM definition, as shown below: - -```yaml -apiVersion: "vmoperator.jdrupes.org/v1" -kind: VirtualMachine - -spec: - pools: - - test-vms -``` - -## Accessing a VM from the pool - -Users can access a VM from a pool using the widget described in -[user view](user-gui.html). The widget must be configured to -provide access to a pool instead of to a specific VM. - -![VM Access configuration](ConfigAccess-preview.png){: width="500"} - -Assignment happens when the "Start" icon is clicked. If the assigned VM -is not already running, it will be started automatically. The assigned -VM's name apears in the widget above the action icons. - -![VM Access via pool](PoolAccess-preview.png) - -Apart from showing the assigned VM, the widget behaves in the same way -as when configured for accessing a specific VM. - -## Guest OS Requirements - -To ensure proper functionality when using VM pools, certain requirements -must be met on the guest OS. - -### Shared file system - -All VMs in the pool must mount a shared file system as the home directory. -When using the -[sample agent](https://github.com/mnlipp/VM-Operator/tree/main/dev-example/vmop-agent), -the file system must support POSIX file access control lists (ACLs). - -### User management - -All VMs in the pool must map a given user name to the same user -id. This is typically accomplished by using a central user management, -such as LDAP. The drawback of such a solution is that it is rather -complicated to configure. - -As an alternative, the sample auto login agent provides a very simple -approach that uses the shared home directory for managing the user ids. -Simplified, the script searches for a home directory with the given user -name and derives the user id from it. It then checks if the user id is -known by the guest operating system. If not, the user is added. - -Details can be found in the comments of the sample script. diff --git a/webpages/robots-readthedocs.txt b/webpages/robots-readthedocs.txt deleted file mode 100644 index 90e0f33..0000000 --- a/webpages/robots-readthedocs.txt +++ /dev/null @@ -1,3 +0,0 @@ -User-agent: * -Allow: / -Sitemap: https://kubernetes-vm-operator.readthedocs.io/sitemap.xml diff --git a/webpages/robots.txt b/webpages/robots.txt deleted file mode 100644 index e1ed7b0..0000000 --- a/webpages/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -User-agent: * -Allow: / -Sitemap: https://vm-operator.jdrupes.org/sitemap.xml diff --git a/webpages/runner.md b/webpages/runner.md deleted file mode 100644 index e677a7a..0000000 --- a/webpages/runner.md +++ /dev/null @@ -1,114 +0,0 @@ ---- -title: "VM-Operator: The Runner — Starts and monitors a VM" -description: >- - Description of the VM Operator's runner component which starts - QEMU and thus the VM, optionally together with a TPM, in a - kubenernetes pod and monitors everything. -layout: vm-operator ---- - -# The Runner - -For most use cases, QEMU needs to be started and controlled by another -program that manages the QEMU process. This program is called the -runner in this context. - -The most prominent reason for this second program is that it allows -a VM to be shutdown cleanly in response to a TERM signal. QEMU handles -the TERM signal by flushing all buffers and stopping, leaving the disks in -a [crash consistent state](https://gitlab.com/qemu-project/qemu/-/issues/148). -For a graceful shutdown, a parent process must handle the TERM signal, send -the `system_powerdown` command to the qemu process and wait for its completion. - -Another reason for having the runner is that another process needs to be started -before QEMU if the VM is supposed to include a TPM (software TPM). - -Finally, we want some kind of higher level interface for applying runtime -changes to the VM such as changing the CD or configuring the number of -CPUs and the memory. - -The runner takes care of all these issues. Although it is intended to -run in a container (which runs in a Kubernetes pod) it does not require -a container. You can start and use it as an ordinary program on any -system, provided that you have the required commands (qemu, swtpm) -installed. - -## Stand-alone Configuration - -Upon startup, the runner reads its main configuration file -which defaults to `/etc/opt/vmrunner/config.yaml` and may be changed -using the `-c` (or `--config`) command line option. - -A sample configuration file with annotated options can be found -[here](https://github.com/mnlipp/VM-Operator/blob/main/org.jdrupes.vmoperator.runner.qemu/config-sample.yaml). -As the runner implementation uses the -[JGrapes](https://jgrapes.org/) framework, the file -follows the framework's -[conventions](https://jgrapes.org/latest-release/javadoc/org/jgrapes/util/YamlConfigurationStore.html). -The top level "`/Runner`" selects the component to be configured. Nested -within is the information to be applied to the component. - -The main entries in the configuration file are the "template" and -the "vm" information. The runner processes the -[freemarker template](https://freemarker.apache.org/), using the -"vm" information to derive the qemu command. The idea is that -the "vm" section provides high level information such as the boot -mode, the number of CPUs, the RAM size and the disks. The template -defines a particular VM type, i.e. it contains the "nasty details" -that do not need to be modified for some given set of VM instances. - -The templates provided with the runner can be found -[here](https://github.com/mnlipp/VM-Operator/tree/main/org.jdrupes.vmoperator.runner.qemu/templates). -When details of the VM configuration need modification, a new VM type -(i.e. a new template) has to be defined. Authoring a new -template requires some knowledge about the -[qemu invocation](https://www.qemu.org/docs/master/system/invocation.html). -Despite many "warnings" that you find in the web, configuring the -invocation arguments of qemu is only a bit (but not much) more -challenging than editing libvirt's XML. - -## Running in a Pod - -The real purpose of the runner is to run a VM on Kubernetes in a pod. -When running in a Kubernetes pod, `/etc/opt/vmrunner/config.yaml` should be -provided by a -[ConfigMap](https://kubernetes.io/docs/concepts/configuration/configmap/). - -If additional templates are required, some ReadOnlyMany PV should -be mounted in `/opt/vmrunner/templates`. The PV should contain copies -of the standard templates as well as the additional templates. Of course, -a ConfigMap can be used for this purpose again. - -Networking options are rather limited. The assumption is that in general -the VM wants full network connectivity. To achieve this, the pod must -run with host networking and the host's networking must provide a -bridge that the VM can attach to. The only currently supported -alternative is the less performant -"[user networking](https://wiki.qemu.org/Documentation/Networking#User_Networking_(SLIRP))", -which may be used in a stand-alone development configuration. - -## Runtime changes - -The runner supports adaption to changes of the RAM size (using the -balloon device) and to changes of the number of CPUs. Note that -in order to get new CPUs online on Linux guests, you need a -[udev rule](https://docs.kernel.org/core-api/cpu_hotplug.html#user-space-notification) -which is not installed by default[^simplest]. - -The runner also changes the images loaded in CDROM drives. If the -drive is locked, i.e. if it doesn't respond to the "open tray" command -the change will be suspended until the VM opens the tray. - -Finally, `powerdownTimeout` can be changed while the qemu process runs. - -[^simplest]: The simplest form of the rule is probably: - - ```txt - ACTION=="add", SUBSYSTEM=="cpu", ATTR{online}="1" - ``` - -## Testing with Helm - -There is a -[Helm Chart](https://github.com/mnlipp/VM-Operator/tree/main/org.jdrupes.vmoperator.runner.qemu/helm-test) -for testing the runner. diff --git a/webpages/stylesheets/pygment_trac.css b/webpages/stylesheets/pygment_trac.css deleted file mode 100644 index c6a6452..0000000 --- a/webpages/stylesheets/pygment_trac.css +++ /dev/null @@ -1,69 +0,0 @@ -.highlight { background: #ffffff; } -.highlight .c { color: #999988; font-style: italic } /* Comment */ -.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */ -.highlight .k { font-weight: bold } /* Keyword */ -.highlight .o { font-weight: bold } /* Operator */ -.highlight .cm { color: #999988; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #999999; font-weight: bold } /* Comment.Preproc */ -.highlight .c1 { color: #999988; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */ -.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ -.highlight .gd .x { color: #000000; background-color: #ffaaaa } /* Generic.Deleted.Specific */ -.highlight .ge { font-style: italic } /* Generic.Emph */ -.highlight .gr { color: #aa0000 } /* Generic.Error */ -.highlight .gh { color: #999999 } /* Generic.Heading */ -.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ -.highlight .gi .x { color: #000000; background-color: #aaffaa } /* Generic.Inserted.Specific */ -.highlight .go { color: #888888 } /* Generic.Output */ -.highlight .gp { color: #555555 } /* Generic.Prompt */ -.highlight .gs { font-weight: bold } /* Generic.Strong */ -.highlight .gu { color: #800080; font-weight: bold; } /* Generic.Subheading */ -.highlight .gt { color: #aa0000 } /* Generic.Traceback */ -.highlight .kc { font-weight: bold } /* Keyword.Constant */ -.highlight .kd { font-weight: bold } /* Keyword.Declaration */ -.highlight .kn { font-weight: bold } /* Keyword.Namespace */ -.highlight .kp { font-weight: bold } /* Keyword.Pseudo */ -.highlight .kr { font-weight: bold } /* Keyword.Reserved */ -.highlight .kt { color: #445588; font-weight: bold } /* Keyword.Type */ -.highlight .m { color: #009999 } /* Literal.Number */ -.highlight .s { color: #d14 } /* Literal.String */ -.highlight .na { color: #008080 } /* Name.Attribute */ -.highlight .nb { color: #0086B3 } /* Name.Builtin */ -.highlight .nc { color: #445588; font-weight: bold } /* Name.Class */ -.highlight .no { color: #008080 } /* Name.Constant */ -.highlight .ni { color: #800080 } /* Name.Entity */ -.highlight .ne { color: #990000; font-weight: bold } /* Name.Exception */ -.highlight .nf { color: #990000; font-weight: bold } /* Name.Function */ -.highlight .nn { color: #555555 } /* Name.Namespace */ -.highlight .nt { color: #000080 } /* Name.Tag */ -.highlight .nv { color: #008080 } /* Name.Variable */ -.highlight .ow { font-weight: bold } /* Operator.Word */ -.highlight .w { color: #bbbbbb } /* Text.Whitespace */ -.highlight .mf { color: #009999 } /* Literal.Number.Float */ -.highlight .mh { color: #009999 } /* Literal.Number.Hex */ -.highlight .mi { color: #009999 } /* Literal.Number.Integer */ -.highlight .mo { color: #009999 } /* Literal.Number.Oct */ -.highlight .sb { color: #d14 } /* Literal.String.Backtick */ -.highlight .sc { color: #d14 } /* Literal.String.Char */ -.highlight .sd { color: #d14 } /* Literal.String.Doc */ -.highlight .s2 { color: #d14 } /* Literal.String.Double */ -.highlight .se { color: #d14 } /* Literal.String.Escape */ -.highlight .sh { color: #d14 } /* Literal.String.Heredoc */ -.highlight .si { color: #d14 } /* Literal.String.Interpol */ -.highlight .sx { color: #d14 } /* Literal.String.Other */ -.highlight .sr { color: #009926 } /* Literal.String.Regex */ -.highlight .s1 { color: #d14 } /* Literal.String.Single */ -.highlight .ss { color: #990073 } /* Literal.String.Symbol */ -.highlight .bp { color: #999999 } /* Name.Builtin.Pseudo */ -.highlight .vc { color: #008080 } /* Name.Variable.Class */ -.highlight .vg { color: #008080 } /* Name.Variable.Global */ -.highlight .vi { color: #008080 } /* Name.Variable.Instance */ -.highlight .il { color: #009999 } /* Literal.Number.Integer.Long */ - -.type-csharp .highlight .k { color: #0000FF } -.type-csharp .highlight .kt { color: #0000FF } -.type-csharp .highlight .nf { color: #000000; font-weight: normal } -.type-csharp .highlight .nc { color: #2B91AF } -.type-csharp .highlight .nn { color: #000000 } -.type-csharp .highlight .s { color: #A31515 } -.type-csharp .highlight .sc { color: #A31515 } diff --git a/webpages/stylesheets/styles.css b/webpages/stylesheets/styles.css deleted file mode 100644 index 41fb0d0..0000000 --- a/webpages/stylesheets/styles.css +++ /dev/null @@ -1,298 +0,0 @@ -body { - background-color: #fff; - padding:50px; - font: normal 16px/1.5 Verdana, Arial, Helvetica, sans-serif; - color:#595959; -} - -h1, h2, h3, h4, h5, h6, .index-title, .index-subtitle { - color:#222; - margin:0 0 20px; -} - -p, ul, ol, table, pre, dl { - margin:0 0 20px; -} - -h1, h2, h3, .index-title, .index-subtitle { - line-height:1.1; -} - -h1, .index-title { - font-size:28px; - font-weight: 500; -} - -h2 { - color:#393939; - font-weight: 500; -} - -h3, h4, h5, h6, .index-subtitle { - color:#494949; - font-weight: 500; -} - -.index-subtitle { - font-size: 1.17em; -} - -a { - color:#39c; - text-decoration:none; -} - -a:hover { - color:#069; -} - -a small { - font-size:11px; - color:#777; - margin-top:-0.3em; - display:block; -} - -a:hover small { - color:#777; -} - -.wrapper { - /* width:860px; */ - width: 100%; - margin:0 auto; -} - -blockquote { - border-left:1px solid #e5e5e5; - margin:0; - padding:0 0 0 20px; - font-style:italic; -} - -code, pre { - font-family:Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal, Consolas, Liberation Mono, DejaVu Sans Mono, Courier New, monospace; - color:#333; -} - -pre { - font-size: 15px; - padding:8px 15px; - background: #f8f8f8; - border-radius:5px; - border:1px solid #e5e5e5; - overflow-x: auto; -} - -a code { - color: inherit; -} - -table { - width:100%; - border-collapse:collapse; -} - -th, td { - text-align:left; - padding:5px 10px; - border-bottom:1px solid #e5e5e5; -} - -dt { - color:#444; - font-weight:500; -} - -th { - color:#444; -} - -img { - max-width:100%; -} - -header { - /* width:270px; */ - width:calc(29% - 50px); - height:calc(100% - 160px); - overflow: auto; - float:left; - position:fixed; - -webkit-font-smoothing:subpixel-antialiased; -} - -header li { - list-style-type: disc; -} - -header ul { - padding-left: 1rem; -} - -header ul > li { - margin-left: 1rem; -} - -ul.no-bullets { - padding-left: 0; -} - -ul.no-bullets > li { - list-style: none; -} - -strong { - color:#222; - font-weight:500; -} - -section { - width:70%; - max-width:54em; - float:right; - padding-bottom:50px; -} - -small { - font-size:11px; -} - -hr { - border:0; - background:#e5e5e5; - height:1px; - margin:0 0 20px; -} - -footer { - /* width:270px; */ - width:calc(24% - 50px); - height:40px; - float:left; - position:fixed; - padding:30px 0; - bottom:0px; - background-color:white; - -webkit-font-smoothing:subpixel-antialiased; -} - -.post-date { - float: right; -} - -.part-list-title { - margin-bottom:5px; -} - -.part-entry { - margin-bottom:5px; -} - -#search { - - --pagefind-ui-font: inherit; - --pagefind-ui-border-radius: 4px; - - position: absolute; - right: 1em; - top: 1em; - - .pagefind-ui__form { - width: 20em; - margin-left: auto; - - &::before { - top: calc(17px * var(--pagefind-ui-scale)); - } - } - - .pagefind-ui__search-input { - font-weight: inherit; - height: calc(48px * var(--pagefind-ui-scale)); - } - - .pagefind-ui__search-clear { - font-weight: inherit; - height: calc(42px * var(--pagefind-ui-scale)); - } - - .pagefind-ui__drawer { - position: absolute; - right: 0; - width: 40em; - background-color: white; - border: solid var(--pagefind-ui-border-width) var(--pagefind-ui-border); - padding: 0 1em 1em 1em; - } - - .pagefind-ui__message { - padding-top: 0; - } - - .pagefind-ui__result { - padding: 0; - } - - .pagefind-ui__result-title { - font-weight: inherit; - } -} - -@media print, screen and (max-width: 960px) { - - div.wrapper { - width:auto; - margin:0; - } - - header, section, footer { - float:none; - position:static; - width:auto; - } - - header { - padding-right:320px; - } - - section { - border:1px solid #e5e5e5; - border-width:1px 0; - padding:20px 0; - margin:0 0 20px; - } - - header a small { - display:inline; - } -} - -@media print, screen and (max-width: 720px) { - body { - word-wrap:break-word; - } - - header { - padding:0; - } - - pre, code { - word-wrap:normal; - } -} - -@media print, screen and (max-width: 480px) { - body { - padding:15px; - } - -} - -@media print { - body { - padding:0.4in; - font-size:12pt; - color:#444; - } -} diff --git a/webpages/upgrading.md b/webpages/upgrading.md deleted file mode 100644 index f3119b0..0000000 --- a/webpages/upgrading.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -title: "VM-Operator: Upgrading — Issues to watch out for" -description: >- - Information about issues to watch out for when upgrading the VM-Operator. -layout: vm-operator ---- - -# Upgrading - -## To version 4.0.0 - - * The VmViewer conlet has been renamed to VmAccess. This affects the - [configuration](https://vm-operator.jdrupes.org/user-gui.html). - Configuration information using the old path - `/Manager/GuiHttpServer/ConsoleWeblet/WebConsole/ComponentCollector/VmViewer` - is still accepted for backward compatibility until the next major version, - but should be updated. - - The change of name also causes conlets added to the overview page by - users to "disappear" from the GUI. They have to be re-added. - - The latter behavior also applies to the VmConlet conlet which has been - renamed to VmMgmt. - - * The configuration property `passwordValidity` has been moved from component - `/Manager/Controller/DisplaySecretMonitor` to - `/Manager/Controller/Reconciler/DisplaySecretReconciler`. The old path is - still accepted for backward compatibility until the next major version, - but should be updated. - - * The standard [template](./runner.html#stand-alone-configuration) used - to generate the qemu command has been updated. Unless you have enabled - automatic updates of the template in the VM definition, you have to - update the template manually. If you're using your own template, you - have to add a virtual serial port (see the git history of the standard - template for the required addition). - - * Stateful sets from pre 3.4.0 versions are no longer removed automatically - (see notes below). However, PVCs with the old naming scheme are still - reused. - -## To version 3.4.0 - -Starting with this version, the VM-Operator no longer uses a stateful set -with replica set to 1 to (indirectly) start the pod with the VM. Rather -it creates the pod directly. This implies that the PVCs must also be created -by the VM-Operator, which needs additional permissions to do so (update of -`deploy/vmop-role.yaml). As it would be ridiculous to keep the naming scheme -used by the stateful set when generating PVCs, the VM-Operator uses a -[different pattern](controller.html#defining-disks) for creating new PVCs. - -The change is backward compatible: - - * Running pods created by a stateful set are left alone until stopped. - Only then will the stateful set be removed. - - * The VM-Operator looks for existing PVCs generated by a stateful - set in the pre 3.4 versions (naming pattern "*name*-disk-*vmName*-0") - and reuses them. Only new PVCs are generated using the new pattern. - -## To version 3.0.0 - -All configuration files are backward compatible to version 2.3.0. -Note that in order to make use of the new viewer component, -[permissions](https://mnlipp.github.io/VM-Operator/user-gui.html#control-access-to-vms) -must be configured in the CR definition. Also note that -[display secrets](https://mnlipp.github.io/VM-Operator/user-gui.html#securing-access) -are automatically created unless explicitly disabled. - -## To version 2.3.0 - -Starting with version 2.3.0, the web GUI uses a login conlet that -supports OIDC providers. This effects the configuration of the -web GUI components. - -## To version 2.2.0 - -Version 2.2.0 sets the stateful set's `.spec.updateStrategy.type` to -"OnDelete". This fails for no apparent reason if a definition of -the stateful set with the default value "RollingUpdate" already exists. -In order to fix this, either the stateful set or the complete VM definition -must be deleted and the manager must be restarted. diff --git a/webpages/user-gui.md b/webpages/user-gui.md deleted file mode 100644 index 3ff816f..0000000 --- a/webpages/user-gui.md +++ /dev/null @@ -1,152 +0,0 @@ ---- -title: "VM-Operator: User View — Allows users to manage their own VMs" -description: >- - Information about the user view of the VM-Operator, which allows users - to access and optionally manage the VMs for which they have the - respective permissions. -layout: vm-operator ---- - -# User view - -*Since 3.0.0* - -The idea of the user view is to provide an intuitive widget that -allows the users to access their own VMs and to optionally start -and stop them. - -![VM Access](VmAccess-preview.png) - -The configuration options resulting from this seemingly simple -requirement are unexpectedly complex. - -## Control access to VMs - -First of all, we have to define which VMs a user can access. This -is done using the optional property `spec.permissions` of the -VM definition (CRD). - -```yaml -spec: - permissions: - - role: admin - may: - - "*" - - user: test - may: - - start - - stop - - accessConsole -``` - -Permissions can be granted to individual users or to roles. There -is a permission for each possible action. "*" grants them all. - -## Simple usage vs. expert usage - -Next, there are two ways to create the VM widgets (preview conlets -in the framework's terms). They can be created on demand or -automatically for each VM that a logged in user has permission to -access. The former is the preferred way for an administrator who -has access to all VMs and needs to open a particular VM's console -for trouble shooting only. The latter is the preferred way -for a regular user who has access to a limited number of VMs. -In this case, creating the widgets automatically has the additional -benefit that regular users don't need to know how to create and -configure the widgets using the menu and the properties dialog. - -Automatic synchronization of widgets and accessible VMs is controlled -by the property `syncPreviewsFor` of the VM viewer. It's an array with -objects that either specify a role or a user. - -```yaml -"/Manager": - # This configures the GUI - "/GuiHttpServer": - "/ConsoleWeblet": - "/WebConsole": - "/ComponentCollector": - "/VmAccess": - syncPreviewsFor: - - role: user - - user: test - displayResource: - preferredIpVersion: ipv4 -``` - -## Console access - -Access to the VM's console is implemented by generating a -[connection file](https://manpages.debian.org/testing/virt-viewer/remote-viewer.1.en.html#CONNECTION_FILE) -for virt-viewer when the user clicks on -the console icon. If automatic open is enabled for this kind of -files in the browser, the console opens without further user action. - -The file contains all required and optional information to start the -remote viewer. - - * The "host" is by default the IP address of the node that the - VM's pod is running on (remember that the runner uses host - networking). - * The "port" is simply taken from the VM definition. - -In more complex scenarios, an administrator may have set up a load -balancer that hides the worker node's IP addresses or the worker -nodes use an internal network and can only be accessed through a -proxy. For both cases, the values to include in the connection file -can be specified as properties of `spec.vm.display.spice` in the -VM definition. - -```yaml -spec: - vm: - display: - spice: - port: 5930 - server: 192.168.19.32 - proxyUrl: http://vms-spice.some.host:1234 - generateSecret: true -``` - -The value of `server` is used as value for key "host" in the -connection file, thus overriding the default value. The -value of `proxyUrl` is used as value for key "proxy". - -## Securing access - -As described [previously](./controller.html#display-secretpassword), -access to a VM's display can be secured with a password. If a secret -with a password exists for a VM, the password is -included in the connection file. - -While this approach is very convenient for the user, it is not -secure, because this leaves the password as plain text in a file on -the user's computer (the downloaded connection file). To work around -this, the display secret is updated with a random password with -limited validity, unless the display secret defines a `password-expiry` -in the future or with value "never" or doesn't define a -`password-expiry` at all. - -The automatically generated password is the base64 encoded value -of 16 (strong) random bytes (128 random bits). It is valid for -10 seconds only. This may be challenging on a slower computer -or if users may not enable automatic open for connection files -in the browser. The validity can therefore be adjusted in the -configuration.[^oldPath] - -```yaml -"/Manager": - "/Controller": - "/Reconciler": - "/DisplaySecretReconciler": - # Validity of generated password in seconds - passwordValidity: 10 -``` - -[^oldPath]: Before version 4.0, the path for `passwordValidity` was - `/Manager/Controller/DisplaySecretMonitor`. - -Taking into account that the controller generates a display -secret automatically by default, this approach to securing -console access should be sufficient in all cases. (Any feedback -if something has been missed is appreciated.) diff --git a/webpages/webgui.md b/webpages/webgui.md deleted file mode 100644 index 2d6e428..0000000 --- a/webpages/webgui.md +++ /dev/null @@ -1,117 +0,0 @@ ---- -title: "VM-Operator: Web user interface — Provides easy access to VM management" -layout: vm-operator ---- - -# Web user interface - -The manager component provides a GUI via a web server. This web user interface is -implemented using components from the -[JGrapes WebConsole](https://jgrapes.org/WebConsole.html) -project. Configuration of the GUI therefore follows the conventions -of that framework. - -The structure of the configuration information should be easy to -understand from the examples provided. In general, configuration values -are applied to the individual components that make up an application. -The hierarchy of the components is reflected in the configuration -information because components are "addressed" by their position in -that hierarchy. (See -[the package description](latest-release/javadoc/org/jdrupes/vmoperator/manager/package-summary.html) -for information about the complete component structure.) - -## Network access - -By default, the service is made available at port 8080 of the manager -pod. Of course, a kubernetes service and an ingress configuration must -be added as required by the environment. (See the -[definition](https://github.com/mnlipp/VM-Operator/blob/main/deploy/vmop-service.yaml) -from the -[sample deployment](https://github.com/mnlipp/VM-Operator/tree/main/deploy)). - -## User Access - -Access to the web user interface is controlled by the login conlet. The framework -does not include sophisticated components for user management. Rather, -it assumes that an OIDC provider is responsible for user authentication -and role management. - -```yaml -"/Manager": - # "/GuiSocketServer": - # port: 8080 - "/GuiHttpServer": - # This configures the GUI - "/ConsoleWeblet": - "/WebConsole": - "/LoginConlet": - # Starting with version 2.3.0 the preferred approach is to - # configure an OIDC provider for user management and - # authorization. See the text for details. - oidcProviders: {} - - # Support for "local" users is provided as a fallback mechanism. - # Note that up to Version 2.2.x "users" was an object with user names - # as its properties. Starting with 2.3.0 it is a list as shown. - users: - - name: admin - fullName: Administrator - password: "Generate hash with bcrypt" - - name: test - fullName: Test Account - password: "Generate hash with bcrypt" - - # Required for using OIDC, see the text for details. - "/OidcClient": - redirectUri: https://my.server.here/oauth/callback" - - # May be used for assigning roles to both local users and users from - # the OIDC provider. Not needed if roles are managed by the OIDC provider. - "/RoleConfigurator": - rolesByUser: - # User admin has role admin - admin: - - admin - # Non-privileged users are users - test: - - user - # All users have role other - "*": - - other - replace: false - - # Manages the permissions for the roles. - "/RoleConletFilter": - conletTypesByRole: - # Admins can use all conlets - admin: - - "*" - # Users can use the viewer conlet - user: - - org.jdrupes.vmoperator.vmviewer.VmViewer - # Others cannot use any conlet (except login conlet to log out) - other: - # Up to version 2.2.x - # - org.jgrapes.webconlet.locallogin.LoginConlet - # Starting with version 2.3.0 - - org.jgrapes.webconlet.oidclogin.LoginConlet -``` - -How local users can be configured should be obvious from the example. -The configuration of OIDC providers for user authentication (and -optionally for role assignment) is explained in the documentation of the -[login conlet](https://jgrapes.org/javadoc-webconsole/org/jgrapes/webconlet/oidclogin/LoginConlet.html). -Details about the `RoleConfigurator` and `RoleConletFilter` can also be found -in the documentation of the -[JGrapes WebConsole](https://jgrapes.org/WebConsole.html) -project. - -The configuration above allows all users with role "admin" to use all -GUI components and users with role "user" to only use the viewer conlet, -i.e. the [User view](user-gui.html). The fallback role "other" allows -all users to use the login conlet to log out. - -## Views - -The configuration of the components that provide the manager and -users views is explained in the respective sections.