diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index ad8e2c3..0000000 --- a/.editorconfig +++ /dev/null @@ -1,14 +0,0 @@ -root = true - -[*] -charset = utf-8 -insert_final_newline = true -trim_trailing_whitespace = true - -[*.{html,md,yml,yaml}] -indent_size = 2 -indent_style = space - -[*.{gradle,js,ts}] -indent_size = 4 -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/codeql.yml b/.github/workflows/codeql.yml index 380981e..32e0219 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,7 +2,7 @@ name: "CodeQL" on: push: - branches: [ "*" ] + branches: [ "main" ] pull_request: # The branches below must be a subset of the branches above branches: [ "main" ] diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index f47366a..629d13c 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -1,8 +1,10 @@ name: Java CI with Gradle on: - push: {} - pull_request: {} + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] permissions: contents: read @@ -22,10 +24,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..d6cde8a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,17 +8,13 @@ permissions: contents: read packages: write -concurrency: - group: doc_generation - cancel-in-progress: false - jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Install graphviz @@ -31,10 +27,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 -Pdocker.registry=ghcr.io/${{ github.actor }} pushImages diff --git a/.gitignore b/.gitignore index ca012b5..2595367 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,3 @@ bin .classpath .project org.eclipse.jdt.core.prefs -**/.externalToolBuilders/ - -# Node -**/node_modules/ 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/.project b/.project new file mode 100644 index 0000000..505b6f1 --- /dev/null +++ b/.project @@ -0,0 +1,42 @@ + + + VM-Operator + + + + org.eclipse.jdt.core.javanature + org.eclipse.buildship.core.gradleprojectnature + net.sf.eclipsecs.core.CheckstyleNature + ch.acanda.eclipse.pmd.builder.PMDNature + + + + org.eclipse.jdt.core.javabuilder + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + net.sf.eclipsecs.core.CheckstyleBuilder + + + + ch.acanda.eclipse.pmd.builder.PMDBuilder + + + + + + + 1 + 30 + + + org.eclipse.core.resources.regexFilterMatcher + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + + diff --git a/.settings/org.eclipse.buildship.core.prefs b/.settings/org.eclipse.buildship.core.prefs index 72733d9..33d02f4 100644 --- a/.settings/org.eclipse.buildship.core.prefs +++ b/.settings/org.eclipse.buildship.core.prefs @@ -1,11 +1,11 @@ -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) connection.project.dir= eclipse.preferences.version=1 gradle.user.home= -java.home= +java.home=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.362.b09-2.fc37.x86_64 jvm.arguments= offline.mode=false override.workspace.settings=true 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..e1c2df1 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,11 @@ -[![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/VM-Operator.png b/VM-Operator.png deleted file mode 100644 index 9ecb022..0000000 Binary files a/VM-Operator.png and /dev/null differ diff --git a/build.gradle b/build.gradle index eb8e59a..7225e7e 100644 --- a/build.gradle +++ b/build.gradle @@ -6,20 +6,16 @@ buildscript { plugins { id 'org.ajoberstar.grgit' version '5.2.0' - id 'org.ajoberstar.git-publish' version '4.2.0' apply false - id 'pl.allegro.tech.build.axion-release' version '1.17.2' apply false - id 'org.jdrupes.vmoperator.versioning-conventions' + id 'org.ajoberstar.git-publish' version '4.2.0' + 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" } -allprojects { - project.group = 'org.jdrupes.vmoperator' -} +project.group = 'org.jdrupes.vmoperator' 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 +23,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/.project b/buildSrc/.project new file mode 100644 index 0000000..8624d9f --- /dev/null +++ b/buildSrc/.project @@ -0,0 +1,11 @@ + + + buildSrc + + + + + + + + diff --git a/buildSrc/.settings/org.eclipse.core.resources.prefs b/buildSrc/.settings/org.eclipse.core.resources.prefs deleted file mode 100644 index 99f26c0..0000000 --- a/buildSrc/.settings/org.eclipse.core.resources.prefs +++ /dev/null @@ -1,2 +0,0 @@ -eclipse.preferences.version=1 -encoding/=UTF-8 diff --git a/buildSrc/.settings/org.eclipse.core.runtime.prefs b/buildSrc/.settings/org.eclipse.core.runtime.prefs deleted file mode 100644 index 5a0ad22..0000000 --- a/buildSrc/.settings/org.eclipse.core.runtime.prefs +++ /dev/null @@ -1,2 +0,0 @@ -eclipse.preferences.version=1 -line.separator=\n diff --git a/buildSrc/.settings/org.eclipse.jdt.core.prefs b/buildSrc/.settings/org.eclipse.jdt.core.prefs index b25073a..a15493a 100644 --- a/buildSrc/.settings/org.eclipse.jdt.core.prefs +++ b/buildSrc/.settings/org.eclipse.jdt.core.prefs @@ -1,13 +1,24 @@ # -#Wed Oct 02 14:48:43 CEST 2024 -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=21 -org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=21 -org.eclipse.jdt.core.compiler.debug.lineNumber=generate -org.eclipse.jdt.core.compiler.debug.localVariable=generate -org.eclipse.jdt.core.compiler.debug.sourceFile=generate +#Sun May 07 20:03:15 CEST 2023 +org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=21 +org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore +org.eclipse.jdt.core.compiler.source=17 +org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull +org.eclipse.jdt.core.compiler.release=disabled +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning +org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.compliance=17 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..8bad6d4 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -1,34 +1,39 @@ +/* + * 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 // as plugins in the main build. id 'groovy-gradle-plugin' +} - // Apply eclipse plugin - 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'] + } + } -eclipse { - - jdt { - file { - withProperties { properties -> - def formatterPrefs = new Properties() - rootProject.file("../gradle/org.eclipse.jdt.core.formatter.prefs") - .withInputStream { formatterPrefs.load(it) } - properties.putAll(formatterPrefs) - } + test { + groovy { + srcDirs = ['test'] } } } + +/* +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} +*/ 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..0dba0c0 100644 --- a/buildSrc/src/org.jdrupes.vmoperator.java-common-conventions.gradle +++ b/buildSrc/src/org.jdrupes.vmoperator.java-common-conventions.gradle @@ -5,14 +5,12 @@ */ 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' + // Git based versioning + id 'pl.allegro.tech.build.axion-release' + // Apply eclipse plugin id 'eclipse' @@ -23,6 +21,7 @@ plugins { repositories { // Use Maven Central for resolving dependencies. mavenCentral() + mavenLocal() } dependencies { @@ -55,25 +54,32 @@ sourceSets { java { toolchain { - languageVersion = JavaLanguageVersion.of(21) + languageVersion = JavaLanguageVersion.of(17) } } +scmVersion { + versionIncrementer 'incrementMinor' + tag { + def shortened = project.name.startsWith(project.group + ".") ? + project.name.substring(project.group.length() + 1) : project.name + prefix = shortened.replace('.', '-') + "-" + } +} +version = scmVersion.version +ext.isSnapshot = version.endsWith('-SNAPSHOT') + 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..db5e742 100644 --- a/buildSrc/src/org.jdrupes.vmoperator.java-doc-conventions.gradle +++ b/buildSrc/src/org.jdrupes.vmoperator.java-doc-conventions.gradle @@ -1,13 +1,8 @@ plugins { - // Apply the common versioning conventions. - id 'org.jdrupes.vmoperator.versioning-conventions' - - id 'org.ajoberstar.git-publish' -} + // Apply the common convention plugin for shared build configuration between library and application projects. + id 'org.jdrupes.vmoperator.java-common-conventions' -repositories { - // Use Maven Central for resolving dependencies. - mavenCentral() + id 'org.ajoberstar.git-publish' } var docDestinationDir = file("${rootProject.buildDir}/javadoc") @@ -22,28 +17,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 apidocs (type: JavaExec) { +task javadocResources(type: Copy) { + into file(docDestinationDir) + from ("${rootProject.rootDir}/misc") { + include '*.woff2' + } +} + +task java11doc (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 +64,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 +83,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 +92,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 java11doc } diff --git a/buildSrc/src/org.jdrupes.vmoperator.versioning-conventions.gradle b/buildSrc/src/org.jdrupes.vmoperator.versioning-conventions.gradle deleted file mode 100644 index 49b6f74..0000000 --- a/buildSrc/src/org.jdrupes.vmoperator.versioning-conventions.gradle +++ /dev/null @@ -1,36 +0,0 @@ -/* - * This file was generated by the Gradle 'init' task. - * - * This project uses @Incubating APIs which are subject to change. - */ - -plugins { - // Required by axion-release - id 'org.ajoberstar.grgit' - // Git based versioning - 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 - } -} -project.version = scmVersion.version -ext.isSnapshot = version.endsWith('-SNAPSHOT') diff --git a/checkstyle.xml b/checkstyle.xml index 088e543..7fa3b95 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -1,7 +1,7 @@ - - - - \ No newline at end of file diff --git a/misc/stylesheet.css b/misc/stylesheet.css 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/.checkstyle b/org.jdrupes.vmoperator.common/.checkstyle deleted file mode 100644 index 7f2c604..0000000 --- a/org.jdrupes.vmoperator.common/.checkstyle +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/org.jdrupes.vmoperator.common/.eclipse-pmd b/org.jdrupes.vmoperator.common/.eclipse-pmd deleted file mode 100644 index 5d69caa..0000000 --- a/org.jdrupes.vmoperator.common/.eclipse-pmd +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - 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/.settings/org.eclipse.buildship.core.prefs b/org.jdrupes.vmoperator.common/.settings/org.eclipse.buildship.core.prefs deleted file mode 100644 index 258eb47..0000000 --- a/org.jdrupes.vmoperator.common/.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/org.jdrupes.vmoperator.common/.settings/org.eclipse.core.resources.prefs b/org.jdrupes.vmoperator.common/.settings/org.eclipse.core.resources.prefs deleted file mode 100644 index 99f26c0..0000000 --- a/org.jdrupes.vmoperator.common/.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.common/.settings/org.eclipse.core.runtime.prefs b/org.jdrupes.vmoperator.common/.settings/org.eclipse.core.runtime.prefs deleted file mode 100644 index 5a0ad22..0000000 --- a/org.jdrupes.vmoperator.common/.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.common/build.gradle b/org.jdrupes.vmoperator.common/build.gradle deleted file mode 100644 index e72cb14..0000000 --- a/org.jdrupes.vmoperator.common/build.gradle +++ /dev/null @@ -1,17 +0,0 @@ -/* - * This file was generated by the Gradle 'init' task. - * - * This project uses @Incubating APIs which are subject to change. - */ - -plugins { - id 'org.jdrupes.vmoperator.java-library-conventions' -} - -dependencies { - api project(':org.jdrupes.vmoperator.util') - api 'org.jgrapes:org.jgrapes.core:[1.22.1,2)' - api 'io.kubernetes:client-java:[19.0.0,20.0.0)' - api 'org.yaml:snakeyaml' - api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:[2.16.1,3]' -} diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Constants.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Constants.java deleted file mode 100644 index b9de69f..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Constants.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.common; - -/** - * Some constants. - */ -@SuppressWarnings("PMD.DataClass") -public class Constants { - - /** The Constant APP_NAME. */ - public static final String APP_NAME = "vm-runner"; - - /** The Constant VM_OP_NAME. */ - public static final String VM_OP_NAME = "vm-operator"; - - /** - * Constants related to the CRD. - */ - @SuppressWarnings("PMD.ShortClassName") - public static class Crd { - /** The Constant GROUP. */ - public static final String GROUP = "vmoperator.jdrupes.org"; - - /** The Constant KIND_VM. */ - public static final String KIND_VM = "VirtualMachine"; - - /** The Constant KIND_VM_POOL. */ - public static final String KIND_VM_POOL = "VmPool"; - } - - /** - * Status related constants. - */ - public static class Status { - /** The Constant RUNNER_VERSION. */ - public static final String RUNNER_VERSION = "runnerVersion"; - - /** The Constant CPUS. */ - public static final String CPUS = "cpus"; - - /** The Constant RAM. */ - public static final String RAM = "ram"; - - /** The Constant OSINFO. */ - public static final String OSINFO = "osinfo"; - - /** The Constant DISPLAY_PASSWORD_SERIAL. */ - public static final String DISPLAY_PASSWORD_SERIAL - = "displayPasswordSerial"; - - /** The Constant LOGGED_IN_USER. */ - public static final String LOGGED_IN_USER = "loggedInUser"; - - /** The Constant CONSOLE_CLIENT. */ - public static final String CONSOLE_CLIENT = "consoleClient"; - - /** The Constant CONSOLE_USER. */ - public static final String CONSOLE_USER = "consoleUser"; - - /** The Constant ASSIGNMENT. */ - public static final String ASSIGNMENT = "assignment"; - - /** - * Conditions used in Status. - */ - public static class Condition { - /** The Constant COND_RUNNING. */ - public static final String RUNNING = "Running"; - - /** The Constant COND_BOOTED. */ - public static final String BOOTED = "Booted"; - - /** The Constant COND_VMOP_AGENT. */ - public static final String VMOP_AGENT = "VmopAgentConnected"; - - /** The Constant COND_USER_LOGGED_IN. */ - public static final String USER_LOGGED_IN = "UserLoggedIn"; - - /** The Constant COND_CONSOLE. */ - public static final String CONSOLE_CONNECTED = "ConsoleConnected"; - - /** - * Reasons used in conditions. - */ - public static class Reason { - /** The Constant NOT_REQUESTED. */ - public static final String NOT_REQUESTED = "NotRequested"; - - /** The Constant USER_LOGGED_IN. */ - public static final String LOGGED_IN = "LoggedIn"; - } - } - } - - /** - * DisplaySecret related constants. - */ - public static class DisplaySecret { - - /** The Constant NAME. */ - public static final String NAME = "display-secret"; - - /** The Constant PASSWORD. */ - public static final String PASSWORD = "display-password"; - - /** The Constant EXPIRY. */ - public static final String EXPIRY = "password-expiry"; - - } -} diff --git a/org.jdrupes.vmoperator.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 deleted file mode 100644 index 3870337..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8s.java +++ /dev/null @@ -1,249 +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.common; - -import com.google.gson.JsonObject; -import io.kubernetes.client.Discovery; -import io.kubernetes.client.Discovery.APIResource; -import io.kubernetes.client.common.KubernetesListObject; -import io.kubernetes.client.common.KubernetesObject; -import io.kubernetes.client.common.KubernetesType; -import io.kubernetes.client.custom.V1Patch; -import io.kubernetes.client.openapi.ApiClient; -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.openapi.apis.EventsV1Api; -import io.kubernetes.client.openapi.models.EventsV1Event; -import io.kubernetes.client.openapi.models.V1ObjectMeta; -import io.kubernetes.client.openapi.models.V1ObjectReference; -import io.kubernetes.client.util.Strings; -import io.kubernetes.client.util.generic.GenericKubernetesApi; -import io.kubernetes.client.util.generic.KubernetesApiResponse; -import io.kubernetes.client.util.generic.options.PatchOptions; -import java.io.Reader; -import java.net.HttpURLConnection; -import java.time.OffsetDateTime; -import java.util.Map; -import java.util.Optional; -import org.yaml.snakeyaml.LoaderOptions; -import org.yaml.snakeyaml.Yaml; -import org.yaml.snakeyaml.constructor.SafeConstructor; - -/** - * Helpers for K8s API. - */ -@SuppressWarnings({ "PMD.ShortClassName", "PMD.UseUtilityClass" }) -public class K8s { - - /** - * Returns the result from an API call as {@link Optional} if the - * call was successful. Returns an empty `Optional` if the status - * code is 404 (not found). Else throws an exception. - * - * @param the generic type - * @param response the response - * @return the optional - * @throws ApiException the API exception - */ - public static Optional - optional(KubernetesApiResponse response) throws ApiException { - if (response.isSuccess()) { - return Optional.of(response.getObject()); - } - if (response.getHttpStatusCode() == HttpURLConnection.HTTP_NOT_FOUND) { - return Optional.empty(); - } - response.throwsApiException(); - // Never reached - return Optional.empty(); - } - - /** - * Returns a new context with the given version as preferred version. - * - * @param context the context - * @param version the version - * @return the API resource - */ - public static APIResource preferred(APIResource context, String version) { - assert context.getVersions().contains(version); - return new APIResource(context.getGroup(), - context.getVersions(), version, context.getKind(), - context.getNamespaced(), context.getResourcePlural(), - context.getResourceSingular()); - } - - /** - * Return a string representation of the context (API resource). - * - * @param context the context - * @return the string - */ - @SuppressWarnings("PMD.UseLocaleWithCaseConversions") - public static String toString(APIResource context) { - return (Strings.isNullOrEmpty(context.getGroup()) ? "" - : context.getGroup() + "/") - + context.getPreferredVersion().toUpperCase() - + context.getKind(); - } - - /** - * Convert Yaml to Json. - * - * @param client the client - * @param yaml the yaml - * @return the json element - */ - public static JsonObject yamlToJson(ApiClient client, Reader yaml) { - // Avoid Yaml.load due to - // https://github.com/kubernetes-client/java/issues/2741 - Map yamlData - = new Yaml(new SafeConstructor(new LoaderOptions())).load(yaml); - - // There's no short-cut from Java (collections) to Gson - var gson = client.getJSON().getGson(); - var jsonText = gson.toJson(yamlData); - return gson.fromJson(jsonText, JsonObject.class); - } - - /** - * Lookup the specified API resource. If the version is `null` or - * empty, the preferred version in the result is the default - * returned from the server. - * - * @param client the client - * @param group the group - * @param version the version - * @param kind the kind - * @return the optional - * @throws ApiException the api exception - */ - public static Optional context(ApiClient client, - String group, String version, String kind) throws ApiException { - var apiMatch = new Discovery(client).findAll().stream() - .filter(r -> r.getGroup().equals(group) && r.getKind().equals(kind) - && (Strings.isNullOrEmpty(version) - || r.getVersions().contains(version))) - .findFirst(); - if (apiMatch.isEmpty()) { - return Optional.empty(); - } - var apiRes = apiMatch.get(); - if (!Strings.isNullOrEmpty(version)) { - if (!apiRes.getVersions().contains(version)) { - return Optional.empty(); - } - apiRes = new APIResource(apiRes.getGroup(), apiRes.getVersions(), - version, apiRes.getKind(), apiRes.getNamespaced(), - apiRes.getResourcePlural(), apiRes.getResourceSingular()); - } - return Optional.of(apiRes); - } - - /** - * Apply the given patch data. - * - * @param the generic type - * @param the generic type - * @param api the api - * @param existing the existing - * @param update the update - * @return the t - * @throws ApiException the api exception - */ - @SuppressWarnings("PMD.GenericsNaming") - public static - T apply(GenericKubernetesApi api, T existing, String update) - throws ApiException { - PatchOptions opts = new PatchOptions(); - opts.setForce(true); - opts.setFieldManager("kubernetes-java-kubectl-apply"); - var response = api.patch(existing.getMetadata().getNamespace(), - existing.getMetadata().getName(), V1Patch.PATCH_FORMAT_APPLY_YAML, - new V1Patch(update), opts).throwsApiException(); - return response.getObject(); - } - - /** - * Create an object reference. - * - * @param object the object - * @return the v 1 object reference - */ - public static V1ObjectReference - objectReference(KubernetesObject object) { - return new V1ObjectReference().apiVersion(object.getApiVersion()) - .kind(object.getKind()) - .namespace(object.getMetadata().getNamespace()) - .name(object.getMetadata().getName()) - .resourceVersion(object.getMetadata().getResourceVersion()) - .uid(object.getMetadata().getUid()); - } - - /** - * Creates an event related to the object, adding reasonable defaults. - * - * * If `kind` is not set, it is set to "Event". - * * If `metadata.namespace` is not set, it is set - * to the object's namespace. - * * If neither `metadata.name` nor `matadata.generateName` are set, - * set `generateName` to the object's name with a dash appended. - * * If `reportingInstance` is not set, set it to the object's name. - * * If `eventTime` is not set, set it to now. - * * If `type` is not set, set it to "Normal" - * * If `regarding` is not set, set it to the given object. - * - * @param client the client - * @param object the object - * @param event the event - * @throws ApiException the api exception - */ - @SuppressWarnings("PMD.NPathComplexity") - public static void createEvent(ApiClient client, - KubernetesObject object, EventsV1Event event) - throws ApiException { - if (Strings.isNullOrEmpty(event.getKind())) { - event.kind("Event"); - } - if (event.getMetadata() == null) { - event.metadata(new V1ObjectMeta()); - } - if (Strings.isNullOrEmpty(event.getMetadata().getNamespace())) { - event.getMetadata().namespace(object.getMetadata().getNamespace()); - } - if (Strings.isNullOrEmpty(event.getMetadata().getName()) - && Strings.isNullOrEmpty(event.getMetadata().getGenerateName())) { - event.getMetadata() - .generateName(object.getMetadata().getName() + "-"); - } - if (Strings.isNullOrEmpty(event.getReportingInstance())) { - event.reportingInstance(object.getMetadata().getName()); - } - if (event.getEventTime() == null) { - event.eventTime(OffsetDateTime.now()); - } - if (Strings.isNullOrEmpty(event.getType())) { - event.type("Normal"); - } - if (event.getRegarding() == null) { - event.regarding(objectReference(object)); - } - new EventsV1Api(client).createNamespacedEvent( - object.getMetadata().getNamespace(), event, null, null, null, null); - } -} diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sClient.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sClient.java 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.common/src/org/jdrupes/vmoperator/common/package-info.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/package-info.java deleted file mode 100644 index 1e3e750..0000000 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/package-info.java +++ /dev/null @@ -1,22 +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 . - */ - -/** - * Classes and methods shared among the VM operator modules. - */ -package org.jdrupes.vmoperator.common; \ No newline at end of file diff --git a/org.jdrupes.vmoperator.manager.events/.checkstyle b/org.jdrupes.vmoperator.manager.events/.checkstyle deleted file mode 100644 index 7f2c604..0000000 --- a/org.jdrupes.vmoperator.manager.events/.checkstyle +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/org.jdrupes.vmoperator.manager.events/.eclipse-pmd b/org.jdrupes.vmoperator.manager.events/.eclipse-pmd deleted file mode 100644 index 5d69caa..0000000 --- a/org.jdrupes.vmoperator.manager.events/.eclipse-pmd +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/org.jdrupes.vmoperator.manager.events/.settings/org.eclipse.buildship.core.prefs b/org.jdrupes.vmoperator.manager.events/.settings/org.eclipse.buildship.core.prefs deleted file mode 100644 index 258eb47..0000000 --- a/org.jdrupes.vmoperator.manager.events/.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/org.jdrupes.vmoperator.manager.events/.settings/org.eclipse.core.resources.prefs b/org.jdrupes.vmoperator.manager.events/.settings/org.eclipse.core.resources.prefs deleted file mode 100644 index 99f26c0..0000000 --- a/org.jdrupes.vmoperator.manager.events/.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.manager.events/.settings/org.eclipse.core.runtime.prefs b/org.jdrupes.vmoperator.manager.events/.settings/org.eclipse.core.runtime.prefs deleted file mode 100644 index 5a0ad22..0000000 --- a/org.jdrupes.vmoperator.manager.events/.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.manager.events/build.gradle b/org.jdrupes.vmoperator.manager.events/build.gradle deleted file mode 100644 index bb4b8d8..0000000 --- a/org.jdrupes.vmoperator.manager.events/build.gradle +++ /dev/null @@ -1,13 +0,0 @@ -/* - * This file was generated by the Gradle 'init' task. - * - * This project uses @Incubating APIs which are subject to change. - */ - -plugins { - id 'org.jdrupes.vmoperator.java-library-conventions' -} - -dependencies { - api project(':org.jdrupes.vmoperator.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/ModifyVm.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ModifyVm.java deleted file mode 100644 index 9e19255..0000000 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ModifyVm.java +++ /dev/null @@ -1,74 +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.jgrapes.core.Channel; -import org.jgrapes.core.Event; - -/** - * Modifies a VM. - */ -public class ModifyVm extends Event { - - private final String name; - private final String path; - private final Object value; - - /** - * Instantiates a new modify vm event. - * - * @param channels the channels - * @param name the name - */ - public ModifyVm(String name, String path, Object value, - Channel... channels) { - super(channels); - this.name = name; - this.path = path; - this.value = value; - } - - /** - * Gets the name. - * - * @return the name - */ - public String name() { - 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/ResetVm.java deleted file mode 100644 index 778820e..0000000 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ResetVm.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.manager.events; - -import org.jgrapes.core.Event; - -/** - * Triggers a reset of the VM. - */ -public class ResetVm extends Event { - - private final String vmName; - - /** - * Instantiates a new event. - * - * @param vmName the vm name - */ - public ResetVm(String vmName) { - this.vmName = vmName; - } - - /** - * Gets the vm name. - * - * @return the vm name - */ - public String vmName() { - return vmName; - } -} 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/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.events/src/org/jdrupes/vmoperator/manager/events/VmResourceChanged.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmResourceChanged.java deleted file mode 100644 index eac30fb..0000000 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmResourceChanged.java +++ /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 . - */ - -package org.jdrupes.vmoperator.manager.events; - -import org.jdrupes.vmoperator.common.K8sObserver; -import org.jdrupes.vmoperator.common.VmDefinition; -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. - */ -@SuppressWarnings("PMD.DataClass") -public class VmResourceChanged extends Event { - - private final K8sObserver.ResponseType type; - private final VmDefinition vmDefinition; - private final boolean specChanged; - private final boolean podChanged; - - /** - * Instantiates a new VM changed event. - * - * @param type the type - * @param vmDefinition the VM definition - * @param specChanged the spec part changed - */ - public VmResourceChanged(K8sObserver.ResponseType type, - VmDefinition vmDefinition, boolean specChanged, - boolean podChanged) { - this.type = type; - this.vmDefinition = vmDefinition; - this.specChanged = specChanged; - this.podChanged = podChanged; - } - - /** - * Returns the type. - * - * @return the type - */ - public K8sObserver.ResponseType type() { - return type; - } - - /** - * Return the VM definition. - * - * @return the VM definition - */ - public VmDefinition vmDefinition() { - return vmDefinition; - } - - /** - * Indicates if the "spec" part changed. - */ - public boolean specChanged() { - return specChanged; - } - - /** - * Indicates if the pod status changed. - */ - public boolean podChanged() { - return podChanged; - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append(Components.objectName(this)).append(" [") - .append(vmDefinition.name()).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/package-info.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/package-info.java deleted file mode 100644 index 7492a88..0000000 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/package-info.java +++ /dev/null @@ -1,25 +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 . - */ - -/** - * Domain specific {@link org.jgrapes.core.Event}s (and - * {@link org.jgrapes.core.Channel}s) used to communicate between - * the core components of the {@link org.jgrapes.core.Manager} and - * "plugin" components such as the conlets. - */ -package org.jdrupes.vmoperator.manager.events; \ No newline at end of file diff --git a/org.jdrupes.vmoperator.manager/.checkstyle b/org.jdrupes.vmoperator.manager/.checkstyle index 7f2c604..2add381 100644 --- a/org.jdrupes.vmoperator.manager/.checkstyle +++ b/org.jdrupes.vmoperator.manager/.checkstyle @@ -1,10 +1,10 @@ - + - + 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..f0d47ec 100644 --- a/org.jdrupes.vmoperator.manager/build.gradle +++ b/org.jdrupes.vmoperator.manager/build.gradle @@ -9,123 +9,69 @@ plugins { } dependencies { - implementation project(':org.jdrupes.vmoperator.manager.events') + 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.29.0,2)' + implementation project(':org.jdrupes.vmoperator.util') implementation 'commons-cli:commons-cli:1.5.0' + implementation 'org.freemarker:freemarker:[2.3.32,2.4)' + implementation 'io.kubernetes:client-java:[18.0.0,19)' - 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.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)' - - runtimeOnly 'org.jgrapes:org.jgrapes.webconlet.sysinfo:[1.4.0,2)' - runtimeOnly 'org.jgrapes:org.jgrapes.webconlet.logviewer:[0.2.0,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 'com.electronwill.night-config:yaml:3.6.6' 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') } application { applicationName = 'vm-manager' - applicationDefaultJvmArgs = ['-Xmx128m', '-XX:+UseParallelGC', + applicationDefaultJvmArgs = ['-Xmx50m', '-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 + + 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 - - 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 -} - -task pushForTest(type: Exec) { - dependsOn buildImage +task pushLatestImage(type: Exec) { + dependsOn buildLatestImage commandLine 'podman', 'push', '--tls-verify=false', \ - "${project.name}:${project.gitBranch}", \ - "${project.rootProject.properties['docker.testRegistry']}" \ - + "/${project.name}:test" + "localhost/${project.name}:${project.version}", \ + "${project.rootProject.properties['docker.registry']}" \ + + "/${project.name}:latest" } -test { - enabled = project.hasProperty("k8s.testCluster") - - if (enabled) { - dependsOn project.tasks["pushForTest"] - } - - useJUnitPlatform() - - testLogging { - showStandardStreams = true - } - - systemProperty "k8s.testCluster", project.hasProperty("k8s.testCluster") - ? project.getProperty("k8s.testCluster") : null +task pushImages { + dependsOn pushImage + dependsOn pushLatestImage } -// Update favicon: -// # Convert with inkscape to png because convert cannot handle svg -// # background transparency, then -// convert VM-Operator.png -background transparent \ -// -define icon:auto-resize=256,64,48,32,16 favicon.ico diff --git a/org.jdrupes.vmoperator.manager/config-sample.yaml b/org.jdrupes.vmoperator.manager/config-sample.yaml new file mode 100644 index 0000000..abc229f --- /dev/null +++ b/org.jdrupes.vmoperator.manager/config-sample.yaml @@ -0,0 +1,16 @@ +# The values in comments are the defaults. + +"/Manager": + "/Controller": + # Values used when creating the PVC for the runner's data + runnerData: + storageClassName: null + # Amount by which the current cpu count is devided when generating + # the resource properties. + cpuOvercommit: 2 + # Amount by which the current ram size is devided when generating + # the resource properties. + ramOvercommit: 1.5 + + # Only for development: + # namespace: vmop-dev diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/logging.properties b/org.jdrupes.vmoperator.manager/logging.properties similarity index 78% rename from org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/logging.properties rename to org.jdrupes.vmoperator.manager/logging.properties index 2a16af6..a771cbe 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 @@ -16,13 +16,13 @@ # with this program; if not, see . # -handlers=java.util.logging.ConsoleHandler, \ - org.jgrapes.webconlet.logviewer.LogViewerHandler +handlers=java.util.logging.ConsoleHandler -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 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 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/VM-Operator.png b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/VM-Operator.png deleted file mode 100644 index 9ecb022..0000000 Binary files a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/VM-Operator.png and /dev/null differ diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/VM-Operator.svg b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/VM-Operator.svg deleted file mode 100644 index c8616d5..0000000 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/VM-Operator.svg +++ /dev/null @@ -1,184 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 deleted file mode 100644 index 9c9de88..0000000 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/console-brand.ftl.html +++ /dev/null @@ -1,5 +0,0 @@ -${_("consoleTitle")}  - (<#if clusterName()??>${clusterName() + "/"}${ namespace() }) \ 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/favicon.ico b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/favicon.ico deleted file mode 100644 index 1b9443d..0000000 Binary files a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/favicon.ico and /dev/null differ 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 deleted file mode 100644 index 6bcc3a2..0000000 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/l10n.properties +++ /dev/null @@ -1,20 +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 . -# - -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..7aaae51 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: ${ cr.spec.vm.maximumCpus.asInt?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?? > + cpuSockets: ${ cr.spec.vm.cpuTopology.cpuSockets.asInt?c } + diesPerSocket: ${ cr.spec.vm.cpuTopology.diesPerSocket.asInt?c } + coresPerSocket: ${ cr.spec.vm.cpuTopology.coresPerSocket.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: ${ cr.spec.vm.currentCpus.asInt?c } # RAM settings # Maximum defaults to 1G - maximumRam: "${ formatMemory(parseQuantity(spec.vm.maximumRam)) }" - <#if spec.vm.currentRam?? > - currentRam: "${ formatMemory(parseQuantity(spec.vm.currentRam)) }" + maximumRam: "${ cr.spec.vm.maximumRam.asString }" + <#if cr.spec.vm.currentRam?? > + currentRam: "${ 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 deleted file mode 100644 index b7215a5..0000000 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerLoadBalancer.ftl.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - namespace: ${ cr.namespace() } - name: ${ cr.name() } - labels: - app.kubernetes.io/name: ${ constants.APP_NAME } - app.kubernetes.io/instance: ${ cr.name() } - 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() } - controller: false - -spec: - type: LoadBalancer - ports: - - name: spice - port: ${ cr.spec().vm.display.spice.port?c } - selector: - app.kubernetes.io/name: ${ constants.APP_NAME } - app.kubernetes.io/instance: ${ cr.name() } 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/runnerService.ftl.yaml b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerService.ftl.yaml new file mode 100644 index 0000000..e103cdb --- /dev/null +++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerService.ftl.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: Service +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 } + controller: false + +spec: + ports: + - name: spice + port: ${ cr.spec.vm.display.spice.port.asInt?c } + clusterIP: None + selector: + app.kubernetes.io/name: ${ cr.metadata.name.asString } + app.kubernetes.io/instance: ${ cr.metadata.name.asString } 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..707d35e --- /dev/null +++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerSts.ftl.yaml @@ -0,0 +1,176 @@ +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 } + 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 } + + 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 config.cpuOvercommit??> + <#assign factor = config.cpuOvercommit * 1.0 /> + + cpu: ${ (cr.spec.vm.currentCpus.asInt / factor)?floor?c } + + <#if cr.spec.vm.currentRam?? > + <#assign factor = 1.25 /> + <#if config.ramOvercommit??> + <#assign factor = config.ramOvercommit * 1.0 /> + + memory: ${ (parseMemory(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() } + + 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 config.runnerData?? && config.runnerData.storageClassName??> + storageClassName: ${ config.runnerData.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/CmReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/CmReconciler.java new file mode 100644 index 0000000..7b30213 --- /dev/null +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/CmReconciler.java @@ -0,0 +1,141 @@ +/* + * 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.ApiClient; +import io.kubernetes.client.openapi.ApiException; +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 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 org.jdrupes.vmoperator.manager.VmDefChanged.Type; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.SafeConstructor; + +/** + * Delegee for reconciling the config map + */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") +/* default */ class CmReconciler { + + protected final Logger logger = Logger.getLogger(getClass().getName()); + private final Configuration fmConfig; + + /** + * Instantiates a new config map reconciler. + * + * @param fmConfig the fm config + */ + public CmReconciler(Configuration fmConfig) { + this.fmConfig = fmConfig; + } + + /** + * Reconcile. + * + * @param event the event + * @param model the model + * @param channel the channel + * @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 + */ + public DynamicKubernetesObject reconcile(VmDefChanged event, + Map model, VmChannel channel) + throws IOException, TemplateException, ApiException { + // Get API and check if exists + DynamicKubernetesApi cmApi = new DynamicKubernetesApi("", "v1", + "configmaps", channel.client()); + var existing = K8s.get(cmApi, event.object().getMetadata()); + + // If deleted, delete + if (event.type() == Type.DELETED) { + if (existing.isPresent()) { + K8s.delete(cmApi, existing.get()); + } + return null; + } + + // Combine template and data and parse result + 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 mapDef = Dynamics.newFromYaml( + new Yaml(new SafeConstructor(new LoaderOptions())), out.toString()); + + // Apply and maybe force pod update + var newState = K8s.apply(cmApi, mapDef, out.toString()); + maybeForceUpdate(channel.client(), newState); + return newState; + } + + /** + * Triggers update of config map mounted in pod + * See https://ahmet.im/blog/kubernetes-secret-volumes-delay/ + * @param client + * + * @param newCm + */ + private void maybeForceUpdate(ApiClient client, + DynamicKubernetesObject newCm) { + ListOptions listOpts = new ListOptions(); + listOpts.setLabelSelector( + "app.kubernetes.io/managed-by=" + Constants.VM_OP_NAME + "," + + "app.kubernetes.io/name=" + Constants.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().size() == 0) { + return; + } + var pod = pods.getItems().get(0); + + // Patch pod annotation + PatchOptions patchOpts = new PatchOptions(); + patchOpts.setFieldManager("kubernetes-java-kubectl-apply"); + var podMeta = pod.getMetadata(); + var res = podApi.patch(podMeta.getNamespace(), podMeta.getName(), + V1Patch.PATCH_FORMAT_JSON_PATCH, + new V1Patch("[{\"op\": \"replace\", \"path\": " + + "\"/metadata/annotations/vmrunner.jdrupes.org~1cmVersion\", " + + "\"value\": \"" + newCm.getMetadata().getResourceVersion() + + "\"}]"), + patchOpts); + if (!res.isSuccess()) { + logger.warning( + () -> "Cannot patch pod annotations: " + res.getStatus()); + } + } + +} 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 deleted file mode 100644 index 0ca6312..0000000 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ConfigMapReconciler.java +++ /dev/null @@ -1,210 +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 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; -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.yaml.snakeyaml.LoaderOptions; -import org.yaml.snakeyaml.Yaml; -import org.yaml.snakeyaml.constructor.SafeConstructor; - -/** - * Delegee for reconciling the config map - */ -/* default */ class ConfigMapReconciler { - - protected final Logger logger = Logger.getLogger(getClass().getName()); - private final Configuration fmConfig; - - /** - * Instantiates a new config map reconciler. - * - * @param fmConfig the fm config - */ - public ConfigMapReconciler(Configuration fmConfig) { - this.fmConfig = fmConfig; - } - - /** - * Reconcile. - * - * @param model the model - * @param channel the channel - * @param modelChanged the model has changed - * @throws IOException Signals that an I/O exception has occurred. - * @throws TemplateException the template exception - * @throws ApiException the API exception - */ - public void reconcile(Map model, VmChannel channel, - boolean modelChanged) - throws IOException, TemplateException, ApiException { - // Check if an update is needed - var prevData = channel.associated(PrevData.class) - .orElseGet(() -> new PrevData(null, new HashMap<>())); - Object newInputs = model.get("loginRequestedFor"); - if (!modelChanged && Objects.equals(prevData.inputs, newInputs)) { - // Make added data available in new model - model.putAll(prevData.added); - return; - } - prevData = new PrevData(newInputs, prevData.added); - channel.setAssociated(PrevData.class, prevData); - - // Combine template and data and parse result - logger.fine(() -> "Create/update configmap " - + DataPath. 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( - 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) { - } - - /** - * Triggers update of config map mounted in pod - * See https://ahmet.im/blog/kubernetes-secret-volumes-delay/ - * @param client - * - * @param newCm - */ - private void maybeForceUpdate(ApiClient client, - DynamicKubernetesObject newCm) { - 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")); - // 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()) { - return; - } - var pod = pods.getItems().get(0); - - // Patch pod annotation - PatchOptions patchOpts = new PatchOptions(); - patchOpts.setFieldManager("kubernetes-java-kubectl-apply"); - var podMeta = pod.getMetadata(); - var res = podApi.patch(podMeta.getNamespace(), podMeta.getName(), - V1Patch.PATCH_FORMAT_JSON_PATCH, - new V1Patch("[{\"op\": \"replace\", \"path\": " - + "\"/metadata/annotations/vmrunner.jdrupes.org~1cmVersion\", " - + "\"value\": \"" + newCm.getMetadata().getResourceVersion() - + "\"}]"), - patchOpts); - if (!res.isSuccess()) { - logger.warning( - () -> "Cannot patch pod annotations: " + res.getStatus()); - } - } - - 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..d1482b6 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 @@ -21,7 +21,19 @@ package org.jdrupes.vmoperator.manager; /** * Some constants. */ -public class Constants extends org.jdrupes.vmoperator.common.Constants { +public class Constants { + + /** The Constant VM_OP_NAME. */ + public static final String VM_OP_NAME = "vm-operator"; + + /** The Constant VM_OP_GROUP. */ + public static final String VM_OP_GROUP = "vmoperator.jdrupes.org"; + + /** The Constant VM_OP_KIND_VM. */ + public static final String VM_OP_KIND_VM = "VirtualMachine"; + + /** 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..ce04b0f 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,6 @@ -FROM docker.io/eclipse-temurin:21-jre-alpine +FROM ghcr.io/graalvm/jdk-community:17 + +RUN microdnf install findutils && microdnf clean all 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..fe668cb 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,138 +18,28 @@ 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.openapi.Configuration; 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 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.util.events.ConfigurationUpdate; /** * Implements a controller as defined in the * [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 - * when they change. The latter handles the changes and reconciles the - * resources in the cluster. - * - * The controller itself supports a single configuration property: - * ```yaml - * "/Manager": - * "/Controller": - * namespace: vmop-dev - * ``` - * This may only be set when running the Manager (and thus the Controller) - * outside a container during development. - * - * ![Controller components](controller-components.svg) - * - * @startuml controller-components.svg - * skinparam component { - * BackGroundColor #FEFECE - * BorderColor #A80036 - * BorderThickness 1.25 - * BackgroundColor<> #F1F1F1 - * BorderColor<> #181818 - * BorderThickness<> 1 - * } - * - * [Controller] - * [Controller] *--> [VmWatcher] - * [Controller] *--> [Reconciler] - * @enduml */ 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)); - } - - /** - * Special handling of {@link ApiException} thrown by handlers. - * - * @param event the event - */ - @Handler(channels = Channel.class) - public void onHandlingError(HandlingError event) { - if (event.throwable() instanceof ApiException exc) { - logger.log(Level.WARNING, exc, - () -> "Problem accessing kubernetes: " + exc.getResponseBody()); - event.stop(); - } - } - - /** - * Configure the component. - * - * @param event the event - */ - @Handler - public void onConfigurationUpdate(ConfigurationUpdate event) { - event.structured(componentPath()).ifPresent(c -> { - if (c.containsKey("namespace")) { - namespace = (String) c.get("namespace"); - } - }); } /** @@ -165,164 +55,5 @@ public class Controller extends Component { // Make sure to use thread specific client // https://github.com/kubernetes-client/java/issues/100 Configuration.setDefaultApiClient(null); - - // Verify that a namespace has been configured - 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); - 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)); - return; - } - logger.config(() -> "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. - * - * @param event the event - * @throws ApiException the api exception - * @throws InterruptedException - */ - @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; - } - } - } - - 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. - * - * @param event the event - * @throws InterruptedException - */ - @Handler - @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") - public void onPoolChanged(VmPoolChanged event) throws InterruptedException { - if (!event.deleted()) { - return; - } - var vms = newEventPipeline() - .fire(new GetVms().assignedFrom(event.vmPool().name())).get(); - for (var vm : vms) { - vm.channel().fire(new UpdateAssignment(event.vmPool(), null)); - } - } - - /** - * 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; - }); - } } } 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.util/src/org/jdrupes/vmoperator/util/GsonPtr.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/GsonPtr.java similarity index 64% rename from org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/GsonPtr.java rename to org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/GsonPtr.java index c6fb101..6d9070c 100644 --- a/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/GsonPtr.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/GsonPtr.java @@ -16,23 +16,20 @@ * along with this program. If not, see . */ -package org.jdrupes.vmoperator.util; +package org.jdrupes.vmoperator.manager; import com.google.gson.JsonArray; 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; @@ -55,15 +52,14 @@ public class GsonPtr { /** * 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 String} denoted a non-existant member of a * {@link JsonObject}, a new member (of type {@link JsonObject} * is added. * * @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 +88,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 +105,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 +125,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 +160,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,21 +171,10 @@ 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); } - /** - * Returns the Integer value of the selected {@link JsonPrimitive}. - * - * @param selectors the selectors - * @return the as string - */ - public Optional getAsBigInteger(Object... selectors) { - return getAs(JsonPrimitive.class, selectors) - .map(JsonPrimitive::getAsBigInteger); - } - /** * Returns the Long value of the selected {@link JsonPrimitive}. * @@ -232,36 +182,10 @@ 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); } - /** - * Returns the boolean value of the selected {@link JsonPrimitive}. - * - * @param selectors the selectors - * @return the boolean - */ - public Optional getAsBoolean(Object... selectors) { - return getAs(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 +225,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 +272,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.manager/src/org/jdrupes/vmoperator/manager/K8s.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/K8s.java new file mode 100644 index 0000000..efc304e --- /dev/null +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/K8s.java @@ -0,0 +1,162 @@ +/* + * 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 io.kubernetes.client.common.KubernetesListObject; +import io.kubernetes.client.common.KubernetesObject; +import io.kubernetes.client.custom.V1Patch; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.models.V1ConfigMap; +import io.kubernetes.client.openapi.models.V1ConfigMapList; +import io.kubernetes.client.openapi.models.V1ObjectMeta; +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.options.DeleteOptions; +import io.kubernetes.client.util.generic.options.PatchOptions; +import java.util.Optional; + +/** + * Helpers for K8s API. + */ +@SuppressWarnings({ "PMD.ShortClassName", "PMD.UseUtilityClass" }) +public class K8s { + + /** + * Given a groupVersion, returns only the version. + * + * @param groupVersion the group version + * @return the string + */ + 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 an object from its metadata. + * + * @param the generic type + * @param the generic type + * @param api the api + * @param meta the meta + * @return the object + */ + public static + Optional + get(GenericKubernetesApi api, V1ObjectMeta meta) { + var response = api.get(meta.getNamespace(), meta.getName()); + if (response.isSuccess()) { + return Optional.of(response.getObject()); + } + 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(); + } + + /** + * Apply the given patch data. + * + * @param the generic type + * @param the generic type + * @param api the api + * @param existing the existing + * @param update the update + * @throws ApiException the api exception + */ + public static + T apply(GenericKubernetesApi api, T existing, String update) + throws ApiException { + PatchOptions opts = new PatchOptions(); + opts.setForce(true); + opts.setFieldManager("kubernetes-java-kubectl-apply"); + var response = api.patch(existing.getMetadata().getNamespace(), + existing.getMetadata().getName(), V1Patch.PATCH_FORMAT_APPLY_YAML, + new V1Patch(update), opts).throwsApiException(); + return response.getObject(); + } + +} 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 deleted file mode 100644 index a66b432..0000000 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/LoadBalancerReconciler.java +++ /dev/null @@ -1,193 +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 com.google.gson.Gson; -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.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.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 service - */ -/* default */ class LoadBalancerReconciler { - - private static final String LOAD_BALANCER_SERVICE = "loadBalancerService"; - private static final String METADATA - = V1APIService.SERIALIZED_NAME_METADATA; - private static final String LABELS = V1ObjectMeta.SERIALIZED_NAME_LABELS; - private static final String ANNOTATIONS - = V1ObjectMeta.SERIALIZED_NAME_ANNOTATIONS; - protected final Logger logger = Logger.getLogger(getClass().getName()); - private final Configuration fmConfig; - - /** - * Instantiates a new service reconciler. - * - * @param fmConfig the fm config - */ - public LoadBalancerReconciler(Configuration fmConfig) { - this.fmConfig = fmConfig; - } - - /** - * Reconcile. - * - * @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; - } - - // Check if to be generated - @SuppressWarnings({ "unchecked" }) - var lbsDef = 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)) { - 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)) { - 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); - // Avoid Yaml.load due to - // 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); - - // Apply - var svcStub = K8sV1ServiceStub - .get(client, vmDef.namespace(), vmDef.name()); - if (svcStub.apply(svcDef).isEmpty()) { - logger.warning( - () -> "Could not patch service for " + svcStub.name()); - } - } - - 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()); - - // 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))); - } - - private Map mergeReplace(Map dest, - Map src) { - if (src == null) { - return dest; - } - if (dest == null) { - dest = new LinkedHashMap<>(); - } else { - dest = new LinkedHashMap<>(dest); - } - for (var e : src.entrySet()) { - if (e.getValue() == null) { - dest.remove(e.getKey()); - continue; - } - dest.put(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); - } - for (var e : src.entrySet()) { - if (dest.containsKey(e.getKey())) { - continue; - } - dest.put(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..1dcd5c8 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,20 +18,10 @@ 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; -import java.net.InetSocketAddress; -import java.net.URI; -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,219 +31,45 @@ 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; import org.jgrapes.core.Components; -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.webconsole.base.BrowserLocalBackedKVStore; -import org.jgrapes.webconsole.base.ConletComponentFactory; -import org.jgrapes.webconsole.base.ConsoleWeblet; -import org.jgrapes.webconsole.base.KVStoreBasedConsolePolicy; -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")); + public Manager(CommandLine cmdLine) throws IOException { // Prepare component tree attach(new NioDispatcher()); attach(new FileSystemWatcher(channel())); attach(new Controller(channel())); // Configuration store with file in /etc/opt (default) - File cfgFile = new File(cmdLine.getOptionValue('c', + File config = new File(cmdLine.getOptionValue('c', "/etc/opt/" + VM_OP_NAME.replace("-", "") + "/config.yaml")); - 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); + if (!Files.isReadable(config.toPath())) { + throw new IOException("Cannot read configuration file " + config); } - attach(new YamlConfigurationStore(channel(), cfgFile, false)); - fire(new WatchFile(cfgFile.toPath()), channel()); - - // Prepare GUI - Channel httpTransport = new NamedChannel("guiTransport"); - attach(new SocketServer(httpTransport) - .setConnectionLimiter(new PermitsPool(300)) - .setMinimalPurgeableTime(1000) - .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. - HttpServer guiHttpServer = attach(new HttpServer(httpChannel, - httpTransport, Request.In.Get.class, Request.In.Post.class)); - guiHttpServer.setName("GuiHttpServer"); - - // Build HTTP application layer - guiHttpServer.attach(new InMemorySessionManager(httpChannel)); - guiHttpServer.attach(new LanguageSelector(httpChannel)); - URI rootUri; - try { - rootUri = new URI("/"); - } catch (URISyntaxException e) { - // Cannot happen - return; - } - ConsoleWeblet consoleWeblet = guiHttpServer - .attach(new VueJsConsoleWeblet(httpChannel, SELF, rootUri) { - @Override - protected Map createConsoleBaseModel() { - return augmentBaseModel(super.createConsoleBaseModel()); - } - }) - .prependClassTemplateLoader(getClass()) - .prependResourceBundleProvider(getClass()) - .prependConsoleResourceProvider(getClass()); - consoleWeblet.setName("ConsoleWeblet"); - WebConsole console = consoleWeblet.console(); - 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 -> { - if (LoginConlet.class.getName().equals(type)) { - // Explicitly added, see above - return Collections.emptyList(); - } else { - return Arrays.asList(Collections.emptyMap()); - } - })); - } - - 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. - * - * @param event the event - */ - @Handler(channels = Channel.class, priority = -10_000) - @SuppressWarnings("PMD.GuardLogStatement") - public void onHandlingError(HandlingError event) { - logger.log(Level.WARNING, event.throwable(), - () -> "Problem invoking handler with " + event.event() + ": " - + event.message()); - event.stop(); - } - - /** - * On exit. - * - * @param event the event - */ - @Handler - public void onExit(Exit event) { - exitStatus = event.exitStatus(); + attach(new YamlConfigurationStore(channel(), config, false)); + fire(new WatchFile(config.toPath())); } /** @@ -263,7 +79,7 @@ public class Manager extends Component { */ @Handler(priority = -1000) public void onStop(Stop event) { - logger.info(() -> "Application stopped."); + logger.fine(() -> "Application stopped."); } static { @@ -290,18 +106,11 @@ 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(() -> "running on " - + System.getProperty("java.vm.name") - + " (" + System.getProperty("java.vm.version") + ")" - + " from " + System.getProperty("java.vm.vendor")); + Logger.getLogger(Manager.class.getName()).fine(() -> "Version: " + + Manager.class.getPackage().getImplementationVersion()); // Parse the command line arguments CommandLineParser parser = new DefaultParser(); @@ -313,23 +122,19 @@ public class Manager extends Component { // The Manager is the root component app = new Manager(cmd); - // Prepare generation of Stop event + // Prepare Stop Runtime.getRuntime().addShutdownHook(new Thread(() -> { 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..b23a747 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,138 +18,49 @@ 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.SimpleNumber; -import freemarker.template.SimpleScalar; +import freemarker.template.DefaultObjectWrapperBuilder; +import freemarker.template.MalformedTemplateNameException; import freemarker.template.TemplateException; import freemarker.template.TemplateExceptionHandler; +import freemarker.template.TemplateHashModel; import freemarker.template.TemplateMethodModelEx; import freemarker.template.TemplateModelException; -import io.kubernetes.client.custom.Quantity; +import freemarker.template.TemplateNotFoundException; import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi; 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 static org.jdrupes.vmoperator.manager.Constants.VM_OP_GROUP; +import org.jdrupes.vmoperator.manager.VmDefChanged.Type; import org.jdrupes.vmoperator.util.ExtendedObjectWrapper; +import org.jdrupes.vmoperator.util.ParseUtils; import org.jgrapes.core.Channel; import org.jgrapes.core.Component; +import org.jgrapes.core.Components; import org.jgrapes.core.annotation.Handler; import org.jgrapes.util.events.ConfigurationUpdate; /** - * Adapts Kubenetes resources for instances of the Runner - * application (the VMs) to changes in VM definitions (the CRs). - * - * In particular, the reconciler generates and updates: - * - * * A [`PVC`](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) - * for storage used by all VMs as a common repository for CDROM images. - * - * * 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]. - * - * * (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 - * "/Manager": - * "/Controller": - * "/Reconciler": - * ... - * ``` - * - * The reconciler supports the following configuration properties: - * - * * `runnerDataPvc.storageClassName`: The storage class name - * to be used for the "runnerDataPvc" (the small volume used - * by the runner for information such as the EFI variables). By - * default, no `storageClassName` is generated, which causes - * Kubernetes to use storage from the default storage class. - * Define this if you want to use a specific storage class. - * - * * `cpuOvercommit`: The amount by which the current cpu count - * from the VM definition is divided when generating the - * [`resources`](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#resources) - * properties for the VM (defaults to 2). - * - * * `ramOvercommit`: The amount by which the current ram size - * from the VM definition is divided when generating the - * [`resources`](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#resources) - * properties for the VM (defaults to 1.25). - * - * * `loadBalancerService`: If defined, causes a load balancer service - * to be created. This property may be a boolean or - * YAML that defines additional labels or annotations to be merged - * into the service defintion. Here's an example for using - * [MetalLb](https://metallb.universe.tf/) as "internal load balancer": - * ```yaml - * loadBalancerService: - * 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 - * ``` - * 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 + * Adapts Kubenetes resources to changes in VM definitions (CRs). */ -@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 LoadBalancerReconciler lbReconciler; + private final CmReconciler cmReconciler; + private final StsReconciler stsReconciler; + private final ServiceReconciler serviceReconciler; @SuppressWarnings("PMD.UseConcurrentHashMap") private final Map config = new HashMap<>(); @@ -158,7 +69,6 @@ public class Reconciler extends Component { * * @param componentChannel the component channel */ - @SuppressWarnings("PMD.ConstructorCallsOverridableMethod") public Reconciler(Channel componentChannel) { super(componentChannel); @@ -172,23 +82,24 @@ public class Reconciler extends Component { fmConfig.setLogTemplateExceptions(false); fmConfig.setClassForTemplateLoading(Reconciler.class, ""); - cmReconciler = new ConfigMapReconciler(fmConfig); - dsReconciler = attach(new DisplaySecretReconciler(componentChannel)); - pvcReconciler = new PvcReconciler(fmConfig); - podReconciler = new PodReconciler(fmConfig); - lbReconciler = new LoadBalancerReconciler(fmConfig); + cmReconciler = new CmReconciler(fmConfig); + stsReconciler = new StsReconciler(fmConfig); + serviceReconciler = new ServiceReconciler(fmConfig); } /** - * Configures the component. + * Configure the component. * * @param event the event */ @Handler public void onConfigurationUpdate(ConfigurationUpdate event) { - event.structured(componentPath()).ifPresent(c -> { - config.putAll(c); - }); + event.structured(Components.manager(parent()).componentPath()) + .ifPresent(c -> { + if (c.containsKey("runnerData")) { + config.put("runnerData", c.get("runnerData")); + } + }); } /** @@ -197,207 +108,106 @@ 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 { + // Get complete VM (CR) definition + var apiVersion = K8s.version(event.object().getApiVersion()); + DynamicKubernetesApi vmCrApi = new DynamicKubernetesApi(VM_OP_GROUP, + apiVersion, event.crd().getName(), channel.client()); + var defMeta = event.object().getMetadata(); + + // Update the "buffered" definition, if it still exists. + if (event.type() != Type.DELETED) { + K8s.get(vmCrApi, defMeta).ifPresent(def -> channel + .setVmDefinition(patchCr(def.getRaw().deepCopy()))); + } + + // Reconcile + Map model = prepareModel(channel.vmDefinition()); + if (event.type() != Type.DELETED) { + var configMap = cmReconciler.reconcile(event, model, channel); + model.put("cm", configMap.getRaw()); + stsReconciler.reconcile(event, model, channel); + serviceReconciler.reconcile(event, model, channel); + } else { + serviceReconciler.reconcile(event, model, channel); + stsReconciler.reconcile(event, model, channel); + cmReconciler.reconcile(event, model, channel); + } } - private Map prepareModel(VmDefinition vmDef) - throws TemplateModelException, ApiException { + private Map prepareModel(JsonObject 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("reconciler", config); - model.put("constants", constantsMap(Constants.class)); - addLoginRequestedFor(model, vmDef); + model.put("constants", + (TemplateHashModel) new DefaultObjectWrapperBuilder( + Configuration.VERSION_2_3_32) + .build().getStaticModels() + .get(Constants.class.getName())); + model.put("config", config); // 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("parseMemory", 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(); + return ParseUtils.parseMemory(arg.toString()); } catch (NumberFormatException e) { throw new TemplateModelException("Cannot parse memory " + "specified as \"" + arg + "\": " + e.getMessage()); } } - }; + }); + return model; + } - private final TemplateMethodModelEx formatMemoryModel - = new TemplateMethodModelEx() { - @Override - public Object exec(@SuppressWarnings("rawtypes") List arguments) - throws TemplateModelException { - var arg = arguments.get(0); - if (arg instanceof SimpleNumber number) { - arg = number.getAsNumber(); - } - BigInteger bigInt; - if (arg instanceof BigInteger value) { - bigInt = value; - } else if (arg instanceof BigDecimal dec) { - try { - bigInt = dec.toBigIntegerExact(); - } catch (ArithmeticException e) { - return arg; - } - } else if (arg instanceof Integer value) { - bigInt = BigInteger.valueOf(value); - } else if (arg instanceof Long value) { - bigInt = BigInteger.valueOf(value); + private JsonObject patchCr(JsonObject vmDef) { + // Adjust cdromImage path + var disks + = GsonPtr.to(vmDef).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 { - return arg; + cdrom.addProperty("image", imageUri.toString()); } - return Convertions.formatMemory(bigInt); + } catch (URISyntaxException e) { + logger.warning(() -> "Invalid CDROM image: " + image); } - }; + } + return vmDef; + } - 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/ServiceReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ServiceReconciler.java new file mode 100644 index 0000000..0a1c6f9 --- /dev/null +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ServiceReconciler.java @@ -0,0 +1,92 @@ +/* + * 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.DynamicKubernetesApi; +import io.kubernetes.client.util.generic.dynamic.Dynamics; +import java.io.IOException; +import java.io.StringWriter; +import java.util.Map; +import java.util.logging.Logger; +import org.jdrupes.vmoperator.manager.VmDefChanged.Type; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.SafeConstructor; + +/** + * Delegee for reconciling the service + */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") +/* default */ class ServiceReconciler { + + protected final Logger logger = Logger.getLogger(getClass().getName()); + private final Configuration fmConfig; + + /** + * Instantiates a new service reconciler. + * + * @param fmConfig the fm config + */ + public ServiceReconciler(Configuration fmConfig) { + this.fmConfig = fmConfig; + } + + /** + * Reconcile. + * + * @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 { + // Get API and check if exists + DynamicKubernetesApi svcApi = new DynamicKubernetesApi("", "v1", + "services", channel.client()); + var existing = K8s.get(svcApi, event.object().getMetadata()); + + // If deleted, delete + if (event.type() == Type.DELETED) { + if (existing.isPresent()) { + K8s.delete(svcApi, existing.get()); + } + return; + } + + // Combine template and data and parse result + var fmTemplate = fmConfig.getTemplate("runnerService.ftl.yaml"); + StringWriter out = new StringWriter(); + fmTemplate.process(model, out); + // Avoid Yaml.load due to + // https://github.com/kubernetes-client/java/issues/2741 + var mapDef = Dynamics.newFromYaml( + new Yaml(new SafeConstructor(new LoaderOptions())), out.toString()); + + // Apply + K8s.apply(svcApi, mapDef, out.toString()); + } + +} diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/StsReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/StsReconciler.java new file mode 100644 index 0000000..a2fb7f1 --- /dev/null +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/StsReconciler.java @@ -0,0 +1,120 @@ +/* + * 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.manager.VmDefChanged.Type; +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 StsReconciler { + + protected final Logger logger = Logger.getLogger(getClass().getName()); + private final Configuration fmConfig; + + /** + * Instantiates a new config map reconciler. + * + * @param fmConfig the fm config + */ + public StsReconciler(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.object().getMetadata(); + + // Maybe delete + if (event.type() == Type.DELETED) { + // First set replicas to 0 ... + PatchOptions opts = new PatchOptions(); + opts.setFieldManager("kubernetes-java-kubectl-apply"); + stsApi.patch(metadata.getNamespace(), metadata.getName(), + V1Patch.PATCH_FORMAT_JSON_PATCH, + new V1Patch("[{\"op\": \"replace\", \"path\": " + + "\"/spec/replicas\", \"value\": 0}]"), + opts).throwsApiException(); + // ... then delete + stsApi.delete(metadata.getNamespace(), metadata.getName()) + .throwsApiException(); + return; + } + + // 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.events/src/org/jdrupes/vmoperator/manager/events/VmChannel.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmChannel.java similarity index 56% rename from org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmChannel.java rename to org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmChannel.java index 73507ae..4336ca8 100644 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmChannel.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmChannel.java @@ -16,12 +16,11 @@ * along with this program. If not, see . */ -package org.jdrupes.vmoperator.manager.events; +package org.jdrupes.vmoperator.manager; -import org.jdrupes.vmoperator.common.K8sClient; -import org.jdrupes.vmoperator.common.VmDefinition; +import com.google.gson.JsonObject; +import io.kubernetes.client.openapi.ApiClient; import org.jgrapes.core.Channel; -import org.jgrapes.core.Event; import org.jgrapes.core.EventPipeline; import org.jgrapes.core.Subchannel.DefaultSubchannel; @@ -31,9 +30,8 @@ import org.jgrapes.core.Subchannel.DefaultSubchannel; public class VmChannel extends DefaultSubchannel { private final EventPipeline pipeline; - private final K8sClient client; - private VmDefinition definition; - private long generation = -1; + private final ApiClient client; + private JsonObject vmDefinition; /** * Instantiates a new watch channel. @@ -43,7 +41,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,42 +53,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(JsonObject 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; - } - - /** - * Gets the last processed generation. Returns -1 if no - * definition has been processed yet. - * - * @return the generation - */ - public long generation() { - return generation; - } - - /** - * Sets the last processed generation. - * - * @param generation the generation to set - * @return true if value has changed - */ - public boolean setGeneration(long generation) { - if (this.generation == generation) { - return false; - } - this.generation = generation; - return true; + public JsonObject vmDefinition() { + return vmDefinition; } /** @@ -102,25 +77,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/PodChanged.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmDefChanged.java similarity index 55% rename from org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/PodChanged.java rename to org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmDefChanged.java index 8bbcfe8..dd6f7db 100644 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/PodChanged.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmDefChanged.java @@ -16,40 +16,41 @@ * along with this program. If not, see . */ -package org.jdrupes.vmoperator.manager.events; +package org.jdrupes.vmoperator.manager; -import io.kubernetes.client.openapi.models.V1Pod; -import org.jdrupes.vmoperator.common.K8sObserver; +import io.kubernetes.client.openapi.models.V1APIResource; +import io.kubernetes.client.openapi.models.V1Namespace; 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. + * Indicates a change in a VM definition. */ -public class PodChanged extends Event { +public class VmDefChanged extends Event { - private final V1Pod pod; - private final K8sObserver.ResponseType type; + /** + * The type of change. + */ + public enum Type { + ADDED, MODIFIED, DELETED + } + + private final Type type; + private final V1APIResource crd; + private final V1Namespace object; /** * Instantiates a new VM changed event. * - * @param pod the pod * @param type the type + * @param crd the crd + * @param object the object */ - public PodChanged(V1Pod pod, K8sObserver.ResponseType type) { - this.pod = pod; + public VmDefChanged(Type type, V1APIResource crd, V1Namespace object) { this.type = type; - } - - /** - * Gets the pod. - * - * @return the pod - */ - public V1Pod pod() { - return pod; + this.crd = crd; + this.object = object; } /** @@ -57,17 +58,36 @@ public class PodChanged extends Event { * * @return the type */ - public K8sObserver.ResponseType type() { + public Type type() { return type; } + /** + * Returns the Crd. + * + * @return the v 1 API resource + */ + public V1APIResource crd() { + return crd; + } + + /** + * Returns the object. + * + * @return the object. + */ + public V1Namespace object() { + return object; + } + @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append(Components.objectName(this)).append(" [") - .append(pod.getMetadata().getName()).append(' ').append(type); + .append(object.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/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..f20c52e --- /dev/null +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmWatcher.java @@ -0,0 +1,264 @@ +/* + * 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.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.manager.Constants.VM_OP_GROUP; +import static org.jdrupes.vmoperator.manager.Constants.VM_OP_KIND_VM; +import org.jdrupes.vmoperator.manager.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); + } + } + if (namespaceToWatch == null) { + logger.severe(() -> "Namespace to watch not configured and" + + " no file in kubernetes directory."); + event.cancel(true); + fire(new Stop()); + return; + } + logger + .fine(() -> "Controlling 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 -> Constants.VM_OP_KIND_VM.equals(r.getKind())) + .findFirst() + .ifPresent(crd -> watchVmDefs(crd, version)); + } + } + + @SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops", + "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 -> Constants.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=" + Constants.VM_OP_NAME + "," + + "app.kubernetes.io/name=" + Constants.APP_NAME); + for (String resource : List.of("apps/v1/statefulsets", + "v1/configmaps", "v1/secrets")) { + 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") + var watcher = new Thread(() -> { + try { + // Watch sometimes terminates without apparent reason. + while (true) { + 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 (IllegalStateException e) { + logger.log(Level.FINE, e, () -> "Probem watching " + + "(retrying): " + e.getMessage()); + } + } + } catch (IOException | ApiException e) { + logger.log(Level.SEVERE, e, () -> "Probem watching: " + + e.getMessage()); + } + fire(new Stop()); + }); + watcher.setDaemon(true); + watcher.start(); + } + + 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; + } + }); + channel.pipeline().fire(new VmDefChanged(VmDefChanged.Type + .valueOf(item.type), vmsCrd, item.object), 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.object().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..6de13e3 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 @@ -16,179 +16,4 @@ * along with this program. If not, see . */ -/** - * The following diagram shows the components of the manager application. - * - * In framework terms, the {@link org.jdrupes.vmoperator.manager.Manager} - * is the root component of the application. Two of its child components, - * the {@link org.jdrupes.vmoperator.manager.Controller} and the WebGui - * provide the functions that are apparent to the user. - * - * The position of the components in the component tree is important - * when writing the configuration file for the manager and therefore - * shown below. In order to keep the diagram readable, the - * components attached to the - * {@link org.jgrapes.webconsole.base.WebConsole} are shown in a - * separate diagram further down. - * - * ![Manager components](manager-components.svg) - * - * Component hierarchy of the web console: - * - * ![Web console components](console-components.svg) - * - * The components marked as "<<internal>>" have no - * configuration options or use their default values when used - * in this application. - * - * As an example, the following YAML configures a different port for the - * GUI and some users. The relationship to the component tree should - * be obvious. - * ``` - * "/Manager": - * "/GuiSocketServer": - * port: 8888 - * "/GuiHttpServer": - * "/ConsoleWeblet": - * "/WebConsole": - * "/LoginConlet": - * users: - * ... - * ``` - * - * Developers may also be interested in the usage of channels - * by the application's components: - * - * ![Main channels](app-channels.svg) - * - * @startuml manager-components.svg - * skinparam component { - * BackGroundColor #FEFECE - * BorderColor #A80036 - * BorderThickness 1.25 - * BackgroundColor<> #F1F1F1 - * BorderColor<> #181818 - * BorderThickness<> 1 - * } - * skinparam packageStyle rectangle - * - * 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 <> - * [Manager] *-left- [YamlConfigurationStore] - * [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] -right[hidden]- [GuiHttpServer] - * - * [Manager] *-down- [GuiSocketServer:8080] - * [Manager] *-- [GuiHttpServer] - * Component PreferencesStore as PreferencesStore <> - * [GuiHttpServer] *-up- [PreferencesStore] - * Component InMemorySessionManager as InMemorySessionManager <> - * [GuiHttpServer] *-up- [InMemorySessionManager] - * Component LanguageSelector as LanguageSelector <> - * [GuiHttpServer] *-right- [LanguageSelector] - * - * package "Conceptual WebConsole" { - * [ConsoleWeblet] *-- [WebConsole] - * } - * [GuiHttpServer] *-- [ConsoleWeblet] - * @enduml - * - * @startuml console-components.svg - * skinparam component { - * BackGroundColor #FEFECE - * BorderColor #A80036 - * BorderThickness 1.25 - * BackgroundColor<> #F1F1F1 - * BorderColor<> #181818 - * BorderThickness<> 1 - * } - * skinparam packageStyle rectangle - * - * Component BrowserLocalBackedKVStore as BrowserLocalBackedKVStore <> - * [WebConsole] *-up- [BrowserLocalBackedKVStore] - * Component KVStoreBasedConsolePolicy as KVStoreBasedConsolePolicy <> - * [WebConsole] *-up- [KVStoreBasedConsolePolicy] - * - * [WebConsole] *-- [RoleConfigurator] - * [WebConsole] *-- [RoleConletFilter] - * [WebConsole] *-left- [LoginConlet] - * [WebConsole] *-right- [OidcClient] - * - * Component "ComponentCollector\nfor page resources" as cpr <> - * [WebConsole] *-- [cpr] - * Component "ComponentCollector\nfor conlets" as cc <> - * [WebConsole] *-- [cc] - * - * package "Providers and Conlets" { - * [Some component] - * } - * - * [cpr] *-- [Some component] - * [cc] *-- [Some component] - * @enduml - * - * @startuml app-channels.svg - * skinparam packageStyle rectangle - * - * () "manager" as mgr - * 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 - * [InMemorySessionManager] .up. http - * - * package "Conceptual WebConsole" { - * [ConsoleWeblet] .right. 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; +package org.jdrupes.vmoperator.manager; \ No newline at end of file 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/org/jdrupes/vmoperator/manager/BasicTests.java b/org.jdrupes.vmoperator.manager/test/org/jdrupes/vmoperator/manager/BasicTests.java deleted file mode 100644 index d600d3c..0000000 --- a/org.jdrupes.vmoperator.manager/test/org/jdrupes/vmoperator/manager/BasicTests.java +++ /dev/null @@ -1,341 +0,0 @@ -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 java.io.IOException; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.List; -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; -import org.junit.jupiter.api.Test; -import org.yaml.snakeyaml.LoaderOptions; -import org.yaml.snakeyaml.Yaml; -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(); - - @BeforeAll - static void setUpBeforeClass() throws Exception { - var testCluster = System.getProperty("k8s.testCluster"); - 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(); - - // Context for working with our CR - var apiRes = K8s.context(client, Crd.GROUP, null, Crd.KIND_VM); - assertTrue(apiRes.isPresent()); - vmsContext = apiRes.get(); - - // 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(); - - // Load from Yaml - var rdr = new FileReader("test-resources/basic-vm.yaml"); - vmStub = K8sDynamicStub.createFromYaml(client, vmsContext, rdr); - assertTrue(vmStub.model().isPresent()); - } - - private static void waitForManager() - throws ApiException, InterruptedException { - // Wait until available - for (int i = 0; i < 10; i++) { - if (mgrDeployment.model().get().getStatus().getConditions() - .stream().filter(c -> "Available".equals(c.getType())).findAny() - .isPresent()) { - return; - } - Thread.sleep(1000); - } - 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); - } - - @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); - } - // 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); - - 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); - } - - @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; - for (int i = 0; i < 10; i++) { - secrets = K8sV1SecretStub.list(client, "vmop-test", listOpts); - if (secrets.size() > 0) { - break; - } - 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"))); - } - - @Test - void testRunnerPvc() throws ApiException, InterruptedException { - var stub - = K8sV1PvcStub.get(client, "vmop-test", VM_NAME + "-runner-data"); - 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("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()); - } - } - -} diff --git a/org.jdrupes.vmoperator.runner.qemu/.checkstyle b/org.jdrupes.vmoperator.runner.qemu/.checkstyle index 7f2c604..2add381 100644 --- a/org.jdrupes.vmoperator.runner.qemu/.checkstyle +++ b/org.jdrupes.vmoperator.runner.qemu/.checkstyle @@ -1,10 +1,10 @@ - + - + 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..ff9a257 100644 --- a/org.jdrupes.vmoperator.runner.qemu/build.gradle +++ b/org.jdrupes.vmoperator.runner.qemu/build.gradle @@ -9,121 +9,69 @@ 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 project(':org.jdrupes.vmoperator.common') + 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.29.0,2)' + implementation project(':org.jdrupes.vmoperator.util') implementation 'commons-cli:commons-cli:1.5.0' - implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:[2.16.1]' - - runtimeOnly 'org.slf4j:slf4j-jdk14:[2.0.7,3)' + implementation 'org.freemarker:freemarker:[2.3.32,2.4)' + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:[2.15.1,3]' + + runtimeOnly 'com.electronwill.night-config:yaml:3.6.6' } application { applicationName = 'vm-runner.qemu' // Keep sync'd with deploy/vmop-deployment.yaml - applicationDefaultJvmArgs = ['-Xmx32m', '-XX:+UseParallelGC', + applicationDefaultJvmArgs = ['-Xmx32m', '-Djava.util.logging.manager=org.jdrupes.vmoperator.util.LongLoggingManager' ] // Define the main class for the 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}:${project.version}",\ '-f', 'src/org/jdrupes/vmoperator/runner/qemu/Containerfile.arch', '.' } -task pushImageArch(type: Exec) { - dependsOn buildImageArch +task tagLatestArchImage(type: Exec) { + dependsOn buildArchImage + + commandLine 'podman', 'tag', "${project.name}:${project.version}",\ + "${project.name}: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}:${project.version}", \ + "${project.rootProject.properties['docker.registry']}" \ + + "/${project.name}-arch:${project.version}" } -task tagWithVersionArch(type: Exec) { - dependsOn pushImageArch - - enabled = !rootVersion.contains("SNAPSHOT") - - commandLine 'podman', 'push', \ - "${project.name}-arch:${project.gitBranch}",\ - "${registry}/${project.name}-arch:${project.version}" -} - -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) { - dependsOn installDist - inputs.files 'src/org/jdrupes/vmoperator/runner/qemu/Containerfile.alpine' - - commandLine 'podman', 'build', '--pull', - '-t', "${project.name}-alpine:${project.gitBranch}",\ - '-f', 'src/org/jdrupes/vmoperator/runner/qemu/Containerfile.alpine', '.' -} - -task pushImageAlpine(type: Exec) { - dependsOn buildImageAlpine +task pushArchLatestImage(type: Exec) { + dependsOn buildLatestArchImage commandLine 'podman', 'push', '--tls-verify=false', \ - "localhost/${project.name}-alpine:${project.gitBranch}", \ - "${registry}/${project.name}-alpine:${project.gitBranch}" + "localhost/${project.name}:${project.version}", \ + "${project.rootProject.properties['docker.registry']}" \ + + "/${project.name}-arch:latest" } -task tagWithVersionAlpine(type: Exec) { - dependsOn pushImageAlpine - - enabled = !rootVersion.contains("SNAPSHOT") - - commandLine 'podman', 'push', \ - "${project.name}-alpine:${project.gitBranch}",\ - "${registry}/${project.name}-alpine:${project.version}" +task pushImages { + dependsOn pushArchImage + dependsOn pushArchLatestImage } -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 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..3e18871 100644 --- a/org.jdrupes.vmoperator.runner.qemu/config-sample.yaml +++ b/org.jdrupes.vmoperator.runner.qemu/config-sample.yaml @@ -22,36 +22,6 @@ # the first time. Subsequent starts use the copy unless this option is set. # "updateTemplate": false - # The namespace that this runner runs in. Usually obtained from - # /var/run/secrets/kubernetes.io/serviceaccount/namespace. Should only - # be set when starting the runner during development e.g. from the IDE. - # "namespace": ... - - # 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..70de565 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 @@ -7,17 +7,9 @@ "rom": - "/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..0ecde1f 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,29 +24,22 @@ 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; +import org.jdrupes.vmoperator.util.ParseUtils; /** * 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. */ @@ -140,7 +106,7 @@ public class Configuration implements Dto { public int currentCpus = 1; /** The cpu sockets. */ - public int sockets; + public int cpuSockets; /** The dies per socket. */ public int diesPerSocket; @@ -178,7 +144,7 @@ public class Configuration implements Dto { * @param value the new maximum ram */ public void setMaximumRam(String value) { - maximumRam = Convertions.parseMemory(value); + maximumRam = ParseUtils.parseMemory(value); } /** @@ -187,14 +153,13 @@ public class Configuration implements Dto { * @param value the new current ram */ public void setCurrentRam(String value) { - currentRam = Convertions.parseMemory(value); + currentRam = ParseUtils.parseMemory(value); } } /** * 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 deleted file mode 100644 index d0104f3..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Containerfile.alpine +++ /dev/null @@ -1,12 +0,0 @@ -FROM docker.io/alpine - -RUN apk update - -RUN apk add qemu-system-x86_64 qemu-modules ovmf swtpm openjdk21 mtools - -RUN mkdir -p /etc/qemu && echo "allow all" > /etc/qemu/bridge.conf - -COPY build/install/vm-runner.qemu /opt/vmrunner -COPY templates/* /opt/vmrunner/templates/ - -CMD ["/opt/vmrunner/bin/vm-runner.qemu"] 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..b81cace 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 systemd-firstboot --setup-machine-id -RUN pacman-key --init \ - && pacman -Sy --noconfirm archlinux-keyring && pacman -Su --noconfirm \ - && pacman -S --noconfirm which qemu-full virtiofsd \ - edk2-ovmf swtpm iproute2 bridge-utils jre21-openjdk-headless \ - mtools \ +RUN pacman -Suy --noconfirm \ + && pacman -S --noconfirm which qemu-base virtiofsd \ + 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..512db1b 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 @@ -19,21 +19,20 @@ package org.jdrupes.vmoperator.runner.qemu; import com.fasterxml.jackson.databind.node.ObjectNode; +import java.util.ArrayList; 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) @@ -81,28 +81,29 @@ public class CpuController extends Component { /** * On monitor result. * - * @param event the result + * @param result the result */ @Handler - public void onHotpluggableCpuStatus(HotpluggableCpuStatus event) { - if (!event.successful()) { - logger.warning(() -> "Failed to get hotpluggable CPU status " - + "(won't adjust number of CPUs.): " + event.errorMessage()); + public void onHotpluggableCpuStatus(HotpluggableCpuStatus result) { + // Sort + List used = new ArrayList<>(); + List unused = new ArrayList<>(); + for (var itr = result.values().iterator(); itr.hasNext();) { + ObjectNode cpu = (ObjectNode) itr.next(); + if (cpu.has("qom-path")) { + used.add(cpu); + } else { + unused.add(cpu); + } } + currentCpus = used.size(); if (desiredCpus == null) { return; } // Process - currentCpus = event.usedCpus().size(); - int diff = currentCpus - desiredCpus; - if (diff == 0) { - return; - } - diff = addCpus(event.usedCpus(), event.unusedCpus(), diff); - removeCpus(event.usedCpus(), diff); - - // Report result - fire(new MonitorCommand(new QmpQueryHotpluggableCpus())); + int diff = used.size() - desiredCpus; + diff = addCpus(used, unused, diff); + deleteCpus(used, diff); } @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") @@ -117,24 +118,22 @@ public class CpuController extends Component { } } int nextId = 1; - List remaining = new LinkedList<>(unused); - while (diff < 0 && !remaining.isEmpty()) { + while (diff < 0 && !unused.isEmpty()) { String id; do { id = "cpu-" + nextId++; } while (usedIds.contains(id)); - fire(new MonitorCommand(new QmpAddCpu(remaining.get(0), id))); - remaining.remove(0); + fire(new MonitorCommand(new QmpAddCpu(unused.get(0), id))); + unused.remove(0); diff += 1; } return diff; } @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") - private int removeCpus(List used, int diff) { - List removable = new LinkedList<>(used); - while (diff > 0 && !removable.isEmpty()) { - ObjectNode cpu = removable.remove(0); + private int deleteCpus(List used, int diff) { + while (diff > 0 && !used.isEmpty()) { + ObjectNode cpu = used.remove(0); String qomPath = cpu.get("qom-path").asText(); if (!qomPath.startsWith("/machine/peripheral/cpu-")) { continue; @@ -170,7 +169,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..25a5872 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,47 +38,34 @@ 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.MonitorReady; +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; import org.jgrapes.core.Component; import org.jgrapes.core.Components; import org.jgrapes.core.EventPipeline; import org.jgrapes.core.TypedIdKey; import org.jgrapes.core.annotation.Handler; -import org.jgrapes.core.events.HandlingError; import org.jgrapes.core.events.Start; import org.jgrapes.core.events.Started; import org.jgrapes.core.events.Stop; -import org.jgrapes.core.internal.EventProcessor; import org.jgrapes.io.NioDispatcher; import org.jgrapes.io.events.Input; import org.jgrapes.io.events.ProcessExited; @@ -104,13 +90,6 @@ import org.jgrapes.util.events.WatchFile; * * ![Runner state diagram](RunnerStates.svg) * - * The {@link Runner} associates an {@link EventProcessor} with the - * {@link Start} event. This "runner event processor" must be used - * for all events related to the application level function. Components - * that handle events from other sources (and thus event processors) - * must fire any resulting events on the runner event processor in order - * to maintain synchronization. - * * @startuml RunnerStates.svg * [*] --> Initializing * Initializing -> Initializing: InitialConfiguration/configure Runner @@ -118,54 +97,35 @@ 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 "Configure" 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] * * monitor: entry/fire OpenSocketConnection - * monitor --> waitForConfigured: ClientConnected[for monitor] + * monitor --> configure: ClientConnected[for monitor] * monitor -> error: ConnectError[for monitor] - * - * 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 <> @@ -189,58 +149,35 @@ import org.jgrapes.util.events.WatchFile; * error --> terminate * StartingProcess --> terminate: ProcessExited * - * state Stopped { - * state stopped <> * - * stopped --> [*] - * } - * - * terminated --> stopped + * terminated --> [*] * * @enduml * */ @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 +185,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 +194,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,107 +210,72 @@ 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(new StatusUpdater(channel())); - attach(new YamlConfigurationStore(channel(), configFile, false)); - fire(new WatchFile(configFile.toPath())); + attach(qemuMonitor = new QemuMonitor(channel())); + + // 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())); } /** - * Log the exception when a handling error is reported. - * - * @param event the event - */ - @Handler(channels = Channel.class, priority = -10_000) - @SuppressWarnings("PMD.GuardLogStatement") - public void onHandlingError(HandlingError event) { - logger.log(Level.WARNING, event.throwable(), - () -> "Problem invoking handler with " + event.event() + ": " - + event.message()); - event.stop(); - } - - /** - * 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 +286,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 +299,7 @@ public class Runner extends Component { } } - private JsonNode dataFromTemplate(Configuration config) + private JsonNode dataFromTemplate() throws IOException, TemplateNotFoundException, MalformedTemplateNameException, ParseException, TemplateException, JsonProcessingException, JsonMappingException { @@ -426,9 +311,6 @@ public class Runner extends Component { .ofNullable(config.template).orElse(DEFAULT_TEMPLATE)); Files.deleteIfExists(templatePath); Files.copy(sourcePath, templatePath); - logger.fine(() -> "Using template " + sourcePath); - } else { - logger.fine(() -> "Using saved template."); } // Configure data model @@ -439,35 +321,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,35 +342,31 @@ 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()); return; } - // Make sure to use thread specific client - // https://github.com/kubernetes-client/java/issues/100 - io.kubernetes.client.openapi.Configuration.setDefaultApiClient(null); - - // Provide 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 +383,20 @@ public class Runner extends Component { */ @Handler public void onStarted(Started event) { - state = RunState.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) { + state = State.STARTING; + fire(new RunnerStateChange(state)); + // 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 +410,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 +426,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,50 +467,26 @@ 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 onMonitorReady(MonitorReady event) { + 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())); - 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.")); + state = State.RUNNING; + fire(new RunnerStateChange(state)); } } @@ -710,69 +499,35 @@ 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. * * @param event the event */ @Handler(priority = 10_000) - public void onStopFirst(Stop event) { - state = RunState.TERMINATING; - rep.fire(new RunnerStateChange(state, "VmTerminating", - "The VM is being shut down", exitStatus != 0)); - } - - /** - * On stop. - * - * @param event the event - */ - @Handler(priority = -10_000) - public void onStopLast(Stop event) { - state = RunState.STOPPED; - rep.fire(new RunnerStateChange(state, "VmStopped", - "The VM has been shut down")); + public void onStop(Stop event) { + state = State.TERMINATING; + fire(new RunnerStateChange(state)); } private void shutdown() { - if (!Set.of(RunState.TERMINATING, RunState.STOPPED).contains(state)) { + if (state != State.TERMINATING) { fire(new Stop()); } try { @@ -781,7 +536,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 +552,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 +560,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(); } @@ -822,12 +573,9 @@ public class Runner extends Component { public static void main(String[] args) { // The Runner is the root component try { - var logger = Logger.getLogger(Runner.class.getName()); - logger.fine(() -> "Version: " - + Runner.class.getPackage().getImplementationVersion()); - logger.fine(() -> "running on " + System.getProperty("java.vm.name") - + " (" + System.getProperty("java.vm.version") + ")" - + " from " + System.getProperty("java.vm.vendor")); + Logger.getLogger(Runner.class.getName()) + .fine(() -> "Version: " + + Runner.class.getPackage().getImplementationVersion()); CommandLineParser parser = new DefaultParser(); // parse the command line arguments final Options options = new Options(); @@ -843,11 +591,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 deleted file mode 100644 index 127c070..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java +++ /dev/null @@ -1,449 +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 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 java.io.IOException; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.time.Instant; -import java.util.Optional; -import java.util.logging.Level; -import static org.jdrupes.vmoperator.common.Constants.APP_NAME; -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 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.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.util.GsonPtr; -import org.jgrapes.core.Channel; -import org.jgrapes.core.Components; -import org.jgrapes.core.Components.Timer; -import org.jgrapes.core.annotation.Handler; -import org.jgrapes.core.events.HandlingError; -import org.jgrapes.core.events.Start; - -/** - * Updates the CR status. - */ -@SuppressWarnings({ "PMD.CouplingBetweenObjects" }) -public class StatusUpdater extends VmDefUpdater { - - @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 boolean guestShutdownStops; - private boolean shutdownByGuest; - private VmDefinitionStub vmStub; - private String loggedInUser; - private BigInteger lastRamValue; - private Instant lastRamChange; - private Timer balloonTimer; - private BigInteger targetRamValue; - - /** - * Instantiates a new status updater. - * - * @param componentChannel the component channel - */ - public StatusUpdater(Channel componentChannel) { - super(componentChannel); - attach(new ConsoleTracker(componentChannel)); - } - - /** - * On handling error. - * - * @param event the event - */ - @Handler(channels = Channel.class) - public void onHandlingError(HandlingError event) { - if (event.throwable() instanceof ApiException exc) { - logger.log(Level.WARNING, exc, - () -> "Problem accessing kubernetes: " + exc.getResponseBody()); - event.stop(); - } - } - - /** - * 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); - var vmDef = vmStub.model().orElse(null); - if (vmDef == null) { - return; - } - 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)); - } - } - - /** - * On runner configuration update. - * - * @param event the event - * @throws ApiException - */ - @Handler - public void onConfigureQemu(ConfigureQemu event) - throws ApiException { - guestShutdownStops = event.configuration().guestShutdownStops; - loggedInUser = event.configuration().vm.display.loggedInUser; - targetRamValue = event.configuration().vm.currentRam; - - // Remainder applies only if we have a connection to k8s. - if (vmStub == null) { - return; - } - vmStub.updateStatus(from -> { - JsonObject status = from.statusJson(); - if (!event.configuration().hasDisplayPassword) { - status.addProperty(Status.DISPLAY_PASSWORD_SERIAL, -1); - } - status.getAsJsonArray("conditions").asList().stream() - .map(cond -> (JsonObject) cond) - .filter(cond -> Condition.RUNNING - .equals(cond.get("type").getAsString())) - .forEach(cond -> cond.addProperty("observedGeneration", - from.getMetadata().getGeneration())); - updateUserLoggedIn(from); - return status; - }); - } - - /** - * On runner state changed. - * - * @param event the event - * @throws ApiException - */ - @Handler - @SuppressWarnings({ "PMD.AssignmentInOperand" }) - public void onRunnerStateChanged(RunnerStateChange event) - throws ApiException { - VmDefinition vmDef; - if (vmStub == null || (vmDef = vmStub.model().orElse(null)) == 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()) - .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"); - } - 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); - } - - 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; - } - if (!from.conditionStatus(Condition.VMOP_AGENT).orElse(false)) { - updateCondition(from, Condition.USER_LOGGED_IN, false, - "VmopAgentDisconnected", "Waiting for VMOP agent to connect"); - return; - } - 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"); - } - - /** - * Update the current RAM size in the status. Balloon changes happen - * more than once every second during changes. While this is nice - * to watch, this puts a heavy load on the system. Therefore we - * only update the status once every 15 seconds or when the target - * value is reached. - * - * @param event the event - * @throws ApiException - */ - @Handler - public void onBallonChange(BalloonChangeEvent event) throws ApiException { - if (vmStub == null) { - return; - } - Instant now = Instant.now(); - if (lastRamChange == null - || lastRamChange.isBefore(now.minusSeconds(15)) - || event.size().equals(targetRamValue)) { - if (balloonTimer != null) { - balloonTimer.cancel(); - balloonTimer = null; - } - lastRamChange = now; - lastRamValue = event.size(); - updateRam(); - return; - } - - // Save for later processing and maybe start timer - lastRamChange = now; - lastRamValue = event.size(); - if (balloonTimer != null) { - return; - } - final var pipeline = activeEventPipeline(); - balloonTimer = Components.schedule(t -> { - pipeline.submit("Update RAM size", () -> { - try { - updateRam(); - } catch (ApiException e) { - logger.log(Level.WARNING, e, - () -> "Failed to update ram size: " + e.getMessage()); - } - balloonTimer = null; - }); - }, now.plusSeconds(15)); - } - - private void updateRam() throws ApiException { - vmStub.updateStatus(from -> { - JsonObject status = from.statusJson(); - status.addProperty(Status.RAM, - new Quantity(new BigDecimal(lastRamValue), Format.BINARY_SI) - .toSuffixedString()); - return status; - }); - } - - /** - * On ballon change. - * - * @param event the event - * @throws ApiException - */ - @Handler - public void onCpuChange(HotpluggableCpuStatus event) throws ApiException { - if (vmStub == null) { - return; - } - vmStub.updateStatus(from -> { - JsonObject status = from.statusJson(); - status.addProperty(Status.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; - }); - } -} 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/QmpReset.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpReset.java deleted file mode 100644 index 5364811..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpReset.java +++ /dev/null @@ -1,42 +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; - -/** - * A {@link QmpCommand} that send a system_reset to the VM. - */ -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()"; - } - -} 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/BalloonChangeEvent.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/BalloonChangeEvent.java deleted file mode 100644 index 9cc67c8..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/BalloonChangeEvent.java +++ /dev/null @@ -1,47 +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; -import java.math.BigInteger; - -/** - * Signals a change of the balloon. - */ -public class BalloonChangeEvent extends MonitorEvent { - - /** - * Instantiates a new tray moved. - * - * @param kind the kind - * @param data the data - */ - public BalloonChangeEvent(Kind kind, JsonNode data) { - super(kind, data); - } - - /** - * Returns the actual value. - * - * @return the actual value - */ - public BigInteger size() { - return new BigInteger(data().get("actual").asText()); - } -} 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..3ed2e50 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 @@ -19,10 +19,6 @@ package org.jdrupes.vmoperator.runner.qemu.events; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; import org.jdrupes.vmoperator.runner.qemu.commands.QmpCommand; /** @@ -30,11 +26,6 @@ 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<>(); - /** * Instantiates a new hotpluggable cpu result. * @@ -43,39 +34,6 @@ public class HotpluggableCpuStatus extends MonitorResult { */ public HotpluggableCpuStatus(QmpCommand command, JsonNode response) { super(command, response); - if (!successful()) { - return; - } - - // Sort - for (var itr = values().iterator(); itr.hasNext();) { - ObjectNode cpu = (ObjectNode) itr.next(); - if (cpu.has("qom-path")) { - usedCpus.add(cpu); - } else { - unusedCpus.add(cpu); - } - } - usedCpus = Collections.unmodifiableList(usedCpus); - unusedCpus = Collections.unmodifiableList(unusedCpus); - } - - /** - * Gets the used cpus. - * - * @return the usedCpus - */ - public List usedCpus() { - return usedCpus; - } - - /** - * Gets the unused cpus. - * - * @return the unusedCpus - */ - public List unusedCpus() { - return unusedCpus; } } 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..28d2e4c 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 } private final Kind kind; @@ -49,36 +44,20 @@ 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))); - 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 TrayMovedEvent(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 +92,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..ee8515f 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 @@ -19,13 +19,10 @@ package org.jdrupes.vmoperator.runner.qemu.events; import com.fasterxml.jackson.databind.JsonNode; -import java.util.Optional; import org.jdrupes.vmoperator.runner.qemu.commands.QmpAddCpu; -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; @@ -46,9 +43,6 @@ public class MonitorResult extends Event { * @return the monitor result */ public static MonitorResult from(QmpCommand command, JsonNode response) { - if (command instanceof QmpCapabilities) { - return new QmpConfigured(command, response); - } if (command instanceof QmpQueryHotpluggableCpus) { return new HotpluggableCpuStatus(command, response); } @@ -58,9 +52,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); } @@ -98,7 +89,6 @@ public class MonitorResult extends Event { * * @return the json node */ - @SuppressWarnings("PMD.AvoidDuplicateLiterals") public JsonNode values() { if (response.has("return")) { return response.get("return"); @@ -109,50 +99,14 @@ public class MonitorResult extends Event { return null; } - /** - * Returns the error class if this result is an error. - * - * @return the optional - */ - public Optional errorClass() { - if (!response.has("error")) { - return Optional.empty(); - } - return Optional.ofNullable(response.get("error").get("class").asText()); - } - - /** - * Returns the error description if this result is an error. - * - * @return the optional - */ - public Optional errorDescription() { - if (!response.has("error")) { - return Optional.empty(); - } - return Optional.ofNullable(response.get("error").get("desc").asText()); - } - - /** - * Combines error class and error description to a string - * "class: desc". Returns an empty string is this result is not an error. - * - * @return the string - */ - public String errorMessage() { - if (successful()) { - return ""; - } - return errorClass().get() + ": " + errorDescription().get(); - } - @Override public String toString() { StringBuilder builder = new StringBuilder(); 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/QmpConfigured.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/QmpConfigured.java deleted file mode 100644 index 918177b..0000000 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/QmpConfigured.java +++ /dev/null @@ -1,36 +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; -import org.jdrupes.vmoperator.runner.qemu.commands.QmpCommand; - -/** - * Signals that the QMP capabilities have been enabled. - */ -public class QmpConfigured extends MonitorResult { - - /** - * Instantiates a new monitor ready. - */ - public QmpConfigured(QmpCommand command, JsonNode response) { - super(command, response); - } - -} 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..a827fa4 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,9 +18,7 @@ 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,64 +27,22 @@ 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 } - private final RunState state; - private final String reason; - private final String message; - private final boolean failed; + private final State state; /** * 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, - 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) { + public RunnerStateChange(State state, Channel... channels) { super(channels); this.state = state; - this.reason = reason; - this.failed = failed; - this.message = message; } /** @@ -94,50 +50,7 @@ public class RunnerStateChange extends Event { * * @return the state */ - public RunState runState() { + public State state() { return state; } - - /** - * Gets the reason. - * - * @return the reason - */ - public String reason() { - return reason; - } - - /** - * Gets the message. - * - * @return the message - */ - public String message() { - return message; - } - - /** - * Checks if is failed. - * - * @return the failed - */ - public boolean failed() { - return failed; - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(50); - 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(']'); - 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..514f561 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" ] @@ -68,14 +52,12 @@ - [ "-global", "ICH9-LPC.disable_s3=1" ] - [ "-global", "ICH9-LPC.disable_s4=1" ] <#if firmwareRom??> - # * Provide ROM/EEPROM devices (instead of built-in BIOS). - # Don't use cache.direct=on for these as this can results in - # incredibly bad performance when booting. - - [ "-blockdev", "node-name=fw-rom-file,driver=file,\ + # * Provide ROM/EEPROM devices (instead of built-in BIOS) + - [ "-blockdev", "node-name=fw-rom-file,driver=file,cache.direct=on,\ filename=${ firmwareRom },auto-read-only=true,discard=unmap" ] - [ "-blockdev", "node-name=fw-rom-device,driver=raw,\ read-only=true,file=fw-rom-file" ] - - [ "-blockdev", "node-name=fw-eeprom-file,driver=file,\ + - [ "-blockdev", "node-name=fw-eeprom-file,driver=file,cache.direct=on,\ filename=${ firmwareVars },auto-read-only=true,discard=unmap" ] - [ "-blockdev", "node-name=fw-eeprom-device,driver=raw,\ read-only=false,file=fw-eeprom-file" ] @@ -103,10 +85,10 @@ - [ "-cpu", "${ vm.cpuModel }" ] <#if vm.maximumCpus gt 1> - [ "-smp", "1,maxcpus=${ vm.maximumCpus }\ - <#if vm.sockets gt 0>,sockets=${ vm.sockets }\ - <#if vm.diesPerSocket gt 0>,dies=${ vm.diesPerSocket }\ + <#if vm.cpuSockets gt 0>,sockets=${ vm.cpuSockets }\ + <#if vm.diesPerSocket gt 0>,cores=${ vm.diesPerSocket }\ <#if vm.coresPerDie gt 0>,cores=${ vm.coresPerDie }\ - <#if vm.threadsPerCore gt 0>,threads=${ vm.threadsPerCore }" ] + <#if vm.threadsPerCore gt 0>,cores=${ vm.threadsPerCore }" ] <#if vm.accelerator != "none"> - [ "-accel", "${ vm.accelerator }" ] @@ -122,16 +104,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 +119,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 +181,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/.checkstyle b/org.jdrupes.vmoperator.util/.checkstyle index 7f2c604..2add381 100644 --- a/org.jdrupes.vmoperator.util/.checkstyle +++ b/org.jdrupes.vmoperator.util/.checkstyle @@ -1,10 +1,10 @@ - + - + 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/.settings/org.eclipse.jdt.core.prefs b/org.jdrupes.vmoperator.util/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..c3c017d --- /dev/null +++ b/org.jdrupes.vmoperator.util/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,318 @@ +# +#Wed Aug 02 12:50:19 CEST 2023 +org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.parentheses_positions_in_for_statment=common_lines +org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert +org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert +org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=false +org.eclipse.jdt.core.formatter.indentation.size=4 +org.eclipse.jdt.core.formatter.parentheses_positions_in_enum_constant_declaration=common_lines +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert +org.eclipse.jdt.core.formatter.continuation_indentation=2 +org.eclipse.jdt.core.formatter.blank_lines_after_package=1 +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16 +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.comment.indent_root_tags=false +org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true +org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on +org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position=false +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16 +org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert +org.eclipse.jdt.core.formatter.alignment_for_parameterized_type_references=16 +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_enum_constant=insert +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert +org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=true +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16 +org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0 +org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false +org.eclipse.jdt.core.formatter.parentheses_positions_in_catch_clause=common_lines +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=20 +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true +org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line +org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_type_parameters=16 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false +org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert +org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=false +org.eclipse.jdt.core.formatter.parentheses_positions_in_annotation=common_lines +org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert +org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.align_type_members_on_columns=false +org.eclipse.jdt.core.formatter.alignment_for_module_statements=16 +org.eclipse.jdt.core.formatter.alignment_for_assignment=20 +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0 +org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false +org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert +org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.comment.format_header=false +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=20 +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert +org.eclipse.jdt.core.formatter.alignment_for_method_declaration=16 +org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true +org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647 +org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert +org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=16 +org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=20 +org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=true +org.eclipse.jdt.core.compiler.source=17 +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert +org.eclipse.jdt.core.formatter.comment.format_source_code=false +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert +org.eclipse.jdt.core.formatter.blank_lines_before_field=0 +org.eclipse.jdt.core.formatter.blank_lines_before_method=1 +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16 +org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert +org.eclipse.jdt.core.formatter.comment.format_html=false +org.eclipse.jdt.core.formatter.parentheses_positions_in_method_delcaration=common_lines +org.eclipse.jdt.core.formatter.alignment_for_compact_if=16 +org.eclipse.jdt.core.formatter.indent_empty_lines=false +org.eclipse.jdt.core.formatter.alignment_for_type_arguments=16 +org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=20 +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert +org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=false +org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert +org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow=insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16 +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert +org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert +org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true +org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_for_loop_header=16 +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow=insert +org.eclipse.jdt.core.formatter.join_lines_in_comments=false +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert +org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1 +org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert +org.eclipse.jdt.core.formatter.parentheses_positions_in_method_invocation=common_lines +org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 +org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert +org.eclipse.jdt.core.formatter.parentheses_positions_in_switch_statement=common_lines +org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert +org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off +org.eclipse.jdt.core.formatter.alignment_for_enum_constants=16 +org.eclipse.jdt.core.formatter.blank_lines_before_imports=1 +org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert +org.eclipse.jdt.core.formatter.parentheses_positions_in_if_while_statement=common_lines +org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert +org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert +org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1 +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert +org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=true +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert +org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true +org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert +org.eclipse.jdt.core.formatter.comment.line_length=80 +org.eclipse.jdt.core.formatter.use_on_off_tags=false +org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert +org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert +org.eclipse.jdt.core.formatter.alignment_for_binary_expression=20 +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert +org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line +org.eclipse.jdt.core.formatter.compact_else_if=true +org.eclipse.jdt.core.formatter.brace_position_for_lambda_body=end_of_line +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=20 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16 +org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert +org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false +org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16 +org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1 +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert +org.eclipse.jdt.core.formatter.comment.format_line_comments=true +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=84 +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert +org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.formatter.join_wrapped_lines=false +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.wrap_before_conditional_operator=true +org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert +org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=true +org.eclipse.jdt.core.formatter.parentheses_positions_in_try_clause=common_lines +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert +org.eclipse.jdt.core.formatter.tabulation.size=4 +org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert +org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert +org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=1 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16 +org.eclipse.jdt.core.formatter.wrap_before_assignment_operator=true +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert +org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert +eclipse.preferences.version=1 +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1 +org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert +org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=20 +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert +org.eclipse.jdt.core.formatter.comment.format_block_comments=true +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16 +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert +org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true +org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true +org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert +org.eclipse.jdt.core.formatter.parentheses_positions_in_lambda_declaration=common_lines +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.blank_lines_before_package=0 +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert +org.eclipse.jdt.core.javaFormatter=org.eclipse.jdt.core.defaultJavaFormatter +org.eclipse.jdt.core.formatter.comment.indent_parameter_description=true +org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert +org.eclipse.jdt.core.formatter.tabulation.char=space +org.eclipse.jdt.core.formatter.lineSplit=80 +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert diff --git a/org.jdrupes.vmoperator.util/build.gradle b/org.jdrupes.vmoperator.util/build.gradle index 5f2b065..0dac0c5 100644 --- a/org.jdrupes.vmoperator.util/build.gradle +++ b/org.jdrupes.vmoperator.util/build.gradle @@ -9,6 +9,5 @@ plugins { } dependencies { - api 'org.freemarker:freemarker:[2.3.32,2.4)' - api 'com.google.code.gson:gson:2.10.1' + implementation 'org.freemarker:freemarker:[2.3.32,2.4)' } 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.common/src/org/jdrupes/vmoperator/common/Convertions.java b/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/ParseUtils.java similarity index 74% rename from org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Convertions.java rename to org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/ParseUtils.java index 68f52eb..ff2ab31 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Convertions.java +++ b/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/ParseUtils.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.jdrupes.vmoperator.common; +package org.jdrupes.vmoperator.util; import java.math.BigDecimal; import java.math.BigInteger; @@ -26,17 +26,16 @@ import java.util.Map; import java.util.regex.Pattern; /** - * Provides methods for parsing "official" memory sizes.. + * Provides methods for parsing. */ @SuppressWarnings("PMD.UseUtilityClass") -public class Convertions { +public class ParseUtils { @SuppressWarnings({ "PMD.UseConcurrentHashMap", - "PMD.FieldNamingConventions" }) + "PMD.FieldNamingConventions", "PMD.VariableNamingConventions" }) private static final Map unitMap = new HashMap<>(); - @SuppressWarnings({ "PMD.FieldNamingConventions" }) - 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*"); @@ -54,11 +53,9 @@ public class Convertions { scale = BigInteger.valueOf(1024); for (var unit : List.of("KiB", "MiB", "GiB", "TiB", "PiB", "EiB")) { unitMap.put(unit, factor); + unitMap.put(unit.substring(0, 2), factor); factor = factor.multiply(scale); } - unitMappings = unitMap.entrySet().stream() - .sorted((a, b) -> -1 * a.getValue().compareTo(b.getValue())) - .toList(); } /** @@ -67,6 +64,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; @@ -94,20 +92,4 @@ public class Convertions { .toBigInteger(); } - /** - * Format memory size for humans. - * - * @param size the size - * @return the string - */ - public static String formatMemory(BigInteger size) { - for (var mapping : unitMappings) { - if (size.compareTo(mapping.getValue()) >= 0 - && size.mod(mapping.getValue()).equals(BigInteger.ZERO)) { - return (size.divide(mapping.getValue()).toString() - + " " + mapping.getKey()).trim(); - } - } - return size.toString(); - } } diff --git a/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/package-info.java b/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/package-info.java index e1ce2f7..24ffeb7 100644 --- a/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/package-info.java +++ b/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/package-info.java @@ -16,8 +16,4 @@ * along with this program. If not, see . */ -/** - * General utility classes and methods that are independent of the - * specific domain (VM operator). - */ package org.jdrupes.vmoperator.util; \ No newline at end of file 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/.checkstyle b/org.jdrupes.vmoperator.vmaccess/.checkstyle deleted file mode 100644 index 7f2c604..0000000 --- a/org.jdrupes.vmoperator.vmaccess/.checkstyle +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - 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/.gitignore b/org.jdrupes.vmoperator.vmaccess/.gitignore deleted file mode 100644 index a53e74c..0000000 --- a/org.jdrupes.vmoperator.vmaccess/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -/bin/ -/bin_test/ -/generated/ -/build/ diff --git a/org.jdrupes.vmoperator.vmaccess/.settings/org.eclipse.buildship.core.prefs b/org.jdrupes.vmoperator.vmaccess/.settings/org.eclipse.buildship.core.prefs deleted file mode 100644 index 641c156..0000000 --- a/org.jdrupes.vmoperator.vmaccess/.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.vmaccess/.settings/org.eclipse.core.resources.prefs b/org.jdrupes.vmoperator.vmaccess/.settings/org.eclipse.core.resources.prefs deleted file mode 100644 index 99f26c0..0000000 --- a/org.jdrupes.vmoperator.vmaccess/.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.vmaccess/.settings/org.eclipse.core.runtime.prefs b/org.jdrupes.vmoperator.vmaccess/.settings/org.eclipse.core.runtime.prefs deleted file mode 100644 index 5a0ad22..0000000 --- a/org.jdrupes.vmoperator.vmaccess/.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.vmaccess/.settings/org.eclipse.jdt.ui.prefs b/org.jdrupes.vmoperator.vmaccess/.settings/org.eclipse.jdt.ui.prefs deleted file mode 100644 index 784d01f..0000000 --- a/org.jdrupes.vmoperator.vmaccess/.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.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/package.json b/org.jdrupes.vmoperator.vmaccess/package.json deleted file mode 100644 index 0967ef4..0000000 --- a/org.jdrupes.vmoperator.vmaccess/package.json +++ /dev/null @@ -1 +0,0 @@ -{} 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/l10n_en.properties b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/l10n_en.properties deleted file mode 100644 index e69de29..0000000 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/rollup.config.mjs b/org.jdrupes.vmoperator.vmaccess/rollup.config.mjs deleted file mode 100644 index ab1aae9..0000000 --- a/org.jdrupes.vmoperator.vmaccess/rollup.config.mjs +++ /dev/null @@ -1,35 +0,0 @@ -import typescript from 'rollup-plugin-typescript2'; -import postcss from 'rollup-plugin-postcss'; - -let packagePath = "org/jdrupes/vmoperator/vmaccess"; -let baseName = "VmAccess" -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" -} - -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.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/VmAccessFactory.java b/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/VmAccessFactory.java deleted file mode 100644 index 5140056..0000000 --- a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/VmAccessFactory.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.vmaccess; - -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 VmAccess}s. - */ -public class VmAccessFactory implements ConletComponentFactory { - - /* - * (non-Javadoc) - * - * @see org.jgrapes.core.ComponentFactory#componentType() - */ - @Override - public Class componentType() { - return VmAccess.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 VmAccess(componentChannel)); - } - -} 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/browser/l10nBundles-stub.d.ts b/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/browser/l10nBundles-stub.d.ts deleted file mode 100644 index 8ca03f3..0000000 --- a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/browser/l10nBundles-stub.d.ts +++ /dev/null @@ -1 +0,0 @@ -export default new Map>(); 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/tsconfig.json b/org.jdrupes.vmoperator.vmaccess/tsconfig.json deleted file mode 100644 index d9dbb3f..0000000 --- a/org.jdrupes.vmoperator.vmaccess/tsconfig.json +++ /dev/null @@ -1,23 +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/vmaccess/browser/l10nBundles-stub"], - "vue": ["./build/unpacked/org/jgrapes/webconsole/provider/vue/vue/vue"] - } - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "l10nBundles-stub.ts"] -} 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/build.gradle b/org.jdrupes.vmoperator.vmmgmt/build.gradle deleted file mode 100644 index 606c6cd..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/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.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-l10nBundles.ftl.js b/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/VmMgmt-l10nBundles.ftl.js deleted file mode 100644 index a4f1d50..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/VmMgmt-l10nBundles.ftl.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Moodle Tools Console - * Copyright (C) 2022 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 - * 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 General Public License - * for more details. - * - * You should have received a copy of the GNU 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.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/src/org/jdrupes/vmoperator/vmmgmt/package-info.java b/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/package-info.java deleted file mode 100644 index c39c193..0000000 --- a/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/package-info.java +++ /dev/null @@ -1,19 +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; \ No newline at end of file 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 deleted file mode 100644 index 4bfe990..0000000 --- a/package-lock.json +++ /dev/null @@ -1,12604 +0,0 @@ -{ - "name": "VM-Operator", - "lockfileVersion": 3, - "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-plugin-peer-deps-external": "^2.2.3", - "rollup-plugin-postcss": "^4.0.2", - "rollup-plugin-typescript2": "^0.36.0", - "rollup-plugin-vue": "^6.0.0", - "sass": "^1.49.9", - "terser": "^5.14.2", - "tslib": "^2.3.1", - "typedoc": "^0.25.1", - "typedoc-plugin-missing-exports": "^2.1.0", - "typescript": "^5.2.2" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "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==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "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==", - "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==", - "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", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "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==", - "dev": true, - "dependencies": { - "@babel/types": "^7.24.7", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "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==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.24.7", - "@babel/helper-validator-option": "^7.24.7", - "browserslist": "^4.22.2", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "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==", - "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==", - "dev": true, - "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "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==", - "dev": true, - "dependencies": { - "@babel/types": "^7.24.7" - }, - "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==", - "dev": true, - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "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==", - "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" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "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==", - "dev": true, - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "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==", - "dev": true, - "dependencies": { - "@babel/types": "^7.24.7" - }, - "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==", - "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==", - "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==", - "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==", - "dev": true, - "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "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==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/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/highlight/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/highlight/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/highlight/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/highlight/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/highlight/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/parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", - "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", - "dev": true, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "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==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "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==", - "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", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", - "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7", - "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==", - "dev": true, - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "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==", - "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==", - "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==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "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==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@jsdoc/salty": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.8.tgz", - "integrity": "sha512-5e+SFVavj1ORKlKaKr2BmTOekmXbelU7dC0cDkQLqag7xfuTPuGMUFx7KWJuv4bYZrTsoL2Z18VVCOKYxzoHcg==", - "dev": true, - "dependencies": { - "lodash": "^4.17.21" - }, - "engines": { - "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", - "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", - "dev": true, - "dependencies": { - "@gar/promisify": "^1.1.3", - "semver": "^7.3.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/@npmcli/move-file": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", - "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", - "deprecated": "This functionality has been moved to @npmcli/fs", - "dev": true, - "dependencies": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - }, - "engines": { - "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", - "integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==", - "dev": true, - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-builtin-module": "^3.2.1", - "is-module": "^1.0.0", - "resolve": "^1.22.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.78.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "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==", - "dev": true, - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "magic-string": "^0.30.3" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-terser": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", - "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", - "dev": true, - "dependencies": { - "serialize-javascript": "^6.0.1", - "smob": "^1.0.0", - "terser": "^5.17.4" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", - "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", - "dev": true, - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "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", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "dev": true, - "engines": { - "node": ">=10.13.0" - } - }, - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "dev": true, - "dependencies": { - "@types/linkify-it": "^5", - "@types/mdurl": "^2" - } - }, - "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==", - "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==", - "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==", - "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==" - }, - "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==", - "dev": true - }, - "node_modules/@types/parse5": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.3.tgz", - "integrity": "sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==", - "dev": true - }, - "node_modules/@types/resolve": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", - "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==", - "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 - }, - "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==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.24.7", - "@vue/shared": "3.4.29", - "entities": "^4.5.0", - "estree-walker": "^2.0.2", - "source-map-js": "^1.2.0" - } - }, - "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==", - "dev": true, - "dependencies": { - "@vue/compiler-core": "3.4.29", - "@vue/shared": "3.4.29" - } - }, - "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==", - "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", - "estree-walker": "^2.0.2", - "magic-string": "^0.30.10", - "postcss": "^8.4.38", - "source-map-js": "^1.2.0" - } - }, - "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==", - "dev": true, - "dependencies": { - "@vue/compiler-dom": "3.4.29", - "@vue/shared": "3.4.29" - } - }, - "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==", - "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" - } - }, - "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==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "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", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/agentkeepalive": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", - "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", - "dev": true, - "dependencies": { - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "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", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-sequence-parser": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz", - "integrity": "sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==", - "dev": true - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "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" - } - }, - "node_modules/arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/async-foreach": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", - "integrity": "sha512-VUeSMD8nEGBWaZK4lizI1sf3yEC7pnAQ/mrI7pC2fBz2s/tq5jWWEngTwaf0Gruu/OoXRGLGg1XFqpYBiGTYJA==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/bail": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", - "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "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==" - }, - "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==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true - }, - "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "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==", - "dev": true, - "dependencies": { - "fill-range": "^7.1.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==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001629", - "electron-to-chromium": "^1.4.796", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.16" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true - }, - "node_modules/builtin-modules": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", - "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", - "dev": true, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cacache": { - "version": "16.1.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", - "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", - "dev": true, - "dependencies": { - "@npmcli/fs": "^2.1.0", - "@npmcli/move-file": "^2.0.0", - "chownr": "^2.0.0", - "fs-minipass": "^2.1.0", - "glob": "^8.0.1", - "infer-owner": "^1.0.4", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "mkdirp": "^1.0.4", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^9.0.0", - "tar": "^6.1.11", - "unique-filename": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/cacache/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "engines": { - "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", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase-keys": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", - "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", - "dev": true, - "dependencies": { - "camelcase": "^5.3.1", - "map-obj": "^4.0.0", - "quick-lru": "^4.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/caniuse-api": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", - "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", - "dev": true, - "dependencies": { - "browserslist": "^4.0.0", - "caniuse-lite": "^1.0.0", - "lodash.memoize": "^4.1.2", - "lodash.uniq": "^4.5.0" - } - }, - "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==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, - "node_modules/catharsis": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", - "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", - "dev": true, - "dependencies": { - "lodash": "^4.17.15" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/ccount": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", - "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", - "dev": true, - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/character-entities": { - "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==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-html4": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", - "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-legacy": { - "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", - "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==", - "dev": true, - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/colord": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", - "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", - "dev": true - }, - "node_modules/comma-separated-tokens": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", - "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/concat-with-sourcemaps": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz", - "integrity": "sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==", - "dev": true, - "dependencies": { - "source-map": "^0.6.1" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css-declaration-sorter": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz", - "integrity": "sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g==", - "dev": true, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.0.9" - } - }, - "node_modules/css-select": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", - "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.0.1", - "domhandler": "^4.3.1", - "domutils": "^2.8.0", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-tree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", - "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", - "dev": true, - "dependencies": { - "mdn-data": "2.0.14", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "dev": true, - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cssnano": { - "version": "5.1.15", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.15.tgz", - "integrity": "sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==", - "dev": true, - "dependencies": { - "cssnano-preset-default": "^5.2.14", - "lilconfig": "^2.0.3", - "yaml": "^1.10.2" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/cssnano" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/cssnano-preset-default": { - "version": "5.2.14", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz", - "integrity": "sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==", - "dev": true, - "dependencies": { - "css-declaration-sorter": "^6.3.1", - "cssnano-utils": "^3.1.0", - "postcss-calc": "^8.2.3", - "postcss-colormin": "^5.3.1", - "postcss-convert-values": "^5.1.3", - "postcss-discard-comments": "^5.1.2", - "postcss-discard-duplicates": "^5.1.0", - "postcss-discard-empty": "^5.1.1", - "postcss-discard-overridden": "^5.1.0", - "postcss-merge-longhand": "^5.1.7", - "postcss-merge-rules": "^5.1.4", - "postcss-minify-font-values": "^5.1.0", - "postcss-minify-gradients": "^5.1.1", - "postcss-minify-params": "^5.1.4", - "postcss-minify-selectors": "^5.2.1", - "postcss-normalize-charset": "^5.1.0", - "postcss-normalize-display-values": "^5.1.0", - "postcss-normalize-positions": "^5.1.1", - "postcss-normalize-repeat-style": "^5.1.1", - "postcss-normalize-string": "^5.1.0", - "postcss-normalize-timing-functions": "^5.1.0", - "postcss-normalize-unicode": "^5.1.1", - "postcss-normalize-url": "^5.1.0", - "postcss-normalize-whitespace": "^5.1.1", - "postcss-ordered-values": "^5.1.3", - "postcss-reduce-initial": "^5.1.2", - "postcss-reduce-transforms": "^5.1.0", - "postcss-svgo": "^5.1.0", - "postcss-unique-selectors": "^5.1.1" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/cssnano-utils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz", - "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==", - "dev": true, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/csso": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", - "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", - "dev": true, - "dependencies": { - "css-tree": "^1.1.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/de-indent": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", - "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", - "dev": true, - "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==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decamelize-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", - "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", - "dev": true, - "dependencies": { - "decamelize": "^1.1.0", - "map-obj": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decamelize-keys/node_modules/map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decode-named-character-reference": { - "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==", - "dependencies": { - "character-entities": "^2.0.0" - }, - "funding": { - "type": "github", - "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", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "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==", - "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", - "integrity": "sha512-nliqOv5NkE4zMON4UA6AMJE6As35afs8aYXATpU4pTUdIKiARZwrJVEP1boA3Rx1ZXHVkwxkhcq4VkqvsuRLsA==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "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==", - "dev": true, - "dependencies": { - "@babel/core": "^7.18.10", - "@babel/generator": "^7.18.10", - "@babel/parser": "^7.18.11", - "@babel/traverse": "^7.18.11", - "@babel/types": "^7.18.10", - "chalk": "^5.0.1", - "chokidar": "^3.5.3", - "diff": "^5.1.0", - "doctrine-temporary-fork": "2.1.0", - "git-url-parse": "^13.1.0", - "github-slugger": "1.4.0", - "glob": "^8.0.3", - "globals-docs": "^2.4.1", - "highlight.js": "^11.6.0", - "ini": "^3.0.0", - "js-yaml": "^4.1.0", - "konan": "^2.1.1", - "lodash": "^4.17.21", - "mdast-util-find-and-replace": "^2.2.1", - "mdast-util-inject": "^1.1.0", - "micromark-util-character": "^1.1.0", - "parse-filepath": "^1.0.2", - "pify": "^6.0.0", - "read-pkg-up": "^9.1.0", - "remark": "^14.0.2", - "remark-gfm": "^3.0.1", - "remark-html": "^15.0.1", - "remark-reference-links": "^6.0.1", - "remark-toc": "^8.0.1", - "resolve": "^1.22.1", - "strip-json-comments": "^5.0.0", - "unist-builder": "^3.0.0", - "unist-util-visit": "^4.1.0", - "vfile": "^5.3.4", - "vfile-reporter": "^7.0.4", - "vfile-sort": "^3.0.0", - "yargs": "^17.5.1" - }, - "bin": { - "documentation": "bin/documentation.js" - }, - "engines": { - "node": ">=14" - }, - "optionalDependencies": { - "@vue/compiler-sfc": "^3.2.37", - "vue-template-compiler": "^2.7.8" - } - }, - "node_modules/dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "dev": true, - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - }, - "funding": { - "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", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/domhandler": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", - "dev": true, - "dependencies": { - "domelementtype": "^2.2.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, - "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "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==", - "dev": true - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "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" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", - "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==", - "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": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "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==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", - "dev": true, - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" - } - }, - "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==", - "dev": true, - "peer": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "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", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 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==" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "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==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gaze": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", - "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", - "dev": true, - "dependencies": { - "globule": "^1.0.0" - }, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/generic-names": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/generic-names/-/generic-names-4.0.0.tgz", - "integrity": "sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==", - "dev": true, - "dependencies": { - "loader-utils": "^3.2.0" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/git-up": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/git-up/-/git-up-7.0.0.tgz", - "integrity": "sha512-ONdIrbBCFusq1Oy0sC71F5azx8bVkvtZtMJAsv+a6lz5YAmbNnLD6HAB4gptHZVLPR8S2/kVN6Gab7lryq5+lQ==", - "dev": true, - "dependencies": { - "is-ssh": "^1.4.0", - "parse-url": "^8.1.0" - } - }, - "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==", - "dev": true, - "dependencies": { - "git-up": "^7.0.0" - } - }, - "node_modules/github-slugger": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.4.0.tgz", - "integrity": "sha512-w0dzqw/nt51xMVmlaV1+JRzN+oCa1KfcgGEWhxUG16wbdA+Xnt/yoFO8Z8x/V82ZcZ0wy6ln9QDup5avbhiDhQ==", - "dev": true - }, - "node_modules/glob": { - "version": "9.3.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", - "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==", - "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/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "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", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/globals-docs": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/globals-docs/-/globals-docs-2.4.1.tgz", - "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", - "integrity": "sha512-OPTIfhMBh7JbBYDpa5b+Q5ptmMWKwcNcFSR/0c6t8V4f3ZAVBEsKNY37QdVqmLRYSMhOUGYrY0QhSoEpzGr/Eg==", - "dev": true, - "dependencies": { - "glob": "~7.1.1", - "lodash": "^4.17.21", - "minimatch": "~3.0.2" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/globule/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/globule/node_modules/minimatch": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", - "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "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", - "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "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", - "integrity": "sha512-Nz7FfPBuljzsN3tCQ4kCBKqdNhQE2l0Tn+X1ubgKBPRoiDIu1mL08Cfw4k7q71+Duyaw7DXDN+VTAp4Vh3oCOw==", - "dev": true, - "dependencies": { - "@types/hast": "^2.0.0", - "@types/unist": "^2.0.0", - "hastscript": "^7.0.0", - "property-information": "^6.0.0", - "vfile": "^5.0.0", - "vfile-location": "^4.0.0", - "web-namespaces": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-parse-selector": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.1.tgz", - "integrity": "sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==", - "dev": true, - "dependencies": { - "@types/hast": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-raw": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-7.2.3.tgz", - "integrity": "sha512-RujVQfVsOrxzPOPSzZFiwofMArbQke6DJjnFfceiEbFh7S05CbPt0cYN+A5YeD3pso0JQk6O1aHBnx9+Pm2uqg==", - "dev": true, - "dependencies": { - "@types/hast": "^2.0.0", - "@types/parse5": "^6.0.0", - "hast-util-from-parse5": "^7.0.0", - "hast-util-to-parse5": "^7.0.0", - "html-void-elements": "^2.0.0", - "parse5": "^6.0.0", - "unist-util-position": "^4.0.0", - "unist-util-visit": "^4.0.0", - "vfile": "^5.0.0", - "web-namespaces": "^2.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-sanitize": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-4.1.0.tgz", - "integrity": "sha512-Hd9tU0ltknMGRDv+d6Ro/4XKzBqQnP/EZrpiTbpFYfXv/uOhWeKc+2uajcbEvAEH98VZd7eII2PiXm13RihnLw==", - "dev": true, - "dependencies": { - "@types/hast": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-html": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-8.0.4.tgz", - "integrity": "sha512-4tpQTUOr9BMjtYyNlt0P50mH7xj0Ks2xpo8M943Vykljf99HW6EzulIoJP1N3eKOSScEHzyzi9dm7/cn0RfGwA==", - "dev": true, - "dependencies": { - "@types/hast": "^2.0.0", - "@types/unist": "^2.0.0", - "ccount": "^2.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-raw": "^7.0.0", - "hast-util-whitespace": "^2.0.0", - "html-void-elements": "^2.0.0", - "property-information": "^6.0.0", - "space-separated-tokens": "^2.0.0", - "stringify-entities": "^4.0.0", - "zwitch": "^2.0.4" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-parse5": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-7.1.0.tgz", - "integrity": "sha512-YNRgAJkH2Jky5ySkIqFXTQiaqcAtJyVE+D5lkN6CdtOqrnkLfGYYrEcKuHOJZlp+MwjSwuD3fZuawI+sic/RBw==", - "dev": true, - "dependencies": { - "@types/hast": "^2.0.0", - "comma-separated-tokens": "^2.0.0", - "property-information": "^6.0.0", - "space-separated-tokens": "^2.0.0", - "web-namespaces": "^2.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-whitespace": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz", - "integrity": "sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hastscript": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-7.2.0.tgz", - "integrity": "sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==", - "dev": true, - "dependencies": { - "@types/hast": "^2.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-parse-selector": "^3.0.0", - "property-information": "^6.0.0", - "space-separated-tokens": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "optional": true, - "bin": { - "he": "bin/he" - } - }, - "node_modules/highlight.js": { - "version": "11.9.0", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.9.0.tgz", - "integrity": "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==", - "dev": true, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/hosted-git-info/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/hosted-git-info/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/html-void-elements": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz", - "integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", - "dev": true - }, - "node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "dev": true, - "dependencies": { - "ms": "^2.0.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/icss-replace-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz", - "integrity": "sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg==", - "dev": true - }, - "node_modules/icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "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==", - "dev": true - }, - "node_modules/import-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz", - "integrity": "sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==", - "dev": true, - "dependencies": { - "import-from": "^3.0.0" - }, - "engines": { - "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", - "integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==", - "dev": true, - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "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", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/infer-owner": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", - "dev": true - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "node_modules/ini": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ini/-/ini-3.0.1.tgz", - "integrity": "sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ==", - "dev": true, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/install": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/install/-/install-0.13.0.tgz", - "integrity": "sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==", - "dev": true, - "engines": { - "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/is-absolute": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", - "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", - "dev": true, - "dependencies": { - "is-relative": "^1.0.0", - "is-windows": "^1.0.1" - }, - "engines": { - "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", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "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" - } - ], - "engines": { - "node": ">=4" - } - }, - "node_modules/is-builtin-module": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", - "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", - "dev": true, - "dependencies": { - "builtin-modules": "^3.3.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "dev": true, - "dependencies": { - "hasown": "^2.0.0" - }, - "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", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "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", - "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", - "dev": true - }, - "node_modules/is-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", - "dev": true - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "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", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-relative": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", - "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", - "dev": true, - "dependencies": { - "is-unc-path": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-ssh": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.0.tgz", - "integrity": "sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==", - "dev": true, - "dependencies": { - "protocols": "^2.0.1" - } - }, - "node_modules/is-unc-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", - "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", - "dev": true, - "dependencies": { - "unc-path-regex": "^0.1.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "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", - "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==", - "dev": true - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/js2xmlparser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", - "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", - "dev": true, - "dependencies": { - "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==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.20.15", - "@jsdoc/salty": "^0.2.1", - "@types/markdown-it": "^14.1.1", - "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", - "marked": "^4.0.10", - "mkdirp": "^1.0.4", - "requizzle": "^0.2.3", - "strip-json-comments": "^3.1.0", - "underscore": "~1.13.2" - }, - "bin": { - "jsdoc": "jsdoc.js" - }, - "engines": { - "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", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "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", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "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" - }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "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", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/klaw": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", - "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.9" - } - }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/konan": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/konan/-/konan-2.1.1.tgz", - "integrity": "sha512-7ZhYV84UzJ0PR/RJnnsMZcAbn+kLasJhVNWsu8ZyVEJYRpGA5XESQ9d/7zOa08U0Ou4cmB++hMNY/3OSV9KIbg==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.10.5", - "@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", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "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==", - "dependencies": { - "uc.micro": "^2.0.0" - } - }, - "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==", - "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==", - "dev": true, - "peer": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "dev": true - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "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", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", - "dev": true - }, - "node_modules/longest-streak": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", - "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lunr": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", - "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", - "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==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - } - }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "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", - "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", - "dev": true, - "dependencies": { - "agentkeepalive": "^4.2.1", - "cacache": "^16.1.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^2.0.3", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^7.0.0", - "ssri": "^9.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/make-fetch-happen/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/map-obj": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "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" - }, - "bin": { - "markdown-it": "bin/markdown-it.mjs" - } - }, - "node_modules/markdown-it-anchor": { - "version": "8.6.7", - "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", - "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", - "dev": true, - "peerDependencies": { - "@types/markdown-it": "*", - "markdown-it": "*" - } - }, - "node_modules/markdown-table": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz", - "integrity": "sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==", - "dev": true, - "funding": { - "type": "github", - "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", - "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", - "dev": true, - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 12" - } - }, - "node_modules/mdast-util-definitions": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz", - "integrity": "sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==", - "dev": true, - "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-find-and-replace": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.2.tgz", - "integrity": "sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==", - "dev": true, - "dependencies": { - "@types/mdast": "^3.0.0", - "escape-string-regexp": "^5.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mdast-util-from-markdown": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", - "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", - "dev": true, - "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "mdast-util-to-string": "^3.1.0", - "micromark": "^3.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-decode-string": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "unist-util-stringify-position": "^3.0.0", - "uvu": "^0.5.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-from-markdown/node_modules/mdast-util-to-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", - "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", - "dev": true, - "dependencies": { - "@types/mdast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-2.0.2.tgz", - "integrity": "sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg==", - "dev": true, - "dependencies": { - "mdast-util-from-markdown": "^1.0.0", - "mdast-util-gfm-autolink-literal": "^1.0.0", - "mdast-util-gfm-footnote": "^1.0.0", - "mdast-util-gfm-strikethrough": "^1.0.0", - "mdast-util-gfm-table": "^1.0.0", - "mdast-util-gfm-task-list-item": "^1.0.0", - "mdast-util-to-markdown": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-autolink-literal": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.3.tgz", - "integrity": "sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA==", - "dev": true, - "dependencies": { - "@types/mdast": "^3.0.0", - "ccount": "^2.0.0", - "mdast-util-find-and-replace": "^2.0.0", - "micromark-util-character": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-footnote": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.2.tgz", - "integrity": "sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ==", - "dev": true, - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-to-markdown": "^1.3.0", - "micromark-util-normalize-identifier": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-strikethrough": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.3.tgz", - "integrity": "sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ==", - "dev": true, - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-to-markdown": "^1.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-table": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.7.tgz", - "integrity": "sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg==", - "dev": true, - "dependencies": { - "@types/mdast": "^3.0.0", - "markdown-table": "^3.0.0", - "mdast-util-from-markdown": "^1.0.0", - "mdast-util-to-markdown": "^1.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-task-list-item": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.2.tgz", - "integrity": "sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ==", - "dev": true, - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-to-markdown": "^1.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-inject": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-inject/-/mdast-util-inject-1.1.0.tgz", - "integrity": "sha512-CcJ0mHa36QYumDKiZ2OIR+ClhfOM7zIzN+Wfy8tRZ1hpH9DKLCS+Mh4DyK5bCxzE9uxMWcbIpeNFWsg1zrj/2g==", - "dev": true, - "dependencies": { - "mdast-util-to-string": "^1.0.0" - } - }, - "node_modules/mdast-util-phrasing": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz", - "integrity": "sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==", - "dev": true, - "dependencies": { - "@types/mdast": "^3.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-hast": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz", - "integrity": "sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==", - "dev": true, - "dependencies": { - "@types/hast": "^2.0.0", - "@types/mdast": "^3.0.0", - "mdast-util-definitions": "^5.0.0", - "micromark-util-sanitize-uri": "^1.1.0", - "trim-lines": "^3.0.0", - "unist-util-generated": "^2.0.0", - "unist-util-position": "^4.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz", - "integrity": "sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==", - "dev": true, - "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "longest-streak": "^3.0.0", - "mdast-util-phrasing": "^3.0.0", - "mdast-util-to-string": "^3.0.0", - "micromark-util-decode-string": "^1.0.0", - "unist-util-visit": "^4.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown/node_modules/mdast-util-to-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", - "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", - "dev": true, - "dependencies": { - "@types/mdast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-string": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-1.1.0.tgz", - "integrity": "sha512-jVU0Nr2B9X3MU4tSK7JP1CMkSvOj7X5l/GboG1tKRw52lLF1x2Ju92Ms9tNetCcbfX3hzlM73zYo2NKkWSfF/A==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-toc": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/mdast-util-toc/-/mdast-util-toc-6.1.1.tgz", - "integrity": "sha512-Er21728Kow8hehecK2GZtb7Ny3omcoPUVrmObiSUwmoRYVZaXLR751QROEFjR8W/vAQdHMLj49Lz20J55XaNpw==", - "dev": true, - "dependencies": { - "@types/extend": "^3.0.0", - "@types/mdast": "^3.0.0", - "extend": "^3.0.0", - "github-slugger": "^2.0.0", - "mdast-util-to-string": "^3.1.0", - "unist-util-is": "^5.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-toc/node_modules/github-slugger": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", - "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", - "dev": true - }, - "node_modules/mdast-util-toc/node_modules/mdast-util-to-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", - "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", - "dev": true, - "dependencies": { - "@types/mdast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", - "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==" - }, - "node_modules/meow": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", - "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", - "dev": true, - "dependencies": { - "@types/minimist": "^1.2.0", - "camelcase-keys": "^6.2.2", - "decamelize": "^1.2.0", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^3.0.0", - "read-pkg-up": "^7.0.1", - "redent": "^3.0.0", - "trim-newlines": "^3.0.0", - "type-fest": "^0.18.0", - "yargs-parser": "^20.2.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/meow/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/meow/node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, - "node_modules/meow/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/meow/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/meow/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "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", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "dependencies": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/meow/node_modules/read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "dependencies": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/meow/node_modules/read-pkg-up/node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/meow/node_modules/read-pkg/node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/meow/node_modules/read-pkg/node_modules/type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/meow/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "bin": { - "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", - "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "micromark-core-commonmark": "^1.0.1", - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-combine-extensions": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-sanitize-uri": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "uvu": "^0.5.0" - } - }, - "node_modules/micromark-core-commonmark": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", - "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-factory-destination": "^1.0.0", - "micromark-factory-label": "^1.0.0", - "micromark-factory-space": "^1.0.0", - "micromark-factory-title": "^1.0.0", - "micromark-factory-whitespace": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-classify-character": "^1.0.0", - "micromark-util-html-tag-name": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "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", - "integrity": "sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ==", - "dev": true, - "dependencies": { - "micromark-extension-gfm-autolink-literal": "^1.0.0", - "micromark-extension-gfm-footnote": "^1.0.0", - "micromark-extension-gfm-strikethrough": "^1.0.0", - "micromark-extension-gfm-table": "^1.0.0", - "micromark-extension-gfm-tagfilter": "^1.0.0", - "micromark-extension-gfm-task-list-item": "^1.0.0", - "micromark-util-combine-extensions": "^1.0.0", - "micromark-util-types": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-autolink-literal": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.5.tgz", - "integrity": "sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg==", - "dev": true, - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-sanitize-uri": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-footnote": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.1.2.tgz", - "integrity": "sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q==", - "dev": true, - "dependencies": { - "micromark-core-commonmark": "^1.0.0", - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-sanitize-uri": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-strikethrough": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.7.tgz", - "integrity": "sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw==", - "dev": true, - "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-classify-character": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-table": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.7.tgz", - "integrity": "sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw==", - "dev": true, - "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-tagfilter": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.2.tgz", - "integrity": "sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g==", - "dev": true, - "dependencies": { - "micromark-util-types": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-task-list-item": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.5.tgz", - "integrity": "sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ==", - "dev": true, - "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - }, - "funding": { - "type": "opencollective", - "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", - "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-factory-label": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", - "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - } - }, - "node_modules/micromark-factory-space": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", - "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-factory-title": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", - "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-factory-whitespace": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", - "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-character": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", - "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-chunked": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", - "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-util-classify-character": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", - "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-combine-extensions": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", - "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", - "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-util-decode-string": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", - "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-util-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", - "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-util-html-tag-name": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", - "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-util-normalize-identifier": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", - "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-util-resolve-all": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", - "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", - "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-util-subtokenize": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", - "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - } - }, - "node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "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", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "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/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_modules/minimist-options": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", - "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", - "dev": true, - "dependencies": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0", - "kind-of": "^6.0.3" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "dev": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-fetch": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", - "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", - "dev": true, - "dependencies": { - "minipass": "^3.1.6", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "dev": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "dev": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "dev": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass/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/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dev": true, - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/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/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "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==" - }, - "node_modules/nan": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", - "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==", - "dev": true - }, - "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "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", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-gyp": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.1.0.tgz", - "integrity": "sha512-B4J5M1cABxPc5PwfjhbV5hoy2DP9p8lFXASnEN6hugXOa61416tnTZ29x9sSwAd0o99XNIcpvDDy1swAExsVKA==", - "dev": true, - "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "glob": "^10.3.10", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^13.0.0", - "nopt": "^7.0.0", - "proc-log": "^3.0.0", - "semver": "^7.3.5", - "tar": "^6.1.2", - "which": "^4.0.0" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": "^16.14.0 || >=18.0.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==", - "dev": true, - "dependencies": { - "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==", - "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" - }, - "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==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/node-sass/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, - "dependencies": { - "agentkeepalive": "^4.1.3", - "cacache": "^15.2.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^6.0.0", - "minipass": "^3.1.3", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^1.3.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.2", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^6.0.0", - "ssri": "^8.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "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", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "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==", - "dev": true, - "dependencies": { - "abbrev": "^2.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/normalize-package-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", - "dev": true, - "dependencies": { - "hosted-git-info": "^4.0.1", - "is-core-module": "^2.5.0", - "semver": "^7.3.4", - "validate-npm-package-license": "^3.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm": { - "version": "8.19.4", - "resolved": "https://registry.npmjs.org/npm/-/npm-8.19.4.tgz", - "integrity": "sha512-3HANl8i9DKnUA89P4KEgVNN28EjSeDCmvEqbzOAuxCFDzdBZzjUl99zgnGpOUumvW5lvJo2HKcjrsc+tfyv1Hw==", - "bundleDependencies": [ - "@isaacs/string-locale-compare", - "@npmcli/arborist", - "@npmcli/ci-detect", - "@npmcli/config", - "@npmcli/fs", - "@npmcli/map-workspaces", - "@npmcli/package-json", - "@npmcli/run-script", - "abbrev", - "archy", - "cacache", - "chalk", - "chownr", - "cli-columns", - "cli-table3", - "columnify", - "fastest-levenshtein", - "fs-minipass", - "glob", - "graceful-fs", - "hosted-git-info", - "ini", - "init-package-json", - "is-cidr", - "json-parse-even-better-errors", - "libnpmaccess", - "libnpmdiff", - "libnpmexec", - "libnpmfund", - "libnpmhook", - "libnpmorg", - "libnpmpack", - "libnpmpublish", - "libnpmsearch", - "libnpmteam", - "libnpmversion", - "make-fetch-happen", - "minimatch", - "minipass", - "minipass-pipeline", - "mkdirp", - "mkdirp-infer-owner", - "ms", - "node-gyp", - "nopt", - "npm-audit-report", - "npm-install-checks", - "npm-package-arg", - "npm-pick-manifest", - "npm-profile", - "npm-registry-fetch", - "npm-user-validate", - "npmlog", - "opener", - "p-map", - "pacote", - "parse-conflict-json", - "proc-log", - "qrcode-terminal", - "read", - "read-package-json", - "read-package-json-fast", - "readdir-scoped-modules", - "rimraf", - "semver", - "ssri", - "tar", - "text-table", - "tiny-relative-date", - "treeverse", - "validate-npm-package-name", - "which", - "write-file-atomic" - ], - "dev": true, - "workspaces": [ - "docs", - "smoke-tests", - "workspaces/*" - ], - "dependencies": { - "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^5.6.3", - "@npmcli/ci-detect": "^2.0.0", - "@npmcli/config": "^4.2.1", - "@npmcli/fs": "^2.1.0", - "@npmcli/map-workspaces": "^2.0.3", - "@npmcli/package-json": "^2.0.0", - "@npmcli/run-script": "^4.2.1", - "abbrev": "~1.1.1", - "archy": "~1.0.0", - "cacache": "^16.1.3", - "chalk": "^4.1.2", - "chownr": "^2.0.0", - "cli-columns": "^4.0.0", - "cli-table3": "^0.6.2", - "columnify": "^1.6.0", - "fastest-levenshtein": "^1.0.12", - "fs-minipass": "^2.1.0", - "glob": "^8.0.1", - "graceful-fs": "^4.2.10", - "hosted-git-info": "^5.2.1", - "ini": "^3.0.1", - "init-package-json": "^3.0.2", - "is-cidr": "^4.0.2", - "json-parse-even-better-errors": "^2.3.1", - "libnpmaccess": "^6.0.4", - "libnpmdiff": "^4.0.5", - "libnpmexec": "^4.0.14", - "libnpmfund": "^3.0.5", - "libnpmhook": "^8.0.4", - "libnpmorg": "^4.0.4", - "libnpmpack": "^4.1.3", - "libnpmpublish": "^6.0.5", - "libnpmsearch": "^5.0.4", - "libnpmteam": "^4.0.4", - "libnpmversion": "^3.0.7", - "make-fetch-happen": "^10.2.0", - "minimatch": "^5.1.0", - "minipass": "^3.1.6", - "minipass-pipeline": "^1.2.4", - "mkdirp": "^1.0.4", - "mkdirp-infer-owner": "^2.0.0", - "ms": "^2.1.2", - "node-gyp": "^10.1.0", - "nopt": "^6.0.0", - "npm-audit-report": "^3.0.0", - "npm-install-checks": "^5.0.0", - "npm-package-arg": "^9.1.0", - "npm-pick-manifest": "^7.0.2", - "npm-profile": "^6.2.0", - "npm-registry-fetch": "^13.3.1", - "npm-user-validate": "^1.0.1", - "npmlog": "^6.0.2", - "opener": "^1.5.2", - "p-map": "^4.0.0", - "pacote": "^13.6.2", - "parse-conflict-json": "^2.0.2", - "proc-log": "^2.0.1", - "qrcode-terminal": "^0.12.0", - "read": "~1.0.7", - "read-package-json": "^5.0.2", - "read-package-json-fast": "^2.0.3", - "readdir-scoped-modules": "^1.1.0", - "rimraf": "^3.0.2", - "semver": "^7.3.7", - "ssri": "^9.0.1", - "tar": "^6.1.11", - "text-table": "~0.2.0", - "tiny-relative-date": "^1.3.0", - "treeverse": "^2.0.0", - "validate-npm-package-name": "^4.0.0", - "which": "^2.0.2", - "write-file-atomic": "^4.0.1" - }, - "bin": { - "npm": "bin/npm-cli.js", - "npx": "bin/npx-cli.js" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/@colors/colors": { - "version": "1.5.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/npm/node_modules/@gar/promisify": { - "version": "1.1.3", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/@isaacs/string-locale-compare": { - "version": "1.1.0", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/@npmcli/arborist": { - "version": "5.6.3", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/installed-package-contents": "^1.0.7", - "@npmcli/map-workspaces": "^2.0.3", - "@npmcli/metavuln-calculator": "^3.0.1", - "@npmcli/move-file": "^2.0.0", - "@npmcli/name-from-folder": "^1.0.1", - "@npmcli/node-gyp": "^2.0.0", - "@npmcli/package-json": "^2.0.0", - "@npmcli/query": "^1.2.0", - "@npmcli/run-script": "^4.1.3", - "bin-links": "^3.0.3", - "cacache": "^16.1.3", - "common-ancestor-path": "^1.0.1", - "hosted-git-info": "^5.2.1", - "json-parse-even-better-errors": "^2.3.1", - "json-stringify-nice": "^1.1.4", - "minimatch": "^5.1.0", - "mkdirp": "^1.0.4", - "mkdirp-infer-owner": "^2.0.0", - "nopt": "^6.0.0", - "npm-install-checks": "^5.0.0", - "npm-package-arg": "^9.0.0", - "npm-pick-manifest": "^7.0.2", - "npm-registry-fetch": "^13.0.0", - "npmlog": "^6.0.2", - "pacote": "^13.6.1", - "parse-conflict-json": "^2.0.1", - "proc-log": "^2.0.0", - "promise-all-reject-late": "^1.0.0", - "promise-call-limit": "^1.0.1", - "read-package-json-fast": "^2.0.2", - "readdir-scoped-modules": "^1.1.0", - "rimraf": "^3.0.2", - "semver": "^7.3.7", - "ssri": "^9.0.0", - "treeverse": "^2.0.0", - "walk-up-path": "^1.0.0" - }, - "bin": { - "arborist": "bin/index.js" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/@npmcli/ci-detect": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16" - } - }, - "node_modules/npm/node_modules/@npmcli/config": { - "version": "4.2.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/map-workspaces": "^2.0.2", - "ini": "^3.0.0", - "mkdirp-infer-owner": "^2.0.0", - "nopt": "^6.0.0", - "proc-log": "^2.0.0", - "read-package-json-fast": "^2.0.3", - "semver": "^7.3.5", - "walk-up-path": "^1.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/@npmcli/disparity-colors": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "ansi-styles": "^4.3.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/@npmcli/fs": { - "version": "2.1.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@gar/promisify": "^1.1.3", - "semver": "^7.3.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/@npmcli/git": { - "version": "3.0.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/promise-spawn": "^3.0.0", - "lru-cache": "^7.4.4", - "mkdirp": "^1.0.4", - "npm-pick-manifest": "^7.0.0", - "proc-log": "^2.0.0", - "promise-inflight": "^1.0.1", - "promise-retry": "^2.0.1", - "semver": "^7.3.5", - "which": "^2.0.2" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/@npmcli/installed-package-contents": { - "version": "1.0.7", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-bundled": "^1.1.1", - "npm-normalize-package-bin": "^1.0.1" - }, - "bin": { - "installed-package-contents": "index.js" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/npm/node_modules/@npmcli/installed-package-contents/node_modules/npm-bundled": { - "version": "1.1.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-normalize-package-bin": "^1.0.1" - } - }, - "node_modules/npm/node_modules/@npmcli/map-workspaces": { - "version": "2.0.4", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/name-from-folder": "^1.0.1", - "glob": "^8.0.1", - "minimatch": "^5.0.1", - "read-package-json-fast": "^2.0.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { - "version": "3.1.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "cacache": "^16.0.0", - "json-parse-even-better-errors": "^2.3.1", - "pacote": "^13.0.3", - "semver": "^7.3.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/@npmcli/move-file": { - "version": "2.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/@npmcli/name-from-folder": { - "version": "1.0.1", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/@npmcli/node-gyp": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/@npmcli/package-json": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "json-parse-even-better-errors": "^2.3.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/@npmcli/promise-spawn": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "infer-owner": "^1.0.4" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/@npmcli/query": { - "version": "1.2.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-package-arg": "^9.1.0", - "postcss-selector-parser": "^6.0.10", - "semver": "^7.3.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/@npmcli/run-script": { - "version": "4.2.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/node-gyp": "^2.0.0", - "@npmcli/promise-spawn": "^3.0.0", - "node-gyp": "^9.0.0", - "read-package-json-fast": "^2.0.3", - "which": "^2.0.2" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/@tootallnate/once": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/npm/node_modules/abbrev": { - "version": "1.1.1", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/agent-base": { - "version": "6.0.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/npm/node_modules/agentkeepalive": { - "version": "4.2.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.0", - "depd": "^1.1.2", - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/npm/node_modules/aggregate-error": { - "version": "3.1.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/ansi-regex": { - "version": "5.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/npm/node_modules/aproba": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/archy": { - "version": "1.0.0", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/are-we-there-yet": { - "version": "3.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/asap": { - "version": "2.0.6", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/balanced-match": { - "version": "1.0.2", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/bin-links": { - "version": "3.0.3", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "cmd-shim": "^5.0.0", - "mkdirp-infer-owner": "^2.0.0", - "npm-normalize-package-bin": "^2.0.0", - "read-cmd-shim": "^3.0.0", - "rimraf": "^3.0.0", - "write-file-atomic": "^4.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/bin-links/node_modules/npm-normalize-package-bin": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/binary-extensions": { - "version": "2.2.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/brace-expansion": { - "version": "2.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/npm/node_modules/builtins": { - "version": "5.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "semver": "^7.0.0" - } - }, - "node_modules/npm/node_modules/cacache": { - "version": "16.1.3", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/fs": "^2.1.0", - "@npmcli/move-file": "^2.0.0", - "chownr": "^2.0.0", - "fs-minipass": "^2.1.0", - "glob": "^8.0.1", - "infer-owner": "^1.0.4", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "mkdirp": "^1.0.4", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^9.0.0", - "tar": "^6.1.11", - "unique-filename": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "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/npm/node_modules/chownr": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/cidr-regex": { - "version": "3.1.1", - "dev": true, - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "ip-regex": "^4.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/clean-stack": { - "version": "2.2.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/npm/node_modules/cli-columns": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/npm/node_modules/cli-table3": { - "version": "0.6.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "string-width": "^4.2.0" - }, - "engines": { - "node": "10.* || >= 12.*" - }, - "optionalDependencies": { - "@colors/colors": "1.5.0" - } - }, - "node_modules/npm/node_modules/clone": { - "version": "1.0.4", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/npm/node_modules/cmd-shim": { - "version": "5.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "mkdirp-infer-owner": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/npm/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/color-support": { - "version": "1.1.3", - "dev": true, - "inBundle": true, - "license": "ISC", - "bin": { - "color-support": "bin.js" - } - }, - "node_modules/npm/node_modules/columnify": { - "version": "1.6.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "strip-ansi": "^6.0.1", - "wcwidth": "^1.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/npm/node_modules/common-ancestor-path": { - "version": "1.0.1", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/concat-map": { - "version": "0.0.1", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/console-control-strings": { - "version": "1.1.0", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/cssesc": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm/node_modules/debug": { - "version": "4.3.4", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/npm/node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/debuglog": { - "version": "1.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/npm/node_modules/defaults": { - "version": "1.0.3", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "clone": "^1.0.2" - } - }, - "node_modules/npm/node_modules/delegates": { - "version": "1.0.0", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/depd": { - "version": "1.1.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/npm/node_modules/dezalgo": { - "version": "1.0.4", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, - "node_modules/npm/node_modules/diff": { - "version": "5.1.0", - "dev": true, - "inBundle": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/npm/node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/encoding": { - "version": "0.1.13", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/npm/node_modules/env-paths": { - "version": "2.2.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/npm/node_modules/err-code": { - "version": "2.0.3", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/fastest-levenshtein": { - "version": "1.0.12", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/fs-minipass": { - "version": "2.1.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/fs.realpath": { - "version": "1.0.0", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/function-bind": { - "version": "1.1.1", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/gauge": { - "version": "4.0.4", - "dev": true, - "inBundle": true, - "license": "ISC", - "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/npm/node_modules/glob": { - "version": "8.0.3", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/graceful-fs": { - "version": "4.2.10", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/has": { - "version": "1.0.3", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/npm/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/has-unicode": { - "version": "2.0.1", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/hosted-git-info": { - "version": "5.2.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^7.5.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/http-cache-semantics": { - "version": "4.1.1", - "dev": true, - "inBundle": true, - "license": "BSD-2-Clause" - }, - "node_modules/npm/node_modules/http-proxy-agent": { - "version": "5.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/npm/node_modules/https-proxy-agent": { - "version": "5.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/npm/node_modules/humanize-ms": { - "version": "1.2.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "ms": "^2.0.0" - } - }, - "node_modules/npm/node_modules/iconv-lite": { - "version": "0.6.3", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm/node_modules/ignore-walk": { - "version": "5.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "minimatch": "^5.0.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/imurmurhash": { - "version": "0.1.4", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/npm/node_modules/indent-string": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/infer-owner": { - "version": "1.0.4", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/inflight": { - "version": "1.0.6", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/npm/node_modules/inherits": { - "version": "2.0.4", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/ini": { - "version": "3.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/init-package-json": { - "version": "3.0.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-package-arg": "^9.0.1", - "promzard": "^0.3.0", - "read": "^1.0.7", - "read-package-json": "^5.0.0", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4", - "validate-npm-package-name": "^4.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/ip": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/ip-regex": { - "version": "4.3.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/is-cidr": { - "version": "4.0.2", - "dev": true, - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "cidr-regex": "^3.1.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/is-core-module": { - "version": "2.10.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/npm/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/is-lambda": { - "version": "1.0.1", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/isexe": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/json-stringify-nice": { - "version": "1.1.4", - "dev": true, - "inBundle": true, - "license": "ISC", - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/jsonparse": { - "version": "1.3.1", - "dev": true, - "engines": [ - "node >= 0.2.0" - ], - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/just-diff": { - "version": "5.1.1", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/just-diff-apply": { - "version": "5.4.1", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/libnpmaccess": { - "version": "6.0.4", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "aproba": "^2.0.0", - "minipass": "^3.1.1", - "npm-package-arg": "^9.0.1", - "npm-registry-fetch": "^13.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/libnpmdiff": { - "version": "4.0.5", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/disparity-colors": "^2.0.0", - "@npmcli/installed-package-contents": "^1.0.7", - "binary-extensions": "^2.2.0", - "diff": "^5.1.0", - "minimatch": "^5.0.1", - "npm-package-arg": "^9.0.1", - "pacote": "^13.6.1", - "tar": "^6.1.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/libnpmexec": { - "version": "4.0.14", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/arborist": "^5.6.3", - "@npmcli/ci-detect": "^2.0.0", - "@npmcli/fs": "^2.1.1", - "@npmcli/run-script": "^4.2.0", - "chalk": "^4.1.0", - "mkdirp-infer-owner": "^2.0.0", - "npm-package-arg": "^9.0.1", - "npmlog": "^6.0.2", - "pacote": "^13.6.1", - "proc-log": "^2.0.0", - "read": "^1.0.7", - "read-package-json-fast": "^2.0.2", - "semver": "^7.3.7", - "walk-up-path": "^1.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/libnpmfund": { - "version": "3.0.5", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/arborist": "^5.6.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/libnpmhook": { - "version": "8.0.4", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "aproba": "^2.0.0", - "npm-registry-fetch": "^13.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/libnpmorg": { - "version": "4.0.4", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "aproba": "^2.0.0", - "npm-registry-fetch": "^13.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/libnpmpack": { - "version": "4.1.3", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/run-script": "^4.1.3", - "npm-package-arg": "^9.0.1", - "pacote": "^13.6.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/libnpmpublish": { - "version": "6.0.5", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "normalize-package-data": "^4.0.0", - "npm-package-arg": "^9.0.1", - "npm-registry-fetch": "^13.0.0", - "semver": "^7.3.7", - "ssri": "^9.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/libnpmsearch": { - "version": "5.0.4", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-registry-fetch": "^13.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/libnpmteam": { - "version": "4.0.4", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "aproba": "^2.0.0", - "npm-registry-fetch": "^13.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/libnpmversion": { - "version": "3.0.7", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/git": "^3.0.0", - "@npmcli/run-script": "^4.1.3", - "json-parse-even-better-errors": "^2.3.1", - "proc-log": "^2.0.0", - "semver": "^7.3.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/lru-cache": { - "version": "7.13.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/npm/node_modules/make-fetch-happen": { - "version": "10.2.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "agentkeepalive": "^4.2.1", - "cacache": "^16.1.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^2.0.3", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^7.0.0", - "ssri": "^9.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/minimatch": { - "version": "5.1.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/minipass": { - "version": "3.3.4", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-collect": { - "version": "1.0.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/minipass-fetch": { - "version": "2.1.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^3.1.6", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/npm/node_modules/minipass-flush": { - "version": "1.0.5", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/minipass-json-stream": { - "version": "1.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "jsonparse": "^1.3.1", - "minipass": "^3.0.0" - } - }, - "node_modules/npm/node_modules/minipass-pipeline": { - "version": "1.2.4", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-sized": { - "version": "1.0.3", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minizlib": { - "version": "2.1.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/mkdirp": { - "version": "1.0.4", - "dev": true, - "inBundle": true, - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/mkdirp-infer-owner": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "infer-owner": "^1.0.4", - "mkdirp": "^1.0.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/ms": { - "version": "2.1.3", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/mute-stream": { - "version": "0.0.8", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/negotiator": { - "version": "0.6.3", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/npm/node_modules/node-gyp": { - "version": "9.1.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.0", - "glob": "^7.1.4", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^10.0.3", - "nopt": "^5.0.0", - "npmlog": "^6.0.0", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.2", - "which": "^2.0.2" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": "^12.22 || ^14.13 || >=16" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/brace-expansion": { - "version": "1.1.11", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/glob": { - "version": "7.2.3", - "dev": true, - "inBundle": true, - "license": "ISC", - "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/npm/node_modules/node-gyp/node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/nopt": { - "version": "5.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/npm/node_modules/nopt": { - "version": "6.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "abbrev": "^1.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/normalize-package-data": { - "version": "4.0.1", - "dev": true, - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^5.0.0", - "is-core-module": "^2.8.1", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/npm-audit-report": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "chalk": "^4.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/npm-bundled": { - "version": "2.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-normalize-package-bin": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/npm-bundled/node_modules/npm-normalize-package-bin": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/npm-install-checks": { - "version": "5.0.0", - "dev": true, - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "semver": "^7.1.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/npm-normalize-package-bin": { - "version": "1.0.1", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/npm-package-arg": { - "version": "9.1.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "hosted-git-info": "^5.0.0", - "proc-log": "^2.0.1", - "semver": "^7.3.5", - "validate-npm-package-name": "^4.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/npm-packlist": { - "version": "5.1.3", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "glob": "^8.0.1", - "ignore-walk": "^5.0.1", - "npm-bundled": "^2.0.0", - "npm-normalize-package-bin": "^2.0.0" - }, - "bin": { - "npm-packlist": "bin/index.js" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/npm-packlist/node_modules/npm-normalize-package-bin": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/npm-pick-manifest": { - "version": "7.0.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-install-checks": "^5.0.0", - "npm-normalize-package-bin": "^2.0.0", - "npm-package-arg": "^9.0.0", - "semver": "^7.3.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/npm-pick-manifest/node_modules/npm-normalize-package-bin": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/npm-profile": { - "version": "6.2.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-registry-fetch": "^13.0.1", - "proc-log": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/npm-registry-fetch": { - "version": "13.3.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "make-fetch-happen": "^10.0.6", - "minipass": "^3.1.6", - "minipass-fetch": "^2.0.3", - "minipass-json-stream": "^1.0.1", - "minizlib": "^2.1.2", - "npm-package-arg": "^9.0.1", - "proc-log": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/npm-user-validate": { - "version": "1.0.1", - "dev": true, - "inBundle": true, - "license": "BSD-2-Clause" - }, - "node_modules/npm/node_modules/npmlog": { - "version": "6.0.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "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/npm/node_modules/once": { - "version": "1.4.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/npm/node_modules/opener": { - "version": "1.5.2", - "dev": true, - "inBundle": true, - "license": "(WTFPL OR MIT)", - "bin": { - "opener": "bin/opener-bin.js" - } - }, - "node_modules/npm/node_modules/p-map": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm/node_modules/pacote": { - "version": "13.6.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/git": "^3.0.0", - "@npmcli/installed-package-contents": "^1.0.7", - "@npmcli/promise-spawn": "^3.0.0", - "@npmcli/run-script": "^4.1.0", - "cacache": "^16.0.0", - "chownr": "^2.0.0", - "fs-minipass": "^2.1.0", - "infer-owner": "^1.0.4", - "minipass": "^3.1.6", - "mkdirp": "^1.0.4", - "npm-package-arg": "^9.0.0", - "npm-packlist": "^5.1.0", - "npm-pick-manifest": "^7.0.0", - "npm-registry-fetch": "^13.0.1", - "proc-log": "^2.0.0", - "promise-retry": "^2.0.1", - "read-package-json": "^5.0.0", - "read-package-json-fast": "^2.0.3", - "rimraf": "^3.0.2", - "ssri": "^9.0.0", - "tar": "^6.1.11" - }, - "bin": { - "pacote": "lib/bin.js" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/parse-conflict-json": { - "version": "2.0.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "json-parse-even-better-errors": "^2.3.1", - "just-diff": "^5.0.1", - "just-diff-apply": "^5.2.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/path-is-absolute": { - "version": "1.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm/node_modules/postcss-selector-parser": { - "version": "6.0.10", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm/node_modules/proc-log": { - "version": "2.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/promise-all-reject-late": { - "version": "1.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/promise-call-limit": { - "version": "1.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/promise-inflight": { - "version": "1.0.1", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/promise-retry": { - "version": "2.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/promzard": { - "version": "0.3.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "read": "1" - } - }, - "node_modules/npm/node_modules/qrcode-terminal": { - "version": "0.12.0", - "dev": true, - "inBundle": true, - "bin": { - "qrcode-terminal": "bin/qrcode-terminal.js" - } - }, - "node_modules/npm/node_modules/read": { - "version": "1.0.7", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "mute-stream": "~0.0.4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/npm/node_modules/read-cmd-shim": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/read-package-json": { - "version": "5.0.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "glob": "^8.0.1", - "json-parse-even-better-errors": "^2.3.1", - "normalize-package-data": "^4.0.0", - "npm-normalize-package-bin": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/read-package-json-fast": { - "version": "2.0.3", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "json-parse-even-better-errors": "^2.3.0", - "npm-normalize-package-bin": "^1.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/read-package-json/node_modules/npm-normalize-package-bin": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/readable-stream": { - "version": "3.6.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/npm/node_modules/readdir-scoped-modules": { - "version": "1.1.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "debuglog": "^1.0.1", - "dezalgo": "^1.0.0", - "graceful-fs": "^4.1.2", - "once": "^1.3.0" - } - }, - "node_modules/npm/node_modules/retry": { - "version": "0.12.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/npm/node_modules/rimraf": { - "version": "3.0.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.11", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/npm/node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "dev": true, - "inBundle": true, - "license": "ISC", - "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/npm/node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/npm/node_modules/safe-buffer": { - "version": "5.2.1", - "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" - } - ], - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/safer-buffer": { - "version": "2.1.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true - }, - "node_modules/npm/node_modules/semver": { - "version": "7.3.7", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/set-blocking": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/signal-exit": { - "version": "3.0.7", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/smart-buffer": { - "version": "4.2.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/npm/node_modules/socks": { - "version": "2.7.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "ip": "^2.0.0", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.13.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/npm/node_modules/socks-proxy-agent": { - "version": "7.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "agent-base": "^6.0.2", - "debug": "^4.3.3", - "socks": "^2.6.2" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/npm/node_modules/spdx-correct": { - "version": "3.1.1", - "dev": true, - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/npm/node_modules/spdx-exceptions": { - "version": "2.3.0", - "dev": true, - "inBundle": true, - "license": "CC-BY-3.0" - }, - "node_modules/npm/node_modules/spdx-expression-parse": { - "version": "3.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/npm/node_modules/spdx-license-ids": { - "version": "3.0.11", - "dev": true, - "inBundle": true, - "license": "CC0-1.0" - }, - "node_modules/npm/node_modules/ssri": { - "version": "9.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.1.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/string_decoder": { - "version": "1.3.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/npm/node_modules/string-width": { - "version": "4.2.3", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/tar": { - "version": "6.1.11", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/npm/node_modules/text-table": { - "version": "0.2.0", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/tiny-relative-date": { - "version": "1.3.0", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/treeverse": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/unique-filename": { - "version": "2.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^3.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/unique-slug": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/util-deprecate": { - "version": "1.0.2", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/validate-npm-package-license": { - "version": "3.0.4", - "dev": true, - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/npm/node_modules/validate-npm-package-name": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "builtins": "^5.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/walk-up-path": { - "version": "1.0.0", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/wcwidth": { - "version": "1.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "defaults": "^1.0.3" - } - }, - "node_modules/npm/node_modules/which": { - "version": "2.0.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/wide-align": { - "version": "1.1.5", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, - "node_modules/npm/node_modules/wrappy": { - "version": "1.0.2", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/write-file-atomic": { - "version": "4.0.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/yallist": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "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==", - "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" - } - }, - "node_modules/p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "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==", - "dev": true, - "peer": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "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==", - "dev": true, - "peer": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-queue": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", - "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", - "dev": true, - "dependencies": { - "eventemitter3": "^4.0.4", - "p-timeout": "^3.2.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-timeout": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", - "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", - "dev": true, - "dependencies": { - "p-finally": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "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", - "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", - "dev": true, - "dependencies": { - "is-absolute": "^1.0.0", - "map-cache": "^0.2.0", - "path-root": "^0.1.1" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse-path": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-7.0.0.tgz", - "integrity": "sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog==", - "dev": true, - "dependencies": { - "protocols": "^2.0.0" - } - }, - "node_modules/parse-url": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-8.1.0.tgz", - "integrity": "sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w==", - "dev": true, - "dependencies": { - "parse-path": "^7.0.0" - } - }, - "node_modules/parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "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==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/path-root": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", - "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", - "dev": true, - "dependencies": { - "path-root-regex": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-root-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", - "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", - "dev": true, - "engines": { - "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==", - "dev": true - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-6.1.0.tgz", - "integrity": "sha512-KocF8ve28eFjjuBKKGvzOBGzG8ew2OqOOSxTTZhirkzH7h3BI1vyzqlR0qbfcDBve1Yzo3FVlWUAtCRrbVN8Fw==", - "dev": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "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==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-calc": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz", - "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.9", - "postcss-value-parser": "^4.2.0" - }, - "peerDependencies": { - "postcss": "^8.2.2" - } - }, - "node_modules/postcss-colormin": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.3.1.tgz", - "integrity": "sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==", - "dev": true, - "dependencies": { - "browserslist": "^4.21.4", - "caniuse-api": "^3.0.0", - "colord": "^2.9.1", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-convert-values": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz", - "integrity": "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==", - "dev": true, - "dependencies": { - "browserslist": "^4.21.4", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-discard-comments": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", - "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==", - "dev": true, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-discard-duplicates": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", - "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", - "dev": true, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-discard-empty": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", - "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", - "dev": true, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-discard-overridden": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", - "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", - "dev": true, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-load-config": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", - "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", - "dev": true, - "dependencies": { - "lilconfig": "^2.0.5", - "yaml": "^1.10.2" - }, - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-merge-longhand": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz", - "integrity": "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0", - "stylehacks": "^5.1.1" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-merge-rules": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz", - "integrity": "sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==", - "dev": true, - "dependencies": { - "browserslist": "^4.21.4", - "caniuse-api": "^3.0.0", - "cssnano-utils": "^3.1.0", - "postcss-selector-parser": "^6.0.5" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-font-values": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz", - "integrity": "sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-gradients": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz", - "integrity": "sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==", - "dev": true, - "dependencies": { - "colord": "^2.9.1", - "cssnano-utils": "^3.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-params": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz", - "integrity": "sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==", - "dev": true, - "dependencies": { - "browserslist": "^4.21.4", - "cssnano-utils": "^3.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-selectors": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz", - "integrity": "sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.5" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-modules": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/postcss-modules/-/postcss-modules-4.3.1.tgz", - "integrity": "sha512-ItUhSUxBBdNamkT3KzIZwYNNRFKmkJrofvC2nWab3CPKhYBQ1f27XXh1PAPE27Psx58jeelPsxWB/+og+KEH0Q==", - "dev": true, - "dependencies": { - "generic-names": "^4.0.0", - "icss-replace-symbols": "^1.1.0", - "lodash.camelcase": "^4.3.0", - "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.0", - "postcss-modules-scope": "^3.0.0", - "postcss-modules-values": "^4.0.0", - "string-hash": "^1.1.1" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "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==", - "dev": true, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "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==", - "dev": true, - "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "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==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.4" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "dev": true, - "dependencies": { - "icss-utils": "^5.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-normalize-charset": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", - "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==", - "dev": true, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-display-values": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz", - "integrity": "sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-positions": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz", - "integrity": "sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-repeat-style": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz", - "integrity": "sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-string": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz", - "integrity": "sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-timing-functions": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz", - "integrity": "sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-unicode": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz", - "integrity": "sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==", - "dev": true, - "dependencies": { - "browserslist": "^4.21.4", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-url": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz", - "integrity": "sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==", - "dev": true, - "dependencies": { - "normalize-url": "^6.0.1", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-whitespace": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz", - "integrity": "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-ordered-values": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz", - "integrity": "sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==", - "dev": true, - "dependencies": { - "cssnano-utils": "^3.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-reduce-initial": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz", - "integrity": "sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==", - "dev": true, - "dependencies": { - "browserslist": "^4.21.4", - "caniuse-api": "^3.0.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-reduce-transforms": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz", - "integrity": "sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "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==", - "dev": true, - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-svgo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz", - "integrity": "sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0", - "svgo": "^2.7.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-unique-selectors": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz", - "integrity": "sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.5" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "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", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, - "node_modules/promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", - "dev": true - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dev": true, - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/promise.series": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/promise.series/-/promise.series-0.2.0.tgz", - "integrity": "sha512-VWQJyU2bcDTgZw8kpfBpB/ejZASlCrzwz5f2hjb/zlujOEB4oeiAhHygAWq8ubsX2GVkD4kCU5V2dwOTaCY5EQ==", - "dev": true, - "engines": { - "node": ">=0.12" - } - }, - "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==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/protocols": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.1.tgz", - "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", - "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/read-pkg": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-7.1.0.tgz", - "integrity": "sha512-5iOehe+WF75IccPc30bWTbpdDQLOCc3Uu8bi3Dte3Eueij81yx1Mrufk8qBx/YAbR4uL1FdUr+7BKXDwEtisXg==", - "dev": true, - "dependencies": { - "@types/normalize-package-data": "^2.4.1", - "normalize-package-data": "^3.0.2", - "parse-json": "^5.2.0", - "type-fest": "^2.0.0" - }, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-9.1.0.tgz", - "integrity": "sha512-vaMRR1AC1nrd5CQM0PhlRsO5oc2AAigqr7cCrZ/MW/Rsaflz4RlgzkpL4qoU/z1F6wrbd85iFv1OQj/y5RdGvg==", - "dev": true, - "dependencies": { - "find-up": "^6.3.0", - "read-pkg": "^7.1.0", - "type-fest": "^2.5.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/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", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "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", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, - "dependencies": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/remark": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/remark/-/remark-14.0.3.tgz", - "integrity": "sha512-bfmJW1dmR2LvaMJuAnE88pZP9DktIFYXazkTfOIKZzi3Knk9lT0roItIA24ydOucI3bV/g/tXBA6hzqq3FV9Ew==", - "dev": true, - "dependencies": { - "@types/mdast": "^3.0.0", - "remark-parse": "^10.0.0", - "remark-stringify": "^10.0.0", - "unified": "^10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-gfm": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-3.0.1.tgz", - "integrity": "sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==", - "dev": true, - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-gfm": "^2.0.0", - "micromark-extension-gfm": "^2.0.0", - "unified": "^10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-html": { - "version": "15.0.2", - "resolved": "https://registry.npmjs.org/remark-html/-/remark-html-15.0.2.tgz", - "integrity": "sha512-/CIOI7wzHJzsh48AiuIyIe1clxVkUtreul73zcCXLub0FmnevQE0UMFDQm7NUx8/3rl/4zCshlMfqBdWScQthw==", - "dev": true, - "dependencies": { - "@types/mdast": "^3.0.0", - "hast-util-sanitize": "^4.0.0", - "hast-util-to-html": "^8.0.0", - "mdast-util-to-hast": "^12.0.0", - "unified": "^10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-parse": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.2.tgz", - "integrity": "sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==", - "dev": true, - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-from-markdown": "^1.0.0", - "unified": "^10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-reference-links": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/remark-reference-links/-/remark-reference-links-6.0.1.tgz", - "integrity": "sha512-34wY2C6HXSuKVTRtyJJwefkUD8zBOZOSHFZ4aSTnU2F656gr9WeuQ2dL6IJDK3NPd2F6xKF2t4XXcQY9MygAXg==", - "dev": true, - "dependencies": { - "@types/mdast": "^3.0.0", - "unified": "^10.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-stringify": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-10.0.3.tgz", - "integrity": "sha512-koyOzCMYoUHudypbj4XpnAKFbkddRMYZHwghnxd7ue5210WzGw6kOBwauJTRUMq16jsovXx8dYNvSSWP89kZ3A==", - "dev": true, - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-to-markdown": "^1.0.0", - "unified": "^10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-toc": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/remark-toc/-/remark-toc-8.0.1.tgz", - "integrity": "sha512-7he2VOm/cy13zilnOTZcyAoyoolV26ULlon6XyCFU+vG54Z/LWJnwphj/xKIDLOt66QmJUgTyUvLVHi2aAElyg==", - "dev": true, - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-toc": "^6.0.0", - "unified": "^10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/requizzle": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", - "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", - "dev": true, - "dependencies": { - "lodash": "^4.17.21" - } - }, - "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "engines": { - "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" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "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==", - "dev": true, - "dependencies": { - "@types/estree": "1.0.5" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.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" - } - }, - "node_modules/rollup-plugin-peer-deps-external": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/rollup-plugin-peer-deps-external/-/rollup-plugin-peer-deps-external-2.2.4.tgz", - "integrity": "sha512-AWdukIM1+k5JDdAqV/Cxd+nejvno2FVLVeZ74NKggm3Q5s9cbbcOgUPGdbxPi4BXu7xGaZ8HG12F+thImYu/0g==", - "dev": true, - "peerDependencies": { - "rollup": "*" - } - }, - "node_modules/rollup-plugin-postcss": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/rollup-plugin-postcss/-/rollup-plugin-postcss-4.0.2.tgz", - "integrity": "sha512-05EaY6zvZdmvPUDi3uCcAQoESDcYnv8ogJJQRp6V5kZ6J6P7uAVJlrTZcaaA20wTH527YTnKfkAoPxWI/jPp4w==", - "dev": true, - "dependencies": { - "chalk": "^4.1.0", - "concat-with-sourcemaps": "^1.1.0", - "cssnano": "^5.0.1", - "import-cwd": "^3.0.0", - "p-queue": "^6.6.2", - "pify": "^5.0.0", - "postcss-load-config": "^3.0.0", - "postcss-modules": "^4.0.0", - "promise.series": "^0.2.0", - "resolve": "^1.19.0", - "rollup-pluginutils": "^2.8.2", - "safe-identifier": "^0.4.2", - "style-inject": "^0.3.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "postcss": "8.x" - } - }, - "node_modules/rollup-plugin-postcss/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/rollup-plugin-postcss/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/rollup-plugin-postcss/node_modules/pify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", - "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/rollup-plugin-postcss/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, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/rollup-plugin-typescript2": { - "version": "0.36.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.36.0.tgz", - "integrity": "sha512-NB2CSQDxSe9+Oe2ahZbf+B4bh7pHwjV5L+RSYpCu7Q5ROuN94F9b6ioWwKfz3ueL3KTtmX4o2MUH2cgHDIEUsw==", - "dev": true, - "dependencies": { - "@rollup/pluginutils": "^4.1.2", - "find-cache-dir": "^3.3.2", - "fs-extra": "^10.0.0", - "semver": "^7.5.4", - "tslib": "^2.6.2" - }, - "peerDependencies": { - "rollup": ">=1.26.3", - "typescript": ">=2.4.0" - } - }, - "node_modules/rollup-plugin-typescript2/node_modules/@rollup/pluginutils": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", - "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", - "dev": true, - "dependencies": { - "estree-walker": "^2.0.1", - "picomatch": "^2.2.2" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/rollup-plugin-vue": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-vue/-/rollup-plugin-vue-6.0.0.tgz", - "integrity": "sha512-oVvUd84d5u73M2HYM3XsMDLtZRIA/tw2U0dmHlXU2UWP5JARYHzh/U9vcxaN/x/9MrepY7VH3pHFeOhrWpxs/Q==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "hash-sum": "^2.0.0", - "rollup-pluginutils": "^2.8.2" - }, - "peerDependencies": { - "@vue/compiler-sfc": "*" - } - }, - "node_modules/rollup-pluginutils": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", - "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", - "dev": true, - "dependencies": { - "estree-walker": "^0.6.1" - } - }, - "node_modules/rollup-pluginutils/node_modules/estree-walker": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", - "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", - "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "dev": true, - "dependencies": { - "mri": "^1.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "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/safe-identifier": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/safe-identifier/-/safe-identifier-0.4.2.tgz", - "integrity": "sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==", - "dev": true - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "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==", - "dev": true, - "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/sass-graph": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-4.0.1.tgz", - "integrity": "sha512-5YCfmGBmxoIRYHnKK2AKzrAkCoQ8ozO+iumT8K4tXJXRVCPf+7s1/9KxTSW3Rbvf+7Y7b4FR3mWyLnQr3PHocA==", - "dev": true, - "dependencies": { - "glob": "^7.0.0", - "lodash": "^4.17.11", - "scss-tokenizer": "^0.4.3", - "yargs": "^17.2.1" - }, - "bin": { - "sassgraph": "bin/sassgraph" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/scss-tokenizer": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.4.3.tgz", - "integrity": "sha512-raKLgf1LI5QMQnG+RxHz6oK0sL3x3I4FN2UDLqgLOGO8hodECNnNh5BXn7fAyBxrA8zVzdQizQ6XjNJQ+uBwMw==", - "dev": true, - "dependencies": { - "js-base64": "^2.4.9", - "source-map": "^0.7.3" - } - }, - "node_modules/scss-tokenizer/node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "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==", - "dev": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "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==", - "dev": true, - "dependencies": { - "ansi-sequence-parser": "^1.1.0", - "jsonc-parser": "^3.2.0", - "vscode-oniguruma": "^1.7.0", - "vscode-textmate": "^8.0.0" - } - }, - "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" - } - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/smob": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", - "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", - "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==", - "dev": true, - "dependencies": { - "ip-address": "^9.0.5", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", - "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", - "dev": true, - "dependencies": { - "agent-base": "^6.0.2", - "debug": "^4.3.3", - "socks": "^2.6.2" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "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==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/space-separated-tokens": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", - "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "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==", - "dev": true - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "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==", - "dev": true - }, - "node_modules/ssri": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", - "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", - "dev": true, - "dependencies": { - "minipass": "^3.1.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/stable": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", - "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility", - "dev": true - }, - "node_modules/stdout-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", - "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", - "dev": true, - "dependencies": { - "readable-stream": "^2.0.1" - } - }, - "node_modules/stdout-stream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/stdout-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "node_modules/stdout-stream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string-hash": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", - "integrity": "sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==", - "dev": true - }, - "node_modules/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/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==", - "dev": true, - "dependencies": { - "character-entities-html4": "^2.0.0", - "character-entities-legacy": "^3.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/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-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", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "dependencies": { - "min-indent": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.1.tgz", - "integrity": "sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw==", - "dev": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/style-inject": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/style-inject/-/style-inject-0.3.0.tgz", - "integrity": "sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==", - "dev": true - }, - "node_modules/stylehacks": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", - "integrity": "sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==", - "dev": true, - "dependencies": { - "browserslist": "^4.21.4", - "postcss-selector-parser": "^6.0.4" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/supports-color": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", - "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/svgo": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", - "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", - "dev": true, - "dependencies": { - "@trysound/sax": "0.2.0", - "commander": "^7.2.0", - "css-select": "^4.1.3", - "css-tree": "^1.1.3", - "csso": "^4.2.0", - "picocolors": "^1.0.0", - "stable": "^0.1.8" - }, - "bin": { - "svgo": "bin/svgo" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "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==", - "dev": true, - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/tar/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/terser": { - "version": "5.31.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.1.tgz", - "integrity": "sha512-37upzU1+viGvuFtBo9NPufCb9dwM0+l9hMxYyWfBA+fbwrPqNJAhbZ6W47bBFnZHKHTUBnMvi87434qq+qnxOg==", - "dev": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "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", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/trim-lines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", - "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/trim-newlines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", - "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "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==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/true-case-path": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-2.2.1.tgz", - "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==", - "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", - "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typedoc": { - "version": "0.25.13", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.13.tgz", - "integrity": "sha512-pQqiwiJ+Z4pigfOnnysObszLiU3mVLWAExSPf+Mu06G/qsc3wzbuM56SZQvONhHLncLUhYzOVkjFFpFfL5AzhQ==", - "dev": true, - "dependencies": { - "lunr": "^2.3.9", - "marked": "^4.3.0", - "minimatch": "^9.0.3", - "shiki": "^0.14.7" - }, - "bin": { - "typedoc": "bin/typedoc" - }, - "engines": { - "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" - } - }, - "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==", - "dev": true, - "peerDependencies": { - "typedoc": "0.24.x || 0.25.x" - } - }, - "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==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "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==" - }, - "node_modules/unc-path-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", - "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/underscore": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", - "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", - "dev": true - }, - "node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unified/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/unique-filename": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", - "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", - "dev": true, - "dependencies": { - "unique-slug": "^3.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/unique-slug": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", - "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", - "dev": true, - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/unist-builder": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-3.0.1.tgz", - "integrity": "sha512-gnpOw7DIpCA0vpr6NqdPvTWnlPTApCTRzr+38E6hCWx3rz/cjo83SsKIlS1Z+L5ttScQ2AwutNnb8+tAvpb6qQ==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-generated": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.1.tgz", - "integrity": "sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-position": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", - "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "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==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "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", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true - }, - "node_modules/uvu": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", - "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", - "dev": true, - "dependencies": { - "dequal": "^2.0.0", - "diff": "^5.0.0", - "kleur": "^4.0.3", - "sade": "^1.7.3" - }, - "bin": { - "uvu": "bin.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-location": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-4.1.0.tgz", - "integrity": "sha512-YF23YMyASIIJXpktBa4vIGLJ5Gs88UB/XePgqPmTa7cDA+JeO3yclbpheQYCHjVHBn/yePzrXuygIL+xbvRYHw==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-reporter": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/vfile-reporter/-/vfile-reporter-7.0.5.tgz", - "integrity": "sha512-NdWWXkv6gcd7AZMvDomlQbK3MqFWL1RlGzMn++/O2TI+68+nqxCPTvLugdOtfSzXmjh+xUyhp07HhlrbJjT+mw==", - "dev": true, - "dependencies": { - "@types/supports-color": "^8.0.0", - "string-width": "^5.0.0", - "supports-color": "^9.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile": "^5.0.0", - "vfile-message": "^3.0.0", - "vfile-sort": "^3.0.0", - "vfile-statistics": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-reporter/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/vfile-reporter/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/vfile-reporter/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/vfile-reporter/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/vfile-sort": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/vfile-sort/-/vfile-sort-3.0.1.tgz", - "integrity": "sha512-1os1733XY6y0D5x0ugqSeaVJm9lYgj0j5qdcZQFyxlZOSy1jYarL77lLyb5gK4Wqr1d5OxmuyflSO3zKyFnTFw==", - "dev": true, - "dependencies": { - "vfile": "^5.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-statistics": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/vfile-statistics/-/vfile-statistics-2.0.1.tgz", - "integrity": "sha512-W6dkECZmP32EG/l+dp2jCLdYzmnDBIw6jwiLZSER81oR5AHRcVqL+k3Z+pfH1R73le6ayDkJRMk0sutj1bMVeg==", - "dev": true, - "dependencies": { - "vfile": "^5.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vscode-oniguruma": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", - "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", - "dev": true - }, - "node_modules/vscode-textmate": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz", - "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", - "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==", - "dev": true, - "optional": true, - "dependencies": { - "de-indent": "^1.0.2", - "he": "^1.2.0" - } - }, - "node_modules/web-namespaces": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", - "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "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==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/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/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/xmlcreate": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", - "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", - "dev": true - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, - "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "engines": { - "node": ">=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==", - "dev": true, - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zwitch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", - "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 1f72d6f..0000000 --- a/package.json +++ /dev/null @@ -1,42 +0,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-plugin-peer-deps-external": "^2.2.3", - "rollup-plugin-postcss": "^4.0.2", - "rollup-plugin-typescript2": "^0.36.0", - "rollup-plugin-vue": "^6.0.0", - "sass": "^1.49.9", - "terser": "^5.14.2", - "tslib": "^2.3.1", - "typedoc": "^0.25.1", - "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", - "rules": { - "quotes": "off" - } - }, - "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..9074ac6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,10 +11,5 @@ 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.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/.eclipse-pmd b/spice-squid/.eclipse-pmd deleted file mode 100644 index 5d69caa..0000000 --- a/spice-squid/.eclipse-pmd +++ /dev/null @@ -1,7 +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.