diff --git a/.dockerignore b/.dockerignore index cd594ac67..a14c86dfc 100644 --- a/.dockerignore +++ b/.dockerignore @@ -10,5 +10,10 @@ node/**/node_modules/ node/**/build/ node/**/dist/ +theia/**/node_modules/ +theia/**/lib/ +theia/**/src-gen/ +theia/**/gen-webpack.*.js + # Java build artifacts java/**/target/ \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 3b977b016..a0f41f133 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,33 +1,33 @@ name: Bug Report description: Create a bug report to help us improve -labels: bug +labels: ["bug"] body: -- type: markdown - attributes: - value: | - This template is not meant for security vulnerabilities disclosure. Any such issue, created in this repo, will be deleted on sight. - Instead, please report vulnerabilities to [security@theia-cloud.io](mailto:security@theia-cloud.io). For more details, please read [SECURITY.md](https://github.com/eclipsesource/theia-cloud/blob/main/SECURITY.md) in the repository root. -- type: textarea - validations: - required: true - attributes: - label: Describe the bug - description: A clear and concise description of what the bug is. -- type: textarea - validations: - required: true - attributes: - label: Expected behavior - description: A clear and concise description of what you expected to happen. -- type: input - attributes: - label: Cluster provider - description: The cluster provider (e.g. Minikube, AWS, GKE, Azure, Docker Desktop). Please include your OS when using a local cluster. If you are using Terraform, please include the configuration files if possible. -- type: input - attributes: - label: Version - description: The used version. Such as the helm chart version or the commit id the bug was discovered on -- type: textarea - attributes: - label: Additional information - description: Additional information to analyze the bug such as logs, screenshots, screencasts. + - type: markdown + attributes: + value: | + This template is not meant for security vulnerabilities disclosure. Any such issue, created in this repo, will be deleted on sight. + Instead, please report vulnerabilities to [security@theia-cloud.io](mailto:security@theia-cloud.io). For more details, please read [SECURITY.md](https://github.com/eclipse-theia/theia-cloud/blob/main/SECURITY.md) in the repository root. + - type: textarea + validations: + required: true + attributes: + label: Describe the bug + description: A clear and concise description of what the bug is. + - type: textarea + validations: + required: true + attributes: + label: Expected behavior + description: A clear and concise description of what you expected to happen. + - type: input + attributes: + label: Cluster provider + description: The cluster provider (e.g. Minikube, AWS, GKE, Azure, Docker Desktop). Please include your OS when using a local cluster. If you are using Terraform, please include the configuration files if possible. + - type: input + attributes: + label: Version + description: The used version. Such as the helm chart version or the commit id the bug was discovered on + - type: textarea + attributes: + label: Additional information + description: Additional information to analyze the bug such as logs, screenshots, screencasts. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index fa5283c54..5653012d6 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,8 @@ -blank_issues_enabled: false +blank_issues_enabled: true contact_links: - name: Question - url: https://github.com/eclipsesource/theia-cloud/discussions + url: https://github.com/eclipse-theia/theia-cloud/discussions about: Please ask questions on the Theia Cloud discussions page. - name: Professional support url: https://theia-cloud.io/support/ - about: Professional support by EclipseSource \ No newline at end of file + about: Professional support by EclipseSource diff --git a/.github/workflows/auto-assign.yml b/.github/workflows/auto-assign.yml new file mode 100644 index 000000000..02f3c03d8 --- /dev/null +++ b/.github/workflows/auto-assign.yml @@ -0,0 +1,23 @@ +name: Auto Assign PR Author + +on: + pull_request: + types: [opened, reopened, ready_for_review] + +jobs: + auto-assign: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Auto assign PR author + uses: actions/github-script@v7 + with: + script: | + await github.rest.issues.addAssignees({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + assignees: [context.payload.pull_request.user.login] + }); + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..df4572ae6 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,62 @@ +name: Build and Push Theia Cloud Images + +on: + pull_request: + branches: + - main + + push: + branches: + - main + + schedule: + - cron: '0 2 * * *' + + release: + types: + - published + - prereleased + + workflow_dispatch: + inputs: + image_tag: + description: "Optional image tag override (e.g. my-test-tag, or 2.3.0 for the release train)" + required: false + default: "" + type: string + disable_layer_cache: + description: "Disable the Docker layer cache (--no-cache) for stress testing" + required: false + default: false + type: boolean + +permissions: + contents: read + packages: write + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: false + +jobs: + build: + name: ${{ matrix.component }} + strategy: + # One component failing must not cancel the other two. + fail-fast: false + matrix: + include: + - component: operator + - component: service + - component: conversion-webhook + uses: EduIDE/.github/.github/workflows/build-and-push-docker-image.yml@v1 + with: + docker-file: dockerfiles/${{ matrix.component }}/Dockerfile + image-name: eduide/eduide-cloud/${{ matrix.component }} + docker-context: . + image-tag: ${{ github.event_name == 'release' && github.event.release.tag_name || github.event_name == 'workflow_dispatch' && inputs.image_tag || '' }} + no-cache: ${{ github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.disable_layer_cache) || false }} + # These are small Java images; the disk-reclaim step is not worth the time. + free-disk-space: false + secrets: + docker-secrets: ${{ secrets.SENTRY_AUTH_TOKEN != '' && format('SENTRY_AUTH_TOKEN={0}', secrets.SENTRY_AUTH_TOKEN) || '' }} diff --git a/.github/workflows/ci-conversion-webhook.yml b/.github/workflows/ci-conversion-webhook.yml deleted file mode 100644 index 36347acbc..000000000 --- a/.github/workflows/ci-conversion-webhook.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: "[DOCKER] conversion-webhook" - -on: - push: - branches: - - main - paths: - - "java/common/**" - - "java/conversion/**" - - "dockerfiles/conversion-webhook/**" - # Publish when a workflow has changed (this is needed to detect version updates) - - ".github/workflows/ci-conversion-webhook.yml" - - ".github/workflows/reusable-docker.yml" - pull_request: - branches: - - main - paths: - - "java/common/**" - - "java/conversion/**" - - "dockerfiles/conversion-webhook/**" - release: - types: - - published - -jobs: - run: - uses: ./.github/workflows/reusable-docker.yml - with: - docker_org: theiacloud - docker_image: theia-cloud-conversion-webhook - docker_file: dockerfiles/conversion-webhook/Dockerfile - secrets: - dockerhub_username: ${{ secrets.DOCKERHUB_USERNAME }} - dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/ci-landing-page.yml b/.github/workflows/ci-landing-page.yml deleted file mode 100644 index 1bc303445..000000000 --- a/.github/workflows/ci-landing-page.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: "[DOCKER] landing-page" - -on: - push: - branches: - - main - paths: - - "node/package-lock.json" - - "node/common/**" - - "node/landing-page/**" - - "dockerfiles/landing-page/**" - # Publish when a workflow has changed (this is needed to detect version updates) - - ".github/workflows/ci-landing-page.yml" - - ".github/workflows/reusable-docker.yml" - pull_request: - branches: - - main - paths: - - "node/package-lock.json" - - "node/common/**" - - "node/landing-page/**" - - "dockerfiles/landing-page/**" - release: - types: - - published - -jobs: - run: - uses: ./.github/workflows/reusable-docker.yml - with: - docker_org: theiacloud - docker_image: theia-cloud-landing-page - docker_file: dockerfiles/landing-page/Dockerfile - secrets: - dockerhub_username: ${{ secrets.DOCKERHUB_USERNAME }} - dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/ci-maven-common.yml b/.github/workflows/ci-maven-common.yml deleted file mode 100644 index 1d743bf62..000000000 --- a/.github/workflows/ci-maven-common.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: "[MAVEN] common" - -on: - push: - branches: - - main - paths: - - "java/common/**" - pull_request: - branches: - - main - paths: - - "java/common/**" - release: - types: - - published - -jobs: - run: - permissions: - packages: write - uses: ./.github/workflows/reusable-maven.yml - with: - path_to_package: common/org.eclipse.theia.cloud.common/ - secrets: inherit diff --git a/.github/workflows/ci-maven-conf.yml b/.github/workflows/ci-maven-conf.yml deleted file mode 100644 index 3596557ac..000000000 --- a/.github/workflows/ci-maven-conf.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: "[MAVEN] maven-conf" - -on: - push: - branches: - - main - paths: - - "java/common/maven-conf/**" - pull_request: - branches: - - main - paths: - - "java/common/maven-conf/**" - release: - types: - - published - -jobs: - run: - permissions: - packages: write - uses: ./.github/workflows/reusable-maven.yml - with: - path_to_package: common/maven-conf/ - secrets: inherit diff --git a/.github/workflows/ci-maven-operator.yml b/.github/workflows/ci-maven-operator.yml deleted file mode 100644 index 94fe211fc..000000000 --- a/.github/workflows/ci-maven-operator.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: "[MAVEN] operator" - -on: - push: - branches: - - main - paths: - - "java/common/**" - - "java/operator/org.eclipse.theia.cloud.operator/**" - pull_request: - branches: - - main - paths: - - "java/common/**" - - "java/operator/org.eclipse.theia.cloud.operator/**" - release: - types: - - published - -jobs: - run: - permissions: - packages: write - uses: ./.github/workflows/reusable-maven.yml - with: - path_to_package: operator/org.eclipse.theia.cloud.operator/ - dependencies: common/maven-conf/,common/org.eclipse.theia.cloud.common/ - secrets: inherit diff --git a/.github/workflows/ci-maven-service.yml b/.github/workflows/ci-maven-service.yml deleted file mode 100644 index 67b13ea76..000000000 --- a/.github/workflows/ci-maven-service.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: "[MAVEN] service" -on: - push: - branches: - - main - paths: - - "java/common/**" - - "java/service/org.eclipse.theia.cloud.service/**" - pull_request: - branches: - - main - paths: - - "java/common/**" - - "java/service/org.eclipse.theia.cloud.service/**" - release: - types: - - published - -jobs: - run: - permissions: - packages: write - uses: ./.github/workflows/reusable-maven.yml - with: - path_to_package: service/org.eclipse.theia.cloud.service/ - dependencies: common/maven-conf/,common/org.eclipse.theia.cloud.common/ - secrets: inherit diff --git a/.github/workflows/ci-monitor-theia.yml b/.github/workflows/ci-monitor-theia.yml deleted file mode 100644 index 4a0af06d3..000000000 --- a/.github/workflows/ci-monitor-theia.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: "[NPM] monitor-theia" - -on: - push: - branches: - - main - paths: - - "node/package-lock.json" - - "node/monitor-theia/**" - # Publish when a workflow has changed (this is needed to detect version updates) - - ".github/workflows/ci-monitor-theia.yml" - - ".github/workflows/reusable-npm.yml" - pull_request: - branches: - - main - paths: - - "node/package-lock.json" - - "node/monitor-theia/**" - release: - types: - - published - -jobs: - run: - uses: ./.github/workflows/reusable-npm.yml - permissions: - contents: read - id-token: write - with: - package_workspace: monitor-theia - secrets: - npm-token: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/ci-node-common.yml b/.github/workflows/ci-node-common.yml deleted file mode 100644 index 4198f309f..000000000 --- a/.github/workflows/ci-node-common.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: "[NPM] common" - -on: - push: - branches: - - main - paths: - - "node/package-lock.json" - - "node/common/**" - # Publish when a workflow has changed (this is needed to detect version updates) - - ".github/workflows/ci-node-common.yml" - - ".github/workflows/reusable-npm.yml" - pull_request: - branches: - - main - paths: - - "node/package-lock.json" - - "node/common/**" - release: - types: - - published - -jobs: - run: - uses: ./.github/workflows/reusable-npm.yml - permissions: - contents: read - id-token: write - with: - package_workspace: common - secrets: - npm-token: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/ci-operator.yml b/.github/workflows/ci-operator.yml deleted file mode 100644 index 8dbf82e32..000000000 --- a/.github/workflows/ci-operator.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: "[DOCKER] operator" - -on: - push: - branches: - - main - paths: - - "java/common/**" - - "java/operator/**" - - "dockerfiles/operator/**" - # Publish when a workflow has changed (this is needed to detect version updates) - - ".github/workflows/ci-operator.yml" - - ".github/workflows/reusable-docker.yml" - pull_request: - branches: - - main - paths: - - "java/common/**" - - "java/operator/**" - - "dockerfiles/operator/**" - release: - types: - - published - -jobs: - run: - uses: ./.github/workflows/reusable-docker.yml - with: - docker_org: theiacloud - docker_image: theia-cloud-operator - docker_file: dockerfiles/operator/Dockerfile - secrets: - dockerhub_username: ${{ secrets.DOCKERHUB_USERNAME }} - dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/ci-service.yml b/.github/workflows/ci-service.yml deleted file mode 100644 index 317803680..000000000 --- a/.github/workflows/ci-service.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: "[DOCKER] service" - -on: - push: - branches: - - main - paths: - - "java/common/**" - - "java/service/**" - - "dockerfiles/service/**" - # Publish when a workflow has changed (this is needed to detect version updates) - - ".github/workflows/ci-service.yml" - - ".github/workflows/reusable-docker.yml" - pull_request: - branches: - - main - paths: - - "java/common/**" - - "java/service/**" - - "dockerfiles/service/**" - release: - types: - - published - -jobs: - run: - uses: ./.github/workflows/reusable-docker.yml - with: - docker_org: theiacloud - docker_image: theia-cloud-service - docker_file: dockerfiles/service/Dockerfile - secrets: - dockerhub_username: ${{ secrets.DOCKERHUB_USERNAME }} - dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/ci-wondershaper.yml b/.github/workflows/ci-wondershaper.yml deleted file mode 100644 index 8bd725bf1..000000000 --- a/.github/workflows/ci-wondershaper.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: "[DOCKER] wondershaper" - -on: - push: - branches: - - main - paths: - - "dockerfiles/wondershaper/**" - # Publish when a workflow has changed (this is needed to detect version updates) - - ".github/workflows/ci-wondershaper.yml" - - ".github/workflows/reusable-docker.yml" - pull_request: - branches: - - main - paths: - - "dockerfiles/wondershaper/**" - release: - types: - - published - -jobs: - run: - uses: ./.github/workflows/reusable-docker.yml - with: - docker_org: theiacloud - docker_image: theia-cloud-wondershaper - docker_file: dockerfiles/wondershaper/Dockerfile - secrets: - dockerhub_username: ${{ secrets.DOCKERHUB_USERNAME }} - dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/publish-demos.yml b/.github/workflows/publish-demos.yml deleted file mode 100644 index 86cbd8192..000000000 --- a/.github/workflows/publish-demos.yml +++ /dev/null @@ -1,80 +0,0 @@ -name: "[DOCKER] demos" - -on: - workflow_dispatch: - inputs: - publish_theia_cloud_demo: - description: "Publish theia-cloud-demo?" - type: boolean - required: true - publish_theia_cloud_activity_demo_theia: - description: "Publish theia-cloud-activity-demo-theia?" - type: boolean - required: true - publish_theia_cloud_activity_demo: - description: "Publishtheia-cloud-activity-demo?" - type: boolean - required: true - -env: - VERSION: 0.12.0-next - -jobs: - check-version: - runs-on: ubuntu-latest - outputs: - is_next_version: ${{ steps.version_check.outputs.is_next_version }} - version: ${{ env.VERSION }} - steps: - - id: version_check - run: | - if [[ $VERSION == *"-next" ]]; then - echo "is_next_version=true" >> $GITHUB_OUTPUT - else - echo "is_next_version=false" >> $GITHUB_OUTPUT - fi - - publish-theia-cloud-demo: - if: inputs.publish_theia_cloud_demo == true - needs: check-version - uses: ./.github/workflows/reusable-demo.yml - with: - docker_org: theiacloud - docker_image: theia-cloud-demo - docker_file: demo/dockerfiles/demo-theia-docker/Dockerfile - docker_location: demo/dockerfiles/demo-theia-docker/. - publish_next: ${{ needs.check-version.outputs.is_next_version }} - version: ${{ needs.check-version.outputs.version }} - secrets: - dockerhub_username: ${{ secrets.DOCKERHUB_USERNAME }} - dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN }} - - publish-theia-cloud-activity-demo-theia: - if: inputs.publish_theia_cloud_activity_demo_theia == true - needs: check-version - uses: ./.github/workflows/reusable-demo.yml - with: - docker_org: theiacloud - docker_image: theia-cloud-activity-demo-theia - docker_file: demo/dockerfiles/demo-theia-monitor-theia/Dockerfile - docker_location: . - publish_next: ${{ needs.check-version.outputs.is_next_version }} - version: ${{ needs.check-version.outputs.version }} - secrets: - dockerhub_username: ${{ secrets.DOCKERHUB_USERNAME }} - dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN }} - - publish-theia-cloud-activity-demo: - if: inputs.publish_theia_cloud_activity_demo == true - needs: [check-version, publish-theia-cloud-demo] - uses: ./.github/workflows/reusable-demo.yml - with: - docker_org: theiacloud - docker_image: theia-cloud-activity-demo - docker_file: demo/dockerfiles/demo-theia-monitor-vscode/Dockerfile - docker_location: demo/dockerfiles/demo-theia-monitor-vscode/. - publish_next: ${{ needs.check-version.outputs.is_next_version }} - version: ${{ needs.check-version.outputs.version }} - secrets: - dockerhub_username: ${{ secrets.DOCKERHUB_USERNAME }} - dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/reusable-demo.yml b/.github/workflows/reusable-demo.yml deleted file mode 100644 index a48c17852..000000000 --- a/.github/workflows/reusable-demo.yml +++ /dev/null @@ -1,71 +0,0 @@ -name: Reusable workflow for demo applications - -on: - workflow_call: - inputs: - docker_org: - required: true - type: string - docker_image: - required: true - type: string - docker_file: - required: true - type: string - docker_location: - required: true - type: string - publish_next: - required: true - type: string - version: - required: true - type: string - secrets: - dockerhub_username: - required: true - dockerhub_token: - required: true - -jobs: - publish: - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - - name: Create docker tags - id: get_tags - run: | - echo "version_tag=${{ inputs.docker_org }}/${{ inputs.docker_image }}:${{ inputs.version }}" >> $GITHUB_OUTPUT - echo "next_sha_tag=${{ inputs.docker_org }}/${{ inputs.docker_image }}:${{ inputs.version }}.$(git rev-parse --short ${{ github.sha }})" >> $GITHUB_OUTPUT - echo "next_tag_for_release=${{ inputs.docker_org }}/${{ inputs.docker_image }}:${{ inputs.version }}-next" >> $GITHUB_OUTPUT - echo "latest_tag=${{ inputs.docker_org }}/${{ inputs.docker_image }}:latest" >> $GITHUB_OUTPUT - - - name: Build Docker image - run: docker build -t ${{ steps.get_tags.outputs.version_tag }} -f ${{ inputs.docker_file }} ${{ inputs.docker_location }} - - - name: Login to DockerHub - uses: docker/login-action@v3 - with: - username: ${{ secrets.dockerhub_username }} - password: ${{ secrets.dockerhub_token }} - - # Push version and SHA tag (for next versions) - - name: Push version and SHA tag - if: inputs.publish_next == 'true' - run: | - docker push ${{ steps.get_tags.outputs.version_tag }} - docker tag ${{ steps.get_tags.outputs.version_tag }} ${{ steps.get_tags.outputs.next_sha_tag }} - docker push ${{ steps.get_tags.outputs.next_sha_tag }} - - # Push version, next and latest tag for releases (for valid semver versions) - - name: Push version and latest tag - if: inputs.publish_next == 'false' - run: | - docker push ${{ steps.get_tags.outputs.version_tag }} - docker tag ${{ steps.get_tags.outputs.version_tag }} ${{ steps.get_tags.outputs.latest_tag }} - docker push ${{ steps.get_tags.outputs.latest_tag }} - docker tag ${{ steps.get_tags.outputs.version_tag }} ${{ steps.get_tags.outputs.next_tag_for_release }} - docker push ${{ steps.get_tags.outputs.next_tag_for_release }} diff --git a/.github/workflows/reusable-docker.yml b/.github/workflows/reusable-docker.yml deleted file mode 100644 index 56a615da8..000000000 --- a/.github/workflows/reusable-docker.yml +++ /dev/null @@ -1,105 +0,0 @@ -name: Reusable workflow for docker images - -on: - workflow_call: - inputs: - docker_org: - required: true - type: string - docker_image: - required: true - type: string - docker_file: - required: true - type: string - secrets: - dockerhub_username: - required: true - dockerhub_token: - required: true - -env: - VERSION: 0.12.0-next - -jobs: - build: - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - - name: Create docker tags - id: get_tags - run: | - echo "version_tag=${{ inputs.docker_org }}/${{ inputs.docker_image }}:${{ env.VERSION }}" >> $GITHUB_OUTPUT - - - name: Build the Docker image - run: | - docker build -t ${{ steps.get_tags.outputs.version_tag }} -f ${{ inputs.docker_file }} . - - publish: - runs-on: ubuntu-latest - if: github.event_name == 'push' || github.event_name == 'release' - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - # Check if we have a valid *-next version for merges on main - - name: Check version (only for push events) - if: github.event_name == 'push' - id: version_check - run: | - if [[ $VERSION == *"-next" ]]; then - echo "is_next_version=true" >> $GITHUB_OUTPUT - else - echo "is_next_version=false" >> $GITHUB_OUTPUT - fi - - # Create tags according to the state to be published: - # - next version on main merge: create `*-next` and `*-next.` tags - # - release version: create version, `*-next` and `latest` tags - - name: Create docker tags - id: get_tags - if: github.event_name == 'push' && steps.version_check.outputs.is_next_version == 'true' || github.event_name == 'release' - run: | - if [[ $GITHUB_EVENT_NAME == 'push' ]]; then - echo "sha_tag=${{ inputs.docker_org }}/${{ inputs.docker_image }}:${{ env.VERSION }}.$(git rev-parse --short ${{ github.sha }})" >> $GITHUB_OUTPUT - echo "version_tag=${{ inputs.docker_org }}/${{ inputs.docker_image }}:${{ env.VERSION }}" >> $GITHUB_OUTPUT - else - echo "version_tag=${{ inputs.docker_org }}/${{ inputs.docker_image }}:${{ env.VERSION }}" >> $GITHUB_OUTPUT - echo "next_tag=${{ inputs.docker_org }}/${{ inputs.docker_image }}:${{ env.VERSION }}-next" >> $GITHUB_OUTPUT - echo "latest_tag=${{ inputs.docker_org }}/${{ inputs.docker_image }}:latest" >> $GITHUB_OUTPUT - fi - - # Only build when we will publish, so either a main merge with next-version or a release - - name: Build Docker image - if: github.event_name == 'push' && steps.version_check.outputs.is_next_version == 'true' || github.event_name == 'release' - run: docker build -t ${{ steps.get_tags.outputs.version_tag }} -f ${{ inputs.docker_file }} . - - # Only log in to dockerhub when we will publish, so either a main merge with next-version or a release - - name: Login to DockerHub - if: github.event_name == 'push' && steps.version_check.outputs.is_next_version == 'true' || github.event_name == 'release' - uses: docker/login-action@v3 - with: - username: ${{ secrets.dockerhub_username }} - password: ${{ secrets.dockerhub_token }} - - # Push for main merges with next version - - name: Push Docker tags (for push events with next version) - if: github.event_name == 'push' && steps.version_check.outputs.is_next_version == 'true' - run: | - docker push ${{ steps.get_tags.outputs.version_tag }} - docker tag ${{ steps.get_tags.outputs.version_tag }} ${{ steps.get_tags.outputs.sha_tag }} - docker push ${{ steps.get_tags.outputs.sha_tag }} - - # Push for releases - - name: Push Docker tags (for release events) - if: github.event_name == 'release' - run: | - docker push ${{ steps.get_tags.outputs.version_tag }} - docker tag ${{ steps.get_tags.outputs.version_tag }} ${{ steps.get_tags.outputs.latest_tag }} - docker push ${{ steps.get_tags.outputs.latest_tag }} - docker tag ${{ steps.get_tags.outputs.version_tag }} ${{ steps.get_tags.outputs.next_tag }} - docker push ${{ steps.get_tags.outputs.next_tag }} diff --git a/.github/workflows/reusable-maven.yml b/.github/workflows/reusable-maven.yml deleted file mode 100644 index 512f35737..000000000 --- a/.github/workflows/reusable-maven.yml +++ /dev/null @@ -1,121 +0,0 @@ -name: Reusable workflow for maven packages - -on: - workflow_call: - inputs: - path_to_package: - required: true - type: string - dependencies: - description: "List of paths to dependencies. Entries are relative to the java directory and separated by comma (no whitespaces)." - required: false - type: string - -defaults: - run: - working-directory: java - -jobs: - build: - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: "17" - distribution: "temurin" - - - name: Build dependencies - if: ${{ inputs.dependencies != '' }} - run: | - IFS=',' read -r -a dirs <<< "${{ inputs.dependencies }}" - for dir in "${dirs[@]}" - do - cd "$dir" - mvn clean install --no-transfer-progress - cd - - done - - - name: Verify package - run: | - cd ${{ inputs.path_to_package }} - mvn verify -fae --no-transfer-progress - - publish: - runs-on: ubuntu-latest - if: github.event_name == 'push' || github.event_name == 'release' - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - - name: Run Version Check - id: version_check - run: | - cd ${{ inputs.path_to_package }} - VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) - if [[ "$VERSION" == *-SNAPSHOT ]]; then - echo "is_snapshot_version=true" >> $GITHUB_OUTPUT - else - echo "is_snapshot_version=false" >> $GITHUB_OUTPUT - fi - - # Only continue from here when the version matches the event, so either: - # - main merge with snapshot version - # - release with proper version - - name: Set up JDK 17 - if: | - (github.event_name == 'push' && steps.version_check.outputs.is_snapshot_version == 'true') || - (github.event_name == 'release' && steps.version_check.outputs.is_snapshot_version == 'false') - uses: actions/setup-java@v4 - with: - java-version: "17" - distribution: "temurin" - - - name: Build dependencies - if: | - ${{ inputs.dependencies != '' }} && - ( - (github.event_name == 'push' && steps.version_check.outputs.is_snapshot_version == 'true') || - (github.event_name == 'release' && steps.version_check.outputs.is_snapshot_version == 'false') - ) - run: | - IFS=',' read -r -a dirs <<< "${{ inputs.dependencies }}" - for dir in "${dirs[@]}" - do - cd "$dir" - mvn clean install --no-transfer-progress - cd - - done - - - name: Publish package to GitHub Packages - if: | - (github.event_name == 'push' && steps.version_check.outputs.is_snapshot_version == 'true') || - (github.event_name == 'release' && steps.version_check.outputs.is_snapshot_version == 'false') - run: | - cd ${{ inputs.path_to_package }} - mvn --batch-mode deploy - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Authenticate to Google Cloud - if: | - (github.event_name == 'push' && steps.version_check.outputs.is_snapshot_version == 'true') || - (github.event_name == 'release' && steps.version_check.outputs.is_snapshot_version == 'false') - uses: google-github-actions/auth@v2 - with: - credentials_json: "${{ secrets.GCP_SA_KEY }}" - - - name: Publish package to Google Artifact Registry - if: | - (github.event_name == 'push' && steps.version_check.outputs.is_snapshot_version == 'true') || - (github.event_name == 'release' && steps.version_check.outputs.is_snapshot_version == 'false') - run: | - cd ${{ inputs.path_to_package }} - mvn --batch-mode deploy -Partifact-registry - env: - GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }} diff --git a/.github/workflows/reusable-npm.yml b/.github/workflows/reusable-npm.yml deleted file mode 100644 index c5e300bec..000000000 --- a/.github/workflows/reusable-npm.yml +++ /dev/null @@ -1,83 +0,0 @@ -name: Reusable workflow for npm packages - -on: - workflow_call: - inputs: - package_workspace: - required: true - type: string - description: The package's workspace path relative to the node directory. - secrets: - npm-token: - required: true - -permissions: - contents: read - id-token: write - -jobs: - build: - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - defaults: - run: - working-directory: ./node - - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Install dependencies - run: npm ci - - - name: Build package - run: npm run build -w ${{ inputs.package_workspace }} - - publish: - runs-on: ubuntu-latest - if: github.event_name == 'push' || github.event_name == 'release' - defaults: - run: - working-directory: ./node - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - - name: Check version (only for push events) - if: github.event_name == 'push' - id: version_check - run: | - VERSION=$(cd ${{ inputs.package_workspace }} && node --print "require('./package.json').version") - echo "Package version: ${VERSION}" - if [[ $VERSION == *"-next" ]]; then - echo "is_next_version=true" >> $GITHUB_OUTPUT - else - echo "is_next_version=false" >> $GITHUB_OUTPUT - fi - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - registry-url: "https://registry.npmjs.org" - - - name: Install dependencies - run: npm ci - - - name: Publish next version - if: github.event_name == 'push' && steps.version_check.outputs.is_next_version == 'true' - run: npm run publish:next -w ${{ inputs.package_workspace }} - env: - NODE_AUTH_TOKEN: ${{ secrets.npm-token }} - NPM_CONFIG_PROVENANCE: true - - - name: Publish latest version - if: github.event_name == 'release' - run: npm run publish:latest -w ${{ inputs.package_workspace }} - env: - NODE_AUTH_TOKEN: ${{ secrets.npm-token }} - NPM_CONFIG_PROVENANCE: true diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml deleted file mode 100644 index ec563894f..000000000 --- a/.github/workflows/stale.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Close inactive issues -on: - schedule: - - cron: "30 9 * * *" - -jobs: - close-issues: - runs-on: ubuntu-latest - permissions: - issues: write - pull-requests: write - steps: - - uses: actions/stale@v6.0.1 - with: - days-before-stale: 180 - days-before-close: 14 - stale-issue-label: "stale" - stale-issue-message: "This issue is stale because it has been open for 180 days with no activity." - close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale." - stale-pr-message: "This PR is stale because it has been open 180 days with no activity." - close-pr-message: "This PR was closed because it has been inactive for 14 days since being marked as stale." - repo-token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 79ffbe1aa..0cab4cd17 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,20 @@ .openapi-generator .openapi-generator-ignore openapitools.json -.DS_Store \ No newline at end of file + +# IDE files +.classpath +.project +.settings/ +.factorypath +*.iml +.idea/ +.vscode/ + +# OS files +.DS_Store + +# Build artifacts +target/ +node_modules +node/e2e-tests/test-results/.last-run.json diff --git a/.vscode/launch.json b/.vscode/launch.json index 75329ecaa..81e19a2a3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,14 +2,24 @@ "version": "0.2.0", "configurations": [ { + // This works with terraform config 2-01_try-now "type": "java", - "name": "Debug Default Operator", + "name": "Debug Operator (try-now)", "request": "launch", "cwd": "${workspaceFolder}/java/operator/org.eclipse.theia.cloud.defaultoperator", "mainClass": "org.eclipse.theia.cloud.defaultoperator.DefaultTheiaCloudOperatorLauncher", "args": [ + "--keycloak", + "--keycloakURL", + "https://${input:minikubeIP}.nip.io/keycloak/", + "--keycloakRealm", + "TheiaCloud", + "--keycloakClientId", + "theia-cloud", "--instancesHost", - "ws.$(minikube.ip).nip.io", + "ws.${input:minikubeIP}.nip.io", + "--serviceUrl", + "https://service.${input:minikubeIP}.nip.io", "--cloudProvider", "MINIKUBE", "--sessionsPerUser", @@ -19,10 +29,201 @@ "--storageClassName", "default", "--requestedStorage", - "250Mi" + "250Mi", + "--bandwidthLimiter", + "WONDERSHAPER", + "--oAuth2ProxyVersion", + "v7.5.1" ], "vmArgs": "-Dlog4j2.configurationFile=log4j2.xml", "preLaunchTask": "Build and Install Operator library" + }, + { + // This works with terraform config 2-02_monitor with authentication + "type": "java", + "name": "Debug Operator (monitor-auth)", + "request": "launch", + "cwd": "${workspaceFolder}/java/operator/org.eclipse.theia.cloud.defaultoperator", + "mainClass": "org.eclipse.theia.cloud.defaultoperator.DefaultTheiaCloudOperatorLauncher", + "args": [ + "--enableMonitor", + "--enableActivityTracker", + "--keycloak", + "--keycloakURL", + "https://${input:minikubeIP}.nip.io/keycloak/", + "--keycloakRealm", + "TheiaCloud", + "--keycloakClientId", + "theia-cloud", + "--instancesHost", + "ws.${input:minikubeIP}.nip.io", + "--serviceUrl", + "https://service.${input:minikubeIP}.nip.io", + "--cloudProvider", + "MINIKUBE", + "--sessionsPerUser", + "3", + "--appId", + "asdfghjkl", + "--storageClassName", + "default", + "--requestedStorage", + "250Mi", + "--bandwidthLimiter", + "WONDERSHAPER", + "--oAuth2ProxyVersion", + "v7.5.1" + ], + "vmArgs": "-Dlog4j2.configurationFile=log4j2.xml", + "preLaunchTask": "Build and Install Operator library" + }, + { + // This works with terraform config 2-02_monitor with no authentication + "type": "java", + "name": "Debug Operator (monitor-no-auth)", + "request": "launch", + "cwd": "${workspaceFolder}/java/operator/org.eclipse.theia.cloud.defaultoperator", + "mainClass": "org.eclipse.theia.cloud.defaultoperator.DefaultTheiaCloudOperatorLauncher", + "args": [ + "--enableMonitor", + "--enableActivityTracker", + "--instancesHost", + "ws.${input:minikubeIP}.nip.io", + "--serviceUrl", + "https://service.${input:minikubeIP}.nip.io", + "--cloudProvider", + "MINIKUBE", + "--sessionsPerUser", + "3", + "--appId", + "asdfghjkl", + "--storageClassName", + "default", + "--requestedStorage", + "250Mi", + "--bandwidthLimiter", + "WONDERSHAPER", + "--oAuth2ProxyVersion", + "v7.5.1" + ], + "vmArgs": "-Dlog4j2.configurationFile=log4j2.xml", + "preLaunchTask": "Build and Install Operator library" + }, + { + // This works with terraform config 2-03_try-now_paths + "type": "java", + "name": "Debug Operator (try-now-paths)", + "request": "launch", + "cwd": "${workspaceFolder}/java/operator/org.eclipse.theia.cloud.defaultoperator", + "mainClass": "org.eclipse.theia.cloud.defaultoperator.DefaultTheiaCloudOperatorLauncher", + "args": [ + "--keycloak", + "--keycloakURL", + "https://${input:minikubeIP}.nip.io/keycloak/", + "--keycloakRealm", + "TheiaCloud", + "--keycloakClientId", + "theia-cloud", + "--usePaths", + "--instancesPath", + "instances", + "--instancesHost", + "${input:minikubeIP}.nip.io", + "--serviceUrl", + "https://${input:minikubeIP}.nip.io/service", + "--cloudProvider", + "MINIKUBE", + "--sessionsPerUser", + "3", + "--appId", + "asdfghjkl", + "--storageClassName", + "default", + "--requestedStorage", + "250Mi", + "--bandwidthLimiter", + "WONDERSHAPER", + "--oAuth2ProxyVersion", + "v7.5.1" + ], + "vmArgs": "-Dlog4j2.configurationFile=log4j2.xml", + "preLaunchTask": "Build and Install Operator library" + }, + { + // This works with terraform config 2-04_try-now_paths_eager_start + "type": "java", + "name": "Debug Operator (eager start try-now-paths)", + "request": "launch", + "cwd": "${workspaceFolder}/java/operator/org.eclipse.theia.cloud.defaultoperator", + "mainClass": "org.eclipse.theia.cloud.defaultoperator.DefaultTheiaCloudOperatorLauncher", + "args": [ + "--keycloak", + "--keycloakURL", + "https://${input:minikubeIP}.nip.io/keycloak/", + "--keycloakRealm", + "TheiaCloud", + "--keycloakClientId", + "theia-cloud", + "--usePaths", + "--instancesPath", + "instances", + "--instancesHost", + "${input:minikubeIP}.nip.io", + "--serviceUrl", + "https://${input:minikubeIP}.nip.io/service", + "--cloudProvider", + "MINIKUBE", + "--sessionsPerUser", + "3", + "--appId", + "asdfghjkl", + "--storageClassName", + "default", + "--requestedStorage", + "250Mi", + "--bandwidthLimiter", + "WONDERSHAPER", + "--oAuth2ProxyVersion", + "v7.5.1", + "--eagerStart" + ], + "vmArgs": "-Dlog4j2.configurationFile=log4j2.xml", + "preLaunchTask": "Build and Install Operator library" + }, + { + // Attach to the service running (Task: Run Service) + "type": "java", + "name": "Attach to Service", + "request": "attach", + "hostName": "localhost", + "port": 5005 + }, + { + "type": "node", + "request": "launch", + "name": "Debug Theia Example Browser Backend", + "program": "${workspaceRoot}/theia/examples/browser-app/src-gen/backend/main.js", + "args": ["--loglevel=debug", "--port=3000", "--no-cluster"], + "env": { + "NODE_ENV": "development" + }, + "sourceMaps": true, + "outFiles": [ + "${workspaceRoot}/theia/node_modules/@theia/*/lib/**/*.js", + "${workspaceRoot}/theia/*/lib/**/*.js", + "${workspaceRoot}/theia/examples/browser-app/src-gen/**/*.js" + ], + "smartStep": true, + "internalConsoleOptions": "openOnSessionStart", + "outputCapture": "std" + } + ], + "inputs": [ + { + "type": "promptString", + "id": "minikubeIP", + "description": "Provide your minikube ip address (execute 'minikube ip').", + "default": "192.168.59.101" } ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index eff85040d..e62443c22 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -37,7 +37,33 @@ "label": "Run Service", "type": "shell", "command": "mvn", - "args": ["compile", "quarkus:dev"], + "args": [ + "compile", + "quarkus:dev", + "-Dtheia.cloud.service.auth.token=asdfghjkl", + "-Dquarkus.http.port=8081", + "-Dtheia.cloud.use.keycloak=false" + ], + "options": { + "cwd": "${workspaceFolder}/java/service/org.eclipse.theia.cloud.service" + }, + "problemMatcher": [], + "dependsOn": "Build and Install Common library" + }, + { + "label": "Run Service (with keycloak)", + "type": "shell", + "command": "mvn", + "args": [ + "compile", + "quarkus:dev", + "-Dtheia.cloud.service.auth.token=asdfghjkl", + "-Dquarkus.http.port=8081", + "-Dtheia.cloud.use.keycloak=true", + "-Dquarkus.oidc.auth-server-url=${input:keycloakURL}/realms/TheiaCloud", + "-Dquarkus.oidc.client-id=theia-cloud", + "-Dquarkus.oidc.credentials.secret=publicbutoauth2proxywantsasecret" + ], "options": { "cwd": "${workspaceFolder}/java/service/org.eclipse.theia.cloud.service" }, @@ -69,5 +95,13 @@ "problemMatcher": [], "dependsOn": "Build and Install Common library" } + ], + "inputs": [ + { + "type": "promptString", + "id": "keycloakURL", + "description": "Provide the keycloak url", + "default": "https://192.168.59.101.nip.io/keycloak" + } ] } diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..d9b53d068 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,206 @@ +# AGENTS.md — Theia Cloud Operator + +Reference guide for AI coding agents operating in this repository. +Read this file at the start of every session. + +--- + +## Project Overview + +Multi-language, multi-module system for running Theia IDE instances on Kubernetes. + +| Module | Language | Purpose | +|--------|----------|---------| +| `java/common/` | Java 21 / Maven | Shared K8s client, CRD models, utilities | +| `java/operator/` | Java 21 / Maven + Guice | Kubernetes Operator (manages Sessions, AppDefinitions) | +| `java/service/` | Java 21 / Maven + Quarkus | REST API for workspace/session management | +| `java/conversion/` | Java 21 / Maven | K8s CRD conversion webhooks | +| `node/` | TypeScript / NPM Workspaces | Landing page (React/Vite), monitor, e2e tests | +| `theia/` | TypeScript / Yarn + Lerna | Theia extensions (monitor, config-store) | +| `dockerfiles/` | — | Multi-stage Docker images for all services | +| `terraform/` | Terraform | Not actively maintained by our team, might be outdated. | + +Current version: `1.2.0-SNAPSHOT`. Active feature branch: `feature/external-ls-v2`. + +--- + +## Build & Test Commands + +### Java (Maven) + +All Java modules share a parent POM at `java/common/maven-conf/pom.xml`. +Run from `java/` (all modules) or from any individual module directory. + +```bash +# Build everything +cd java && mvn clean install + +# Build a single module (e.g. operator) +cd java/operator/org.eclipse.theia.cloud.operator && mvn clean install + +# Run all tests +mvn test + +# Run a single test class +mvn test -Dtest=WorkspaceResourceTests + +# Run a single test method +mvn test -Dtest=WorkspaceResourceTests#testCreateWorkspace + +# Integration tests (Quarkus @QuarkusTest) +mvn verify + +# Native Quarkus build +mvn install -Pnative + +# Sentry source bundle upload (requires SENTRY_AUTH_TOKEN) +mvn install -Psentry +``` + +### Node / Frontend (`node/`) + +Node ≥ 20 required. + +```bash +cd node +npm install +npm run build # builds all workspaces + monitor +npm run lint # lints all workspaces + monitor +npm run test # runs e2e-tests workspace +``` + +### Theia Extensions (`theia/`) + +Yarn 1.x + Lerna. + +```bash +cd theia +yarn +yarn build # clean + lerna run build +yarn lint # lerna run lint +yarn watch # lerna run watch (dev mode) +``` + +--- + +## Java Code Style + +### File Header + +Every Java file must start with the EPL-2.0 copyright block: + +```java +/******************************************************************************** + * Copyright (C) EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +``` + +### General Conventions + +- **Java version**: 21. Use records, text blocks, and pattern matching where appropriate. +- **Indentation**: 4 spaces, no tabs. +- **Braces**: Egyptian style (opening brace on same line). +- **Line length**: ~120 characters. +- **Imports**: No wildcards. Group: `java.*`, `javax.*`/`jakarta.*`, third-party, project internal. Static imports at top, e.g. `import static org.eclipse.theia.cloud.common.util.LogMessageUtil.formatLogMessage;`. +- **Naming**: PascalCase for classes/records/enums, camelCase for methods/variables, SCREAMING_SNAKE_CASE for constants. +- **Annotations**: `@Inject` (Google Guice in operator; Jakarta in service), `@Singleton`, `@Override` always present. + +### Logging + +- Logger declaration: `private static final Logger LOGGER = LogManager.getLogger(ClassName.class);` (Log4j 2). +- Always pass `correlationId` as first argument via `formatLogMessage(correlationId, "...")`. +- Use `LOGGER.info(...)` for normal flow, `LOGGER.warn(...)` for best-effort failures, `LOGGER.error(...)` for unexpected errors. +- Never log sensitive data (tokens, passwords). + +### Tracing (Sentry) + +Use `Tracing.*` — **not** `SentryHelper.*` (removed): + +```java +ISpan span = Tracing.childSpan("operation.name", "Human description"); +try { + // work + Tracing.finishSuccess(span); +} catch (Exception e) { + Tracing.finishError(span, e); + throw e; +} +``` + +Always finalize spans in `finally` or catch blocks — never leave them open. + +### Error Handling + +- Catch specific exceptions first; add `catch (RuntimeException e)` as a safety net in factory/resource methods so callers always receive `Optional.empty()` rather than propagated exceptions. +- Never use empty catch blocks. +- Service layer: throw `TheiaCloudWebException` (maps to HTTP error responses). +- Operator layer: return `boolean` / `Optional` to signal success/failure; log at `warn`/`error` level. + +### Kubernetes (Fabric8) + +- Always null-check `getEnv()`, `getVolumes()`, `getVolumeMounts()` — they can return `null` on freshly-templated objects. Initialize to `new ArrayList<>()` before mutation. +- Use `TheiaCloudPersistentVolumeUtil.getTheiaContainer(podSpec, appDefSpec)` to locate the Theia container (never match by container name directly). +- Fabric8 `.edit()` lambdas cannot return values; use `AtomicBoolean` / `AtomicReference` to communicate state out of the lambda. + +### Sidecar Specifics (operator module) + +- Sidecar operations are **best-effort**: if `createSidecars()` returns `false`, log a warning but continue the session flow. +- Sidecar configuration is read from `AppDefinitionSpec.getSidecars()` (v1beta11 CRD) **only**. Do not reintroduce `options["langserver-image"]` fallback. +- No hardcoded language detection — sidecar names and languages are fully user-defined in the CRD. +- Sidecar resources (Deployment + Service) are created via Fabric8 builders in `SidecarResourceFactory` — no YAML templates. +- Rollback orphaned sidecar resources via `factory.deleteResources(session, correlationId)`, not via the K8s client directly. +- Eager path: sidecar Deployment+Service must be created **before** Theia Deployment so K8s DNS resolves service names at pod startup. +- Session release (eager): only restart sidecar Pods (`restartPrewarmedSidecarPods`), do **not** delete Deployment+Service — they are reused by the pool. +- Lazy path: use the same naming source for injected sidecar host env vars and created Service names (session-based naming) to avoid DNS mismatch. + +### Session Launch Rules (service module) + +- Ephemeral session launch is **not supported** for sidecar-enabled app definitions. Return `400 Bad Request` and instruct callers to use workspace-backed session launch. +- Sidecar-enabled sessions should be workspace-backed so Theia and sidecar can operate on shared persistent storage. + +--- + +## TypeScript / Frontend Code Style + +Configured via `.prettierrc.js` (in `theia/`) and ESLint with `typescript-eslint` + `simple-import-sort`. + +| Setting | Value | +|---------|-------| +| Indent | 2 spaces | +| Quotes | Single (`'`) | +| JSX quotes | Single | +| Trailing commas | None | +| Print width | 120 (100 for JSON/YAML) | +| Arrow parens | Avoid for single arg | +| End of line | LF | + +- Imports sorted by `eslint-plugin-simple-import-sort`. +- File headers: same EPL-2.0 copyright block as Java (adapted for JS/TS comment syntax). + +--- + +## Key Patterns to Follow + +### Dependency Injection + +- **Operator**: Google Guice — use `@Inject` on fields, bind in a `Module`. +- **Service**: Quarkus/Jakarta CDI — `@Inject`, `@ApplicationScoped`, `@RequestScoped`. +- Never instantiate service classes with `new` inside production code. + +### Optional Usage + +- Prefer `Optional` return types over `null` for "may not exist" results. +- Always check `Optional.isEmpty()` before acting, and log at warn/error if absent when presence was expected. + +### Tests + +- **Framework**: JUnit 5 (`@Test`, `@BeforeEach`), Mockito (`@Mock`, `@InjectMocks`). +- **Quarkus tests**: `@QuarkusTest` + `@InjectMock` for CDI bean injection in tests. +- Test class names end with `Tests` (e.g., `WorkspaceResourceTests`). +- Static imports: `org.junit.jupiter.api.Assertions.*`, `org.mockito.Mockito.*`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 669370037..1e5194dfe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,50 +1,111 @@ # Changelog -## [0.12.0] - estimated 2024-10 +## [1.2.0] - estimated between 2025-11 and 2026-05 + +## [1.1.2] - 2025-09-26 + +- [java/operator] Use ingress path type ImplementationSpecific instead of Prefix [#449](https://github.com/eclipse-theia/theia-cloud/pull/449) + +## [1.1.1] - 2025-08-01 + +- [java/operator] Create missing internal services in eager start app definition handler [#441](https://github.com/eclipse-theia/theia-cloud/pull/441) + +## [1.1.0] - 2025-08-01 + +- [theia] Introduce new folder `theia` for all Theia extensions and an example app to test these [#389](https://github.com/eclipse-theia/theia-cloud/pull/389) +- [node/monitor-theia] Move Theia monitor extension to `theia/extensions/monitor-theia` [#389](https://github.com/eclipse-theia/theia-cloud/pull/389) +- [theia/extensions/monitor-theia] Update Theia dependencies to `^1.55.0` [#389](https://github.com/eclipse-theia/theia-cloud/pull/389) +- [ci] Add Theia CI workflow, add reusable Theia extension publish workflow [#389](https://github.com/eclipse-theia/theia-cloud/pull/388) +- [java] Fix and improve eager start handling [#393](https://github.com/eclipse-theia/theia-cloud/pull/393), [#396](https://github.com/eclipse-theia/theia-cloud/pull/396) +- [java/service] Add admin user concept and app definition scaling endpoint [#400](https://github.com/eclipse-theia/theia-cloud/pull/400) +- [docker] Provide ARM64 images [#404](https://github.com/eclipse-theia/theia-cloud/pull/404), [#408](https://github.com/eclipse-theia/theia-cloud/pull/408) +- [java/common] Truncate labels longer than 63 chars [#413](https://github.com/eclipse-theia/theia-cloud/pull/413), [#417](https://github.com/eclipse-theia/theia-cloud/pull/417) +- [java/conversion] Allow to set conversion webhook certificate reload period [#410](https://github.com/eclipse-theia/theia-cloud/pull/410) +- [all components] Introduce config store Theia extension and REST endpoint [#432](https://github.com/eclipse-theia/theia-cloud/pull/432), [#438](https://github.com/eclipse-theia/theia-cloud/pull/438) + +### Breaking Changes in 1.1.0 + +- [java/common] Changed `LabelsUtil.createSessionLabels` to accept `AppDefinition` instead of `AppDefinitionSpec` + +## [1.0.1] - 2025-03-24 + +- [java/common] Truncate labels longer than 63 chars [#413](https://github.com/eclipse-theia/theia-cloud/pull/413) + +## [1.0.0] - 2024-11-29 + +- [java/operator] Add Theia Cloud Labels on Resources created by operator [#362](https://github.com/eclipse-theia/theia-cloud/pull/362) +- [node] Updated Node Dependencies [#371](https://github.com/eclipse-theia/theia-cloud/pull/371) - contributed on behalf of STMicroelectronics +- [terraform] Update `cert-manager`, `ingress-nginx`, and `keycloak` [#371](https://github.com/eclipse-theia/theia-cloud/pull/371) - contributed on behalf of STMicroelectronics + +## [0.12.0] - 2024-10-30 + +- [All components] Documented naming conventions and updated code to follow [#368](https://github.com/eclipse-theia/theia-cloud/pull/368) +- [documentation] Add REST API markdown docs [#363](https://github.com/eclipse-theia/theia-cloud/pull/363) +- [node/common] Update Common Package to support listing app definitions [#361](https://github.com/eclipse-theia/theia-cloud/pull/361) +- [java/service] Extend service with a list app definitions endpoint [#361](https://github.com/eclipse-theia/theia-cloud/pull/361) +- [documentation] Improved debugging documentation [#354](https://github.com/eclipse-theia/theia-cloud/pull/354) +- [java] Fixed issue where monitor might have stopped session during session startup [#354](https://github.com/eclipse-theia/theia-cloud/pull/354) +- [terraform] Added Devcontainer for GKE demo [#353](https://github.com/eclipse-theia/theia-cloud/pull/353) +- [github] Moved from `eclipsesource` to `eclipse-theia` org as an official Theia subproject [#353](https://github.com/eclipse-theia/theia-cloud/pull/353) [#358](https://github.com/eclipse-theia/theia-cloud/pull/358) +- [node] Updated Node Dependencies [#345](https://github.com/eclipse-theia/theia-cloud/pull/345) - contributed on behalf of STMicroelectronics +- [java] Updated Maven Dependencies [#345](https://github.com/eclipse-theia/theia-cloud/pull/345) - contributed on behalf of STMicroelectronics + +### Breaking Changes in 0.12.0 + +See the helm chart Changelog for [more details](https://github.com/eclipse-theia/theia-cloud-helm/blob/main/CHANGELOG.md#breaking-changes-in-0120). + +We did some renaming in preparation for 1.0.0 + +- NOTE: default namespace renamed from `theiacloud` to `theia-cloud` +- Config Map template label key updated from `theiacloud` to `theia-cloud.io/template-purpose` +- PVC label `theia.cloud.workspace.name` renamed to `theia-cloud.io/workspace-name` +- System property `THEIA_CLOUD_APP_ID` renamed to `THEIACLOUD_APP_ID` +- System property `THEIA_CLOUD_USE_KEYCLOAK` renamed to `THEIACLOUD_USE_KEYCLOAK` +- Theia Monitor paths changed from `/services/theiacloud-` to `/services/theia-cloud-` ## [0.11.0] - 2024-07-23 -- [common] Add option field to CRDs and increase version to `Session.v1beta8`, `Workspace.v1beta5` and `AppDefinition.v1beta10` [#293](https://github.com/eclipsesource/theia-cloud/pull/293) | [#55](https://github.com/eclipsesource/theia-cloud-helm/pull/55) -- [java] Separate operator default implementation from library to allow for easier customization [#303](https://github.com/eclipsesource/theia-cloud/pull/303) -- [node] Unify the existing landing and try now pages to a new ViteJS based landing page [#304](https://github.com/eclipsesource/theia-cloud/pull/304) | [#58](https://github.com/eclipsesource/theia-cloud-helm/pull/58) - contributed on behalf of STMicroelectronics +- [common] Add option field to CRDs and increase version to `Session.v1beta8`, `Workspace.v1beta5` and `AppDefinition.v1beta10` [#293](https://github.com/eclipse-theia/theia-cloud/pull/293) | [#55](https://github.com/eclipse-theia/theia-cloud-helm/pull/55) +- [java] Separate operator default implementation from library to allow for easier customization [#303](https://github.com/eclipse-theia/theia-cloud/pull/303) +- [node] Unify the existing landing and try now pages to a new ViteJS based landing page [#304](https://github.com/eclipse-theia/theia-cloud/pull/304) | [#58](https://github.com/eclipse-theia/theia-cloud-helm/pull/58) - contributed on behalf of STMicroelectronics - The new page is based on the old try now page but uses ViteJS instead of the deprecated Create React App - Extend configuration options for the new landing page for texts and logo file type - Removed terms and conditions - Build the common package as ESM and CJS bundles for extended compatibility -- [common] Add `ingressHostnamePrefixes` list to `AppDefinition.v1beta10` [#298](https://github.com/eclipsesource/theia-cloud/pull/298) | [#57](https://github.com/eclipsesource/theia-cloud-helm/pull/57) -- [java] Improved naming for kubernetes resources [#326](https://github.com/eclipsesource/theia-cloud/pull/326) +- [common] Add `ingressHostnamePrefixes` list to `AppDefinition.v1beta10` [#298](https://github.com/eclipse-theia/theia-cloud/pull/298) | [#57](https://github.com/eclipse-theia/theia-cloud-helm/pull/57) +- [java] Improved naming for kubernetes resources [#326](https://github.com/eclipse-theia/theia-cloud/pull/326) ### Breaking Changes in 0.11.0 -See the helm chart Changelog for [more details](https://github.com/eclipsesource/theia-cloud-helm/blob/main/CHANGELOG.md). +See the helm chart Changelog for [more details](https://github.com/eclipse-theia/theia-cloud-helm/blob/main/CHANGELOG.md). ## [0.10.0] - 2024-04-02 -- [.github/workflows] Improve version detection in workflows (do not build release commits, auto-detect version for demo publishing) [#280](https://github.com/eclipsesource/theia-cloud/pull/280) - contributed on behalf of STMicroelectronics -- [node] Separate `monitor` package from other workspaces to fix bundling the extension [#280](https://github.com/eclipsesource/theia-cloud/pull/280) - contributed on behalf of STMicroelectronics -- [conversion] Provide java conversion webhook for CRD updates [#283](https://github.com/eclipsesource/theia-cloud/pull/283) | [#49](https://github.com/eclipsesource/theia-cloud-helm/pull/49) - contributed on behalf of STMicroelectronics -- [.github/workflows] Add ci for `conversion-webhook` and fix typo to build on version bumps [#283](https://github.com/eclipsesource/theia-cloud/pull/283) | [#49](https://github.com/eclipsesource/theia-cloud-helm/pull/49) - contributed on behalf of STMicroelectronics -- [common] Update CRs, keep previous version and offer Hub (used by conversion-webhook) [#283](https://github.com/eclipsesource/theia-cloud/pull/283) | [#49](https://github.com/eclipsesource/theia-cloud-helm/pull/49) - contributed on behalf of STMicroelectronics +- [.github/workflows] Improve version detection in workflows (do not build release commits, auto-detect version for demo publishing) [#280](https://github.com/eclipse-theia/theia-cloud/pull/280) - contributed on behalf of STMicroelectronics +- [node] Separate `monitor` package from other workspaces to fix bundling the extension [#280](https://github.com/eclipse-theia/theia-cloud/pull/280) - contributed on behalf of STMicroelectronics +- [conversion] Provide java conversion webhook for CRD updates [#283](https://github.com/eclipse-theia/theia-cloud/pull/283) | [#49](https://github.com/eclipse-theia/theia-cloud-helm/pull/49) - contributed on behalf of STMicroelectronics +- [.github/workflows] Add ci for `conversion-webhook` and fix typo to build on version bumps [#283](https://github.com/eclipse-theia/theia-cloud/pull/283) | [#49](https://github.com/eclipse-theia/theia-cloud-helm/pull/49) - contributed on behalf of STMicroelectronics +- [common] Update CRs, keep previous version and offer Hub (used by conversion-webhook) [#283](https://github.com/eclipse-theia/theia-cloud/pull/283) | [#49](https://github.com/eclipse-theia/theia-cloud-helm/pull/49) - contributed on behalf of STMicroelectronics - Move status like fields to status - `Session.v1beta7`: Move `url`, `lastActivity` and `error` fields from the spec to the status. - `Workspace.v1beta4`: Move the `error` field from the spec to the status. Also add the `error` field to `Workspace.v1beta3` as it was missing - Remove `timeout.strategy` from AppDefinition - `AppDefinition.v1beta9`: Removed `timeout.strategy` and `timeout.limit` is now just `timeout`. This was done, as there is only one Strategy left. -- [java] Update io.fabric8.kubernetes-client to version 6.10.0. Also update Quarkus platform to 3.8.1. This provides kubernetes 1.29 support [#287](https://github.com/eclipsesource/theia-cloud/pull/287) -- [terraform] Change terraform values to conform to helm chart changes [#289](https://github.com/eclipsesource/theia-cloud/pull/289) | [#52](https://github.com/eclipsesource/theia-cloud-helm/pull/52) - contributed on behalf of STMicroelectronics +- [java] Update io.fabric8.kubernetes-client to version 6.10.0. Also update Quarkus platform to 3.8.1. This provides kubernetes 1.29 support [#287](https://github.com/eclipse-theia/theia-cloud/pull/287) +- [terraform] Change terraform values to conform to helm chart changes [#289](https://github.com/eclipse-theia/theia-cloud/pull/289) | [#52](https://github.com/eclipse-theia/theia-cloud-helm/pull/52) - contributed on behalf of STMicroelectronics ## [0.9.0] - 2024-01-23 -- [node/landing-page] Make npm workspace and remove yarn references from repo [#258](https://github.com/eclipsesource/theia-cloud/pull/258) | [#45](https://github.com/eclipsesource/theia-cloud-helm/pull/45) - contributed on behalf of STMicroelectronics -- [All components] Align [versioning](https://github.com/eclipsesource/theia-cloud#versioning) between all components and introduce Changelog [#258](https://github.com/eclipsesource/theia-cloud/pull/258) | [#45](https://github.com/eclipsesource/theia-cloud-helm/pull/45) - contributed on behalf of STMicroelectronics -- [All components] Update CRD version scheme for all CRDs from `theia.cloud/vXbeta` to `theia.cloud/v1betaX` [#266](https://github.com/eclipsesource/theia-cloud/pull/266) | [#46](https://github.com/eclipsesource/theia-cloud-helm/pull/46) - contributed on behalf of STMicroelectronics -- [java/service] Update Quarkus from 2.12.0 to 3.3.1 [#266](https://github.com/eclipsesource/theia-cloud/pull/266) - contributed on behalf of STMicroelectronics -- [java] Update fabric8 Kubernetes client from 5.12.2 to 6.7.2 [#266](https://github.com/eclipsesource/theia-cloud/pull/266) - contributed on behalf of STMicroelectronics -- [java/common] Introduce hub-objects for all custom resources [#266](https://github.com/eclipsesource/theia-cloud/pull/266) - contributed on behalf of STMicroelectronics -- [demo] Fix demo applications to Theia's latest Community Release (1.43.1) [#267](https://github.com/eclipsesource/theia-cloud/pull/267) - contributed on behalf of STMicroelectronics -- [node/monitor-theia] Make `@theia` dependencies peerDependencies [#267](https://github.com/eclipsesource/theia-cloud/pull/267) - contributed on behalf of STMicroelectronics -- [node] Update to node 20 [#269](https://github.com/eclipsesource/theia-cloud/pull/269) -- [All components] Clean up repository [#275](https://github.com/eclipsesource/theia-cloud/pull/275) - contributed on behalf of STMicroelectronics +- [node/landing-page] Make npm workspace and remove yarn references from repo [#258](https://github.com/eclipse-theia/theia-cloud/pull/258) | [#45](https://github.com/eclipse-theia/theia-cloud-helm/pull/45) - contributed on behalf of STMicroelectronics +- [All components] Align [versioning](https://github.com/eclipse-theia/theia-cloud#versioning) between all components and introduce Changelog [#258](https://github.com/eclipse-theia/theia-cloud/pull/258) | [#45](https://github.com/eclipse-theia/theia-cloud-helm/pull/45) - contributed on behalf of STMicroelectronics +- [All components] Update CRD version scheme for all CRDs from `theia.cloud/vXbeta` to `theia.cloud/v1betaX` [#266](https://github.com/eclipse-theia/theia-cloud/pull/266) | [#46](https://github.com/eclipse-theia/theia-cloud-helm/pull/46) - contributed on behalf of STMicroelectronics +- [java/service] Update Quarkus from 2.12.0 to 3.3.1 [#266](https://github.com/eclipse-theia/theia-cloud/pull/266) - contributed on behalf of STMicroelectronics +- [java] Update fabric8 Kubernetes client from 5.12.2 to 6.7.2 [#266](https://github.com/eclipse-theia/theia-cloud/pull/266) - contributed on behalf of STMicroelectronics +- [java/common] Introduce hub-objects for all custom resources [#266](https://github.com/eclipse-theia/theia-cloud/pull/266) - contributed on behalf of STMicroelectronics +- [demo] Fix demo applications to Theia's latest Community Release (1.43.1) [#267](https://github.com/eclipse-theia/theia-cloud/pull/267) - contributed on behalf of STMicroelectronics +- [node/monitor-theia] Make `@theia` dependencies peerDependencies [#267](https://github.com/eclipse-theia/theia-cloud/pull/267) - contributed on behalf of STMicroelectronics +- [node] Update to node 20 [#269](https://github.com/eclipse-theia/theia-cloud/pull/269) +- [All components] Clean up repository [#275](https://github.com/eclipse-theia/theia-cloud/pull/275) - contributed on behalf of STMicroelectronics ## [0.8.1] - 2023-10-01 diff --git a/README.md b/README.md index ead3fb9df..15039aa6d 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,15 @@ -# Theia Cloud +# EduIDE Cloud -[![Try online](https://img.shields.io/badge/Try_Theia_Cloud-online-blue?logo=icloud&logoColor=f5f5f5)](https://try.theia-cloud.io/) [![Website](https://img.shields.io/badge/Website-black?style=flat&color=blue)](https://theia-cloud.io/) - -The goal of Theia Cloud is to simplify the deployment of Theia-based (and similar) products on Kubernetes. We follow a convention over configuration approach allowing users to get started fast. At the same time, we aim for extensibility allowing developers to customize all aspects of the kubernetes deployment as required. +EduIDE Cloud is based on the Theia Cloud project. The goal of EduIDE Cloud is to simplify the deployment of Theia-based (and similar) products on Kubernetes. We follow a convention over configuration approach allowing users to get started fast. At the same time, we aim for extensibility allowing developers to customize all aspects of the kubernetes deployment as required. For more information, please also visit [our website](https://theia-cloud.io/). ## Feedback, Help and Support -If you encounter any problems feel free to [open an issue](https://github.com/eclipsesource/theia-cloud/issues/new/choose) on the repo. -For questions and discussions please use the [the Github discussions](https://github.com/eclipsesource/theia-cloud/discussions). +If you encounter any problems feel free to [open an issue](https://github.com/eclipse-theia/theia-cloud/issues/new/choose) on the repo. +For questions and discussions please use the [the Github discussions](https://github.com/eclipse-theia/theia-cloud/discussions). You can also reach us via [email](mailto:support@theia-cloud.io?subject=Theia_Cloud). -In addition, EclipseSource also offers [professional support](https://eclipsesource.com/services/developer-support/) for Theia and Theia Cloud. +In addition, EclipseSource also offers [professional support](https://eclipse-theia.com/services/developer-support/) for Theia and Theia Cloud. ## Components @@ -28,11 +26,15 @@ See [Architecture.md](documentation/Architecture.md) for more information on the This REST Service acts as the API for creating and stopping Theia-based products for an authenticated user as well as providing additional information.\ The Theia Cloud service creates, modifies, and deletes the custom resources the operator listens to. -### Sample Dashboard and reusable JS/UI components +#### Scaling admin endpoints + +Scaling operations for `AppDefinition` resources are exposed through admin endpoints under `/service/admin/appdefinition`. +Access is restricted to callers presenting the `X-Admin-Api-Token` header with the token from `theia.cloud.admin.api.token`. +In Helm-based deployments, provide this token through a pre-created Kubernetes Secret referenced by `service.adminApiTokenSecret`. -Theia Cloud comes with a basic landing page/dashboard which allows to launch sessions. +### Reusable JS/UI components -We offer a common ts library for the API of the Theia Cloud service, which may be reused by clients to write their own custom dashboards. +We offer a common ts library for the API of the Theia Cloud service, which may be reused by clients to write custom dashboards. We plan to offer reusable ui components in the future as well. @@ -45,7 +47,7 @@ From version 0.9.0 onwards, every component in this repository (and the helm cha - **Releases:** Standard releases will occur every three months. We recommend to use these for deployments as those are thoroughly tested and are stable versions. You can then update, after three months, when the next version is available. - **Pre-Releases:** Pre-release versions will be released on every commit. These versions will be tagged as `-next.`. The latest version of a next version is available at `-next`. Pre-releases are ideal for testing the latest features and changes or for making contributions. However we do not recommend, to use those versions in deployments. -The [helm charts](https://github.com/eclipsesource/theia-cloud-helm) are referencing the compatible version in their `appVersion` field. +The [helm charts](https://github.com/eclipse-theia/theia-cloud-helm) are referencing the compatible version in their `appVersion` field. Since, npm does not allow tags that follow Semver, next artifacts published to npm have the `next` tag instead of ``. This means, that those dependencies will be updated to newer version, once they are available. So again, for deployments you should either pin the version to a specific commit or use the released versions. @@ -77,13 +79,23 @@ All components are deployed as docker images and may be built with docker. See [ ## Installation -We offer a helm chart at which may be used to install Theia Cloud. Please check our getting started guides below as well, which will explain the possible values in more detail. +We offer a helm chart at which may be used to install Theia Cloud. Please check our getting started guides below as well, which will explain the possible values in more detail. We offer three charts: - `theia-cloud-base` installs cluster wide resources that may be reused by multiple Theia Cloud installations in different namespaces - `theia-cloud-crds` (starting with version 0.8.1) installs the custom resource definitions for Theia Cloud and migration servers for older custom resources. This may be reused by multiple Theia Cloud installations in different namespaces. -- `theia-cloud` installs the Theia Cloud operators, service, and landing-page. It depends on the two above charts. +- `theia-cloud` installs the Theia Cloud operators and service. It depends on the two above charts. + +### Cluster prerequisites (Gateway API / Envoy Gateway) + +Theia Cloud now configures session traffic using Gateway API resources (`HTTPRoute` + `Gateway`). + +- Gateway API CRDs must be installed on the cluster. +- Envoy Gateway must be installed and used as the Gateway controller (GatewayClass should match your Helm value `gateway.className`, default: `envoy`). +- The operator sets `X-Forwarded-Uri` using Envoy runtime syntax (`%REQ(:PATH)%`) in route rules; on non-Envoy controllers this value may be treated as a literal string. + +For setup details, see [documentation/Install.md](documentation/Install.md). Starting with version 0.8.1 you may use helm upgrade to update to newer Theia Cloud version.\ Older versions (before the introduction of `theia-cloud-crds`) require a manual uninstall and reinstall as well as a manual CRD upgrade step. @@ -91,7 +103,7 @@ Older versions (before the introduction of `theia-cloud-crds`) require a manual ### Add & update Theia Cloud helm repo ```bash -helm repo add theia-cloud-remote https://github.eclipsesource.com/theia-cloud-helm +helm repo add theia-cloud-remote https://eclipse-theia.github.io/theia-cloud-helm helm repo update ``` @@ -100,9 +112,9 @@ helm repo update ```bash helm install theia-cloud-base theia-cloud-remote/theia-cloud-base --set issuer.email=your-mail@example.com -helm install theia-cloud-crds theia-cloud-remote/theia-cloud-crds --namespace theiacloud --create-namespace +helm install theia-cloud-crds theia-cloud-remote/theia-cloud-crds --namespace theia-cloud --create-namespace -helm install theia-cloud theia-cloud-remote/theia-cloud --namespace theiacloud +helm install theia-cloud theia-cloud-remote/theia-cloud --namespace theia-cloud ``` ### Install the current next version @@ -112,21 +124,21 @@ Simply add the `--devel` flag: ```bash helm install theia-cloud-base theia-cloud-remote/theia-cloud-base --set issuer.email=your-mail@example.com --devel -helm install theia-cloud-crds theia-cloud-remote/theia-cloud-crds --namespace theiacloud --create-namespace --devel +helm install theia-cloud-crds theia-cloud-remote/theia-cloud-crds --namespace theia-cloud --create-namespace --devel -helm install theia-cloud theia-cloud-remote/theia-cloud --namespace theiacloud --devel +helm install theia-cloud theia-cloud-remote/theia-cloud --namespace theia-cloud --devel ``` ### Optional: switch to the newly created namespace ```bash -kubectl config set-context --current --namespace=theiacloud +kubectl config set-context --current --namespace=theia-cloud ``` ### Uninstall ```bash -helm uninstall theia-cloud -n theiacloud +helm uninstall theia-cloud -n theia-cloud ``` ### Getting started with diff --git a/SECURITY.md b/SECURITY.md index c0cd05402..ded8fca7e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -16,4 +16,4 @@ Full disclosure to the general public happens once there is a patch, fix, or wor Non-committers will be acknowledged for their security research on disclosure if the issue has been reported responsibly. -Publicly disclosed issues are tagged with the [security label](https://github.com/eclipsesource/theia-cloud/labels/security). +Publicly disclosed issues are tagged with the [security label](https://github.com/eclipse-theia/theia-cloud/labels/security). diff --git a/demo/dockerfiles/demo-operator-custom-template/.project b/demo/dockerfiles/demo-operator-custom-template/.project deleted file mode 100644 index 9d0ea3a53..000000000 --- a/demo/dockerfiles/demo-operator-custom-template/.project +++ /dev/null @@ -1,11 +0,0 @@ - - - demo-operator-custom-template - - - - - - - - diff --git a/demo/dockerfiles/demo-operator-custom-template/templateDeployment.yaml b/demo/dockerfiles/demo-operator-custom-template/templateDeployment.yaml index 686dc6304..3ad484399 100644 --- a/demo/dockerfiles/demo-operator-custom-template/templateDeployment.yaml +++ b/demo/dockerfiles/demo-operator-custom-template/templateDeployment.yaml @@ -22,7 +22,7 @@ spec: containers: - name: oauth2-proxy image: quay.io/oauth2-proxy/oauth2-proxy:v7.4.0 - imagePullPolicy: Always + imagePullPolicy: IfNotPresent args: - --config=/etc/oauth2-proxy.cfg - --authenticated-emails-file=/emails/authenticated-emails-list diff --git a/demo/dockerfiles/demo-theia-docker/.project b/demo/dockerfiles/demo-theia-docker/.project deleted file mode 100644 index 655e42dfc..000000000 --- a/demo/dockerfiles/demo-theia-docker/.project +++ /dev/null @@ -1,11 +0,0 @@ - - - demo-theia-docker - - - - - - - - diff --git a/demo/dockerfiles/demo-theia-docker/Dockerfile b/demo/dockerfiles/demo-theia-docker/Dockerfile index 6fb560cf6..952615a44 100644 --- a/demo/dockerfiles/demo-theia-docker/Dockerfile +++ b/demo/dockerfiles/demo-theia-docker/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/eclipse-theia/theia-blueprint/theia-ide:1.52.0 as production-stage +FROM ghcr.io/eclipse-theia/theia-ide/theia-ide:1.56.1 AS production-stage ENV THEIA_MINI_BROWSER_HOST_PATTERN {{hostname}} ENV THEIA_WEBVIEW_ENDPOINT {{hostname}} @@ -6,10 +6,9 @@ ENV THEIA_WEBVIEW_ENDPOINT {{hostname}} # Copy project COPY --chown=theia:theia project /home/project COPY --chown=theia:theia settings.json /home/theia/theia-blueprint/applications/browser/.theia/settings.json -COPY --chown=theia:theia settings.json /home/theia/theia-blueprint/applications/browser/.theia/settings.json # Build projects once RUN mvn clean verify -f /home/project/java/pom.xml && \ cd /home/project/web && \ npm install && npm run build && \ - cd /home/theia \ No newline at end of file + cd /home/theia diff --git a/demo/dockerfiles/demo-theia-docker/package.json b/demo/dockerfiles/demo-theia-docker/package.json deleted file mode 100644 index 920515cbf..000000000 --- a/demo/dockerfiles/demo-theia-docker/package.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "private": true, - "theia": { - "frontend": { - "config": { - "applicationName": "Theia Cloud Example", - "warnOnPotentiallyInsecureHostPattern": false, - "preferences": { - "files.enableTrash": false - } - } - }, - "backend": { - "config": { - "warnOnPotentiallyInsecureHostPattern": false - } - } - }, - "dependencies": { - "@theia/bulk-edit": "1.43.1", - "@theia/callhierarchy": "1.43.1", - "@theia/console": "1.43.1", - "@theia/core": "1.43.1", - "@theia/debug": "1.43.1", - "@theia/editor": "1.43.1", - "@theia/editor-preview": "1.43.1", - "@theia/electron": "1.43.1", - "@theia/external-terminal": "1.43.1", - "@theia/file-search": "1.43.1", - "@theia/filesystem": "1.43.1", - "@theia/getting-started": "1.43.1", - "@theia/keymaps": "1.43.1", - "@theia/markers": "1.43.1", - "@theia/messages": "1.43.1", - "@theia/metrics": "1.43.1", - "@theia/mini-browser": "1.43.1", - "@theia/monaco": "1.43.1", - "@theia/navigator": "1.43.1", - "@theia/outline-view": "1.43.1", - "@theia/output": "1.43.1", - "@theia/plugin-dev": "1.43.1", - "@theia/plugin-ext": "1.43.1", - "@theia/plugin-ext-vscode": "1.43.1", - "@theia/preferences": "1.43.1", - "@theia/process": "1.43.1", - "@theia/property-view": "1.43.1", - "@theia/scm": "1.43.1", - "@theia/scm-extra": "1.43.1", - "@theia/search-in-workspace": "1.43.1", - "@theia/task": "1.43.1", - "@theia/terminal": "1.43.1", - "@theia/timeline": "1.43.1", - "@theia/toolbar": "1.43.1", - "@theia/typehierarchy": "1.43.1", - "@theia/userstorage": "1.43.1", - "@theia/variable-resolver": "1.43.1", - "@theia/vsx-registry": "1.43.1", - "@theia/workspace": "1.43.1" - }, - "devDependencies": { - "@theia/cli": "1.43.1" - }, - "scripts": { - "preinstall": "node-gyp install" - }, - "theiaPluginsDir": "plugins", - "theiaPlugins": { - "vscode.git": "https://open-vsx.org/api/vscode/git/1.52.1/file/vscode.git-1.52.1.vsix", - "vscode.markdown-language-features": "https://open-vsx.org/api/vscode/markdown-language-features/1.39.2/file/vscode.markdown-language-features-1.39.2.vsix", - "vscode-builtin-extensions-pack": "https://open-vsx.org/api/eclipse-theia/builtin-extension-pack/1.50.1/file/eclipse-theia.builtin-extension-pack-1.50.1.vsix", - "redhat.java": "https://open-vsx.org/api/redhat/java/0.73.0/file/redhat.java-0.73.0.vsix", - "vscjava.vscode-java-debug": "https://open-vsx.org/api/vscjava/vscode-java-debug/0.30.0/file/vscjava.vscode-java-debug-0.30.0.vsix", - "vscjava.vscode-java-test": "https://open-vsx.org/api/vscjava/vscode-java-test/0.26.1/file/vscjava.vscode-java-test-0.26.1.vsix", - "vscjava.vscode-maven": "https://open-vsx.org/api/vscjava/vscode-maven/0.21.2/file/vscjava.vscode-maven-0.21.2.vsix", - "vscjava.vscode-java-dependency": "https://open-vsx.org/api/vscjava/vscode-java-dependency/0.16.0/file/vscjava.vscode-java-dependency-0.16.0.vsix", - "redhat.vscode-xml": "https://open-vsx.org/api/redhat/vscode-xml/0.20.0/file/redhat.vscode-xml-0.20.0.vsix", - "cdt-gdb-vscode": "https://open-vsx.org/api/eclipse-cdt/cdt-gdb-vscode/0.0.92/file/eclipse-cdt.cdt-gdb-vscode-0.0.92.vsix", - "vscode-builtin-cpp": "https://open-vsx.org/api/vscode/cpp/1.62.3/file/vscode.cpp-1.62.3.vsix", - "vscode-clangd": "https://open-vsx.org/api/llvm-vs-code-extensions/vscode-clangd/0.1.15/file/llvm-vs-code-extensions.vscode-clangd-0.1.15.vsix", - "vscode-cmake": "https://open-vsx.org/api/ms-vscode/cmake-tools/1.9.2/file/ms-vscode.cmake-tools-1.9.2.vsix" - }, - "theiaPluginsExcludeIds": [ - "vscode.extension-editing", - "vscode.microsoft-authentication", - "vscode.html-language-features" - ] -} diff --git a/demo/dockerfiles/demo-theia-docker/project/java/pom.xml b/demo/dockerfiles/demo-theia-docker/project/java/pom.xml index c4ff1c434..d228e2ef2 100644 --- a/demo/dockerfiles/demo-theia-docker/project/java/pom.xml +++ b/demo/dockerfiles/demo-theia-docker/project/java/pom.xml @@ -7,7 +7,7 @@ com.example demo - 1.0-SNAPSHOT + 0.0.1 demo @@ -21,7 +21,7 @@ junit junit - 4.11 + 4.13.2 test diff --git a/demo/dockerfiles/demo-theia-monitor-theia/.project b/demo/dockerfiles/demo-theia-monitor-theia/.project deleted file mode 100644 index 19470603a..000000000 --- a/demo/dockerfiles/demo-theia-monitor-theia/.project +++ /dev/null @@ -1,11 +0,0 @@ - - - demo-theia-docker-theia - - - - - - - - diff --git a/demo/dockerfiles/demo-theia-monitor-theia/Dockerfile b/demo/dockerfiles/demo-theia-monitor-theia/Dockerfile index 283c24ef7..bc6c5c819 100644 --- a/demo/dockerfiles/demo-theia-monitor-theia/Dockerfile +++ b/demo/dockerfiles/demo-theia-monitor-theia/Dockerfile @@ -1,19 +1,12 @@ -FROM node:20-bookworm as build-stage +FROM node:20-bookworm AS build-stage RUN apt-get update && apt-get install -y libxkbfile-dev libsecret-1-dev -WORKDIR /home/theia -ADD demo/dockerfiles/demo-theia-monitor-theia/package.json ./package.json -## Link package from local -ADD node ./node -WORKDIR /home/theia/node -RUN npm ci && npm run build -w monitor-theia WORKDIR /home/theia -RUN sed -i 's/"@eclipse-theiacloud\/monitor-theia": "next"/"@eclipse-theiacloud\/monitor-theia": "file:node\/monitor-theia"/' package.json +COPY theia ./ -RUN yarn --pure-lockfile && \ - NODE_OPTIONS="--max_old_space_size=4096" yarn theia build && \ - yarn theia download:plugins && \ - yarn --production && \ +RUN yarn --frozen-lockfile && \ + NODE_OPTIONS="--max_old_space_size=4096" yarn build && \ + yarn --production --frozen-lockfile && \ yarn autoclean --init && \ sed -i '/__tests__\|test\|tests\|powered-test/ s/^/#/' .yarnclean && \ echo *.ts >> .yarnclean && \ @@ -22,7 +15,7 @@ RUN yarn --pure-lockfile && \ yarn autoclean --force && \ yarn cache clean -FROM node:20-bookworm-slim as production-stage +FROM node:20-bookworm-slim AS production-stage # Use fixed user id 101 to guarantee it matches the app definition RUN adduser --system --group --uid 101 theia @@ -31,23 +24,16 @@ RUN chmod g+rw /home && \ mkdir -p /home/theia && \ chown -R theia:theia /home/theia && \ chown -R theia:theia /home/project; -RUN apt-get update && apt-get install -y wget apt-transport-https && \ - wget -O - https://packages.adoptium.net/artifactory/api/gpg/key/public | tee /usr/share/keyrings/adoptium.asc && \ - echo "deb [signed-by=/usr/share/keyrings/adoptium.asc] https://packages.adoptium.net/artifactory/deb $(awk -F= '/^VERSION_CODENAME/{print$2}' /etc/os-release) main" | tee /etc/apt/sources.list.d/adoptium.list && \ - apt-get update && apt-get install -y git openssh-client openssh-server bash libsecret-1-0 temurin-17-jdk maven && \ - apt-get purge -y wget && \ - apt-get clean -ENV HOME /home/theia -ENV THEIA_MINI_BROWSER_HOST_PATTERN {{hostname}} -ENV THEIA_WEBVIEW_ENDPOINT {{hostname}} +ENV HOME=/home/theia +ENV THEIA_MINI_BROWSER_HOST_PATTERN={{hostname}} +ENV THEIA_WEBVIEW_ENDPOINT={{hostname}} WORKDIR /home/theia COPY --from=build-stage --chown=theia:theia /home/theia /home/theia EXPOSE 3000 -ENV SHELL=/bin/bash \ - THEIA_DEFAULT_PLUGINS=local-dir:/home/theia/plugins -ENV USE_LOCAL_GIT true +ENV SHELL=/bin/bash +ENV USE_LOCAL_GIT=true USER theia WORKDIR /home/theia -ENTRYPOINT [ "node", "/home/theia/src-gen/backend/main.js" ] +ENTRYPOINT [ "node", "/home/theia/examples/browser-app/src-gen/backend/main.js" ] CMD [ "/home/project", "--hostname=0.0.0.0" ] \ No newline at end of file diff --git a/demo/dockerfiles/demo-theia-monitor-theia/package.json b/demo/dockerfiles/demo-theia-monitor-theia/package.json deleted file mode 100644 index c7d0e4e3a..000000000 --- a/demo/dockerfiles/demo-theia-monitor-theia/package.json +++ /dev/null @@ -1,89 +0,0 @@ -{ - "private": true, - "theia": { - "frontend": { - "config": { - "applicationName": "Theia Cloud Example", - "warnOnPotentiallyInsecureHostPattern": false, - "preferences": { - "files.enableTrash": false - } - } - }, - "backend": { - "config": { - "warnOnPotentiallyInsecureHostPattern": false - } - } - }, - "dependencies": { - "@theia/bulk-edit": "1.43.1", - "@theia/callhierarchy": "1.43.1", - "@theia/console": "1.43.1", - "@theia/core": "1.43.1", - "@theia/debug": "1.43.1", - "@theia/editor": "1.43.1", - "@theia/editor-preview": "1.43.1", - "@theia/electron": "1.43.1", - "@theia/external-terminal": "1.43.1", - "@theia/file-search": "1.43.1", - "@theia/filesystem": "1.43.1", - "@theia/getting-started": "1.43.1", - "@theia/keymaps": "1.43.1", - "@theia/markers": "1.43.1", - "@theia/messages": "1.43.1", - "@theia/metrics": "1.43.1", - "@theia/mini-browser": "1.43.1", - "@theia/monaco": "1.43.1", - "@theia/navigator": "1.43.1", - "@theia/outline-view": "1.43.1", - "@theia/output": "1.43.1", - "@theia/plugin-dev": "1.43.1", - "@theia/plugin-ext": "1.43.1", - "@theia/plugin-ext-vscode": "1.43.1", - "@theia/preferences": "1.43.1", - "@theia/process": "1.43.1", - "@theia/property-view": "1.43.1", - "@theia/scm": "1.43.1", - "@theia/scm-extra": "1.43.1", - "@theia/search-in-workspace": "1.43.1", - "@theia/task": "1.43.1", - "@theia/terminal": "1.43.1", - "@theia/test": "1.43.1", - "@theia/timeline": "1.43.1", - "@theia/toolbar": "1.43.1", - "@theia/typehierarchy": "1.43.1", - "@theia/userstorage": "1.43.1", - "@theia/variable-resolver": "1.43.1", - "@theia/vsx-registry": "1.43.1", - "@theia/workspace": "1.43.1", - "@eclipse-theiacloud/monitor-theia": "next" - }, - "devDependencies": { - "@theia/cli": "1.43.1" - }, - "scripts": { - "preinstall": "node-gyp install" - }, - "theiaPluginsDir": "plugins", - "theiaPlugins": { - "vscode.git": "https://open-vsx.org/api/vscode/git/1.52.1/file/vscode.git-1.52.1.vsix", - "vscode.markdown-language-features": "https://open-vsx.org/api/vscode/markdown-language-features/1.39.2/file/vscode.markdown-language-features-1.39.2.vsix", - "vscode-builtin-extensions-pack": "https://open-vsx.org/api/eclipse-theia/builtin-extension-pack/1.50.1/file/eclipse-theia.builtin-extension-pack-1.50.1.vsix", - "redhat.java": "https://open-vsx.org/api/redhat/java/0.73.0/file/redhat.java-0.73.0.vsix", - "vscjava.vscode-java-debug": "https://open-vsx.org/api/vscjava/vscode-java-debug/0.30.0/file/vscjava.vscode-java-debug-0.30.0.vsix", - "vscjava.vscode-java-test": "https://open-vsx.org/api/vscjava/vscode-java-test/0.26.1/file/vscjava.vscode-java-test-0.26.1.vsix", - "vscjava.vscode-maven": "https://open-vsx.org/api/vscjava/vscode-maven/0.21.2/file/vscjava.vscode-maven-0.21.2.vsix", - "vscjava.vscode-java-dependency": "https://open-vsx.org/api/vscjava/vscode-java-dependency/0.16.0/file/vscjava.vscode-java-dependency-0.16.0.vsix", - "redhat.vscode-xml": "https://open-vsx.org/api/redhat/vscode-xml/0.20.0/file/redhat.vscode-xml-0.20.0.vsix", - "cdt-gdb-vscode": "https://open-vsx.org/api/eclipse-cdt/cdt-gdb-vscode/0.0.92/file/eclipse-cdt.cdt-gdb-vscode-0.0.92.vsix", - "vscode-builtin-cpp": "https://open-vsx.org/api/vscode/cpp/1.62.3/file/vscode.cpp-1.62.3.vsix", - "vscode-clangd": "https://open-vsx.org/api/llvm-vs-code-extensions/vscode-clangd/0.1.15/file/llvm-vs-code-extensions.vscode-clangd-0.1.15.vsix", - "vscode-cmake": "https://open-vsx.org/api/ms-vscode/cmake-tools/1.9.2/file/ms-vscode.cmake-tools-1.9.2.vsix" - }, - "theiaPluginsExcludeIds": [ - "vscode.extension-editing", - "vscode.microsoft-authentication", - "vscode.html-language-features" - ] -} diff --git a/demo/dockerfiles/demo-theia-monitor-theia/yarn.lock b/demo/dockerfiles/demo-theia-monitor-theia/yarn.lock new file mode 100644 index 000000000..1c9241265 --- /dev/null +++ b/demo/dockerfiles/demo-theia-monitor-theia/yarn.lock @@ -0,0 +1,8539 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@ampproject/remapping@^2.2.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" + integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0", "@babel/code-frame@^7.26.2": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" + integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== + dependencies: + "@babel/helper-validator-identifier" "^7.25.9" + js-tokens "^4.0.0" + picocolors "^1.0.0" + +"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.25.9", "@babel/compat-data@^7.26.0": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.3.tgz#99488264a56b2aded63983abd6a417f03b92ed02" + integrity sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g== + +"@babel/core@^7.10.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.0.tgz#d78b6023cc8f3114ccf049eb219613f74a747b40" + integrity sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.26.0" + "@babel/generator" "^7.26.0" + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-module-transforms" "^7.26.0" + "@babel/helpers" "^7.26.0" + "@babel/parser" "^7.26.0" + "@babel/template" "^7.25.9" + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.26.0" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.26.0", "@babel/generator@^7.26.3": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.3.tgz#ab8d4360544a425c90c248df7059881f4b2ce019" + integrity sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ== + dependencies: + "@babel/parser" "^7.26.3" + "@babel/types" "^7.26.3" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^3.0.2" + +"@babel/helper-annotate-as-pure@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz#d8eac4d2dc0d7b6e11fa6e535332e0d3184f06b4" + integrity sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g== + dependencies: + "@babel/types" "^7.25.9" + +"@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz#55af025ce365be3cdc0c1c1e56c6af617ce88875" + integrity sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ== + dependencies: + "@babel/compat-data" "^7.25.9" + "@babel/helper-validator-option" "^7.25.9" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-create-class-features-plugin@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz#7644147706bb90ff613297d49ed5266bde729f83" + integrity sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-member-expression-to-functions" "^7.25.9" + "@babel/helper-optimise-call-expression" "^7.25.9" + "@babel/helper-replace-supers" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + "@babel/traverse" "^7.25.9" + semver "^6.3.1" + +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.25.9": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.26.3.tgz#5169756ecbe1d95f7866b90bb555b022595302a0" + integrity sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.9" + regexpu-core "^6.2.0" + semver "^6.3.1" + +"@babel/helper-define-polyfill-provider@^0.6.2", "@babel/helper-define-polyfill-provider@^0.6.3": + version "0.6.3" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz#f4f2792fae2ef382074bc2d713522cf24e6ddb21" + integrity sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg== + dependencies: + "@babel/helper-compilation-targets" "^7.22.6" + "@babel/helper-plugin-utils" "^7.22.5" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + +"@babel/helper-member-expression-to-functions@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz#9dfffe46f727005a5ea29051ac835fb735e4c1a3" + integrity sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ== + dependencies: + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + +"@babel/helper-module-imports@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz#e7f8d20602ebdbf9ebbea0a0751fb0f2a4141715" + integrity sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw== + dependencies: + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + +"@babel/helper-module-transforms@^7.25.9", "@babel/helper-module-transforms@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz#8ce54ec9d592695e58d84cd884b7b5c6a2fdeeae" + integrity sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw== + dependencies: + "@babel/helper-module-imports" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@babel/traverse" "^7.25.9" + +"@babel/helper-optimise-call-expression@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz#3324ae50bae7e2ab3c33f60c9a877b6a0146b54e" + integrity sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ== + dependencies: + "@babel/types" "^7.25.9" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz#9cbdd63a9443a2c92a725cca7ebca12cc8dd9f46" + integrity sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw== + +"@babel/helper-remap-async-to-generator@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz#e53956ab3d5b9fb88be04b3e2f31b523afd34b92" + integrity sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-wrap-function" "^7.25.9" + "@babel/traverse" "^7.25.9" + +"@babel/helper-replace-supers@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz#ba447224798c3da3f8713fc272b145e33da6a5c5" + integrity sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.25.9" + "@babel/helper-optimise-call-expression" "^7.25.9" + "@babel/traverse" "^7.25.9" + +"@babel/helper-skip-transparent-expression-wrappers@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz#0b2e1b62d560d6b1954893fd2b705dc17c91f0c9" + integrity sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA== + dependencies: + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + +"@babel/helper-string-parser@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" + integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== + +"@babel/helper-validator-identifier@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" + integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== + +"@babel/helper-validator-option@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz#86e45bd8a49ab7e03f276577f96179653d41da72" + integrity sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw== + +"@babel/helper-wrap-function@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz#d99dfd595312e6c894bd7d237470025c85eea9d0" + integrity sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g== + dependencies: + "@babel/template" "^7.25.9" + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + +"@babel/helpers@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.0.tgz#30e621f1eba5aa45fe6f4868d2e9154d884119a4" + integrity sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw== + dependencies: + "@babel/template" "^7.25.9" + "@babel/types" "^7.26.0" + +"@babel/parser@^7.25.9", "@babel/parser@^7.26.0", "@babel/parser@^7.26.3": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.3.tgz#8c51c5db6ddf08134af1ddbacf16aaab48bac234" + integrity sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA== + dependencies: + "@babel/types" "^7.26.3" + +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz#cc2e53ebf0a0340777fff5ed521943e253b4d8fe" + integrity sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/traverse" "^7.25.9" + +"@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz#af9e4fb63ccb8abcb92375b2fcfe36b60c774d30" + integrity sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz#e8dc26fcd616e6c5bf2bd0d5a2c151d4f92a9137" + integrity sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz#807a667f9158acac6f6164b4beb85ad9ebc9e1d1" + integrity sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + "@babel/plugin-transform-optional-chaining" "^7.25.9" + +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz#de7093f1e7deaf68eadd7cc6b07f2ab82543269e" + integrity sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/traverse" "^7.25.9" + +"@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": + version "7.21.0-placeholder-for-preset-env.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" + integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== + +"@babel/plugin-syntax-import-assertions@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz#620412405058efa56e4a564903b79355020f445f" + integrity sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-syntax-import-attributes@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz#3b1412847699eea739b4f2602c74ce36f6b0b0f7" + integrity sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-syntax-unicode-sets-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" + integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-arrow-functions@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz#7821d4410bee5daaadbb4cdd9a6649704e176845" + integrity sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-async-generator-functions@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.9.tgz#1b18530b077d18a407c494eb3d1d72da505283a2" + integrity sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-remap-async-to-generator" "^7.25.9" + "@babel/traverse" "^7.25.9" + +"@babel/plugin-transform-async-to-generator@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz#c80008dacae51482793e5a9c08b39a5be7e12d71" + integrity sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ== + dependencies: + "@babel/helper-module-imports" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-remap-async-to-generator" "^7.25.9" + +"@babel/plugin-transform-block-scoped-functions@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.9.tgz#5700691dbd7abb93de300ca7be94203764fce458" + integrity sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-block-scoping@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz#c33665e46b06759c93687ca0f84395b80c0473a1" + integrity sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-class-properties@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz#a8ce84fedb9ad512549984101fa84080a9f5f51f" + integrity sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-class-static-block@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz#6c8da219f4eb15cae9834ec4348ff8e9e09664a0" + integrity sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-classes@^7.10.0", "@babel/plugin-transform-classes@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz#7152457f7880b593a63ade8a861e6e26a4469f52" + integrity sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-replace-supers" "^7.25.9" + "@babel/traverse" "^7.25.9" + globals "^11.1.0" + +"@babel/plugin-transform-computed-properties@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz#db36492c78460e534b8852b1d5befe3c923ef10b" + integrity sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/template" "^7.25.9" + +"@babel/plugin-transform-destructuring@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz#966ea2595c498224340883602d3cfd7a0c79cea1" + integrity sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-dotall-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz#bad7945dd07734ca52fe3ad4e872b40ed09bb09a" + integrity sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-duplicate-keys@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz#8850ddf57dce2aebb4394bb434a7598031059e6d" + integrity sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz#6f7259b4de127721a08f1e5165b852fcaa696d31" + integrity sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-dynamic-import@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz#23e917de63ed23c6600c5dd06d94669dce79f7b8" + integrity sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-exponentiation-operator@^7.25.9": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz#e29f01b6de302c7c2c794277a48f04a9ca7f03bc" + integrity sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-export-namespace-from@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz#90745fe55053394f554e40584cda81f2c8a402a2" + integrity sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-for-of@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz#4bdc7d42a213397905d89f02350c5267866d5755" + integrity sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + +"@babel/plugin-transform-function-name@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz#939d956e68a606661005bfd550c4fc2ef95f7b97" + integrity sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA== + dependencies: + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/traverse" "^7.25.9" + +"@babel/plugin-transform-json-strings@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz#c86db407cb827cded902a90c707d2781aaa89660" + integrity sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-literals@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz#1a1c6b4d4aa59bc4cad5b6b3a223a0abd685c9de" + integrity sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-logical-assignment-operators@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz#b19441a8c39a2fda0902900b306ea05ae1055db7" + integrity sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-member-expression-literals@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz#63dff19763ea64a31f5e6c20957e6a25e41ed5de" + integrity sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-modules-amd@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz#49ba478f2295101544abd794486cd3088dddb6c5" + integrity sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw== + dependencies: + "@babel/helper-module-transforms" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-modules-commonjs@^7.25.9": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz#8f011d44b20d02c3de44d8850d971d8497f981fb" + integrity sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ== + dependencies: + "@babel/helper-module-transforms" "^7.26.0" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-modules-systemjs@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz#8bd1b43836269e3d33307151a114bcf3ba6793f8" + integrity sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA== + dependencies: + "@babel/helper-module-transforms" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@babel/traverse" "^7.25.9" + +"@babel/plugin-transform-modules-umd@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz#6710079cdd7c694db36529a1e8411e49fcbf14c9" + integrity sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw== + dependencies: + "@babel/helper-module-transforms" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz#454990ae6cc22fd2a0fa60b3a2c6f63a38064e6a" + integrity sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-new-target@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz#42e61711294b105c248336dcb04b77054ea8becd" + integrity sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-nullish-coalescing-operator@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.9.tgz#bcb1b0d9e948168102d5f7104375ca21c3266949" + integrity sha512-ENfftpLZw5EItALAD4WsY/KUWvhUlZndm5GC7G3evUsVeSJB6p0pBeLQUnRnBCBx7zV0RKQjR9kCuwrsIrjWog== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-numeric-separator@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz#bfed75866261a8b643468b0ccfd275f2033214a1" + integrity sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-object-rest-spread@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz#0203725025074164808bcf1a2cfa90c652c99f18" + integrity sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg== + dependencies: + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/plugin-transform-parameters" "^7.25.9" + +"@babel/plugin-transform-object-super@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz#385d5de135162933beb4a3d227a2b7e52bb4cf03" + integrity sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-replace-supers" "^7.25.9" + +"@babel/plugin-transform-optional-catch-binding@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz#10e70d96d52bb1f10c5caaac59ac545ea2ba7ff3" + integrity sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-optional-chaining@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz#e142eb899d26ef715435f201ab6e139541eee7dd" + integrity sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + +"@babel/plugin-transform-parameters@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz#b856842205b3e77e18b7a7a1b94958069c7ba257" + integrity sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-private-methods@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz#847f4139263577526455d7d3223cd8bda51e3b57" + integrity sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-private-property-in-object@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz#9c8b73e64e6cc3cbb2743633885a7dd2c385fe33" + integrity sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-property-literals@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz#d72d588bd88b0dec8b62e36f6fda91cedfe28e3f" + integrity sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-regenerator@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz#03a8a4670d6cebae95305ac6defac81ece77740b" + integrity sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + regenerator-transform "^0.15.2" + +"@babel/plugin-transform-regexp-modifiers@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz#2f5837a5b5cd3842a919d8147e9903cc7455b850" + integrity sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-reserved-words@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz#0398aed2f1f10ba3f78a93db219b27ef417fb9ce" + integrity sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-runtime@^7.10.0": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.25.9.tgz#62723ea3f5b31ffbe676da9d6dae17138ae580ea" + integrity sha512-nZp7GlEl+yULJrClz0SwHPqir3lc0zsPrDHQUcxGspSL7AKrexNSEfTbfqnDNJUO13bgKyfuOLMF8Xqtu8j3YQ== + dependencies: + "@babel/helper-module-imports" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + babel-plugin-polyfill-corejs2 "^0.4.10" + babel-plugin-polyfill-corejs3 "^0.10.6" + babel-plugin-polyfill-regenerator "^0.6.1" + semver "^6.3.1" + +"@babel/plugin-transform-shorthand-properties@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz#bb785e6091f99f826a95f9894fc16fde61c163f2" + integrity sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-spread@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz#24a35153931b4ba3d13cec4a7748c21ab5514ef9" + integrity sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + +"@babel/plugin-transform-sticky-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz#c7f02b944e986a417817b20ba2c504dfc1453d32" + integrity sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-template-literals@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz#6dbd4a24e8fad024df76d1fac6a03cf413f60fe1" + integrity sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-typeof-symbol@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.9.tgz#224ba48a92869ddbf81f9b4a5f1204bbf5a2bc4b" + integrity sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-unicode-escapes@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz#a75ef3947ce15363fccaa38e2dd9bc70b2788b82" + integrity sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-unicode-property-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz#a901e96f2c1d071b0d1bb5dc0d3c880ce8f53dd3" + integrity sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-unicode-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz#5eae747fe39eacf13a8bd006a4fb0b5d1fa5e9b1" + integrity sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-unicode-sets-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz#65114c17b4ffc20fa5b163c63c70c0d25621fabe" + integrity sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/preset-env@^7.10.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.26.0.tgz#30e5c6bc1bcc54865bff0c5a30f6d4ccdc7fa8b1" + integrity sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw== + dependencies: + "@babel/compat-data" "^7.26.0" + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-validator-option" "^7.25.9" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.25.9" + "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.25.9" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.25.9" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.25.9" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.25.9" + "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" + "@babel/plugin-syntax-import-assertions" "^7.26.0" + "@babel/plugin-syntax-import-attributes" "^7.26.0" + "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" + "@babel/plugin-transform-arrow-functions" "^7.25.9" + "@babel/plugin-transform-async-generator-functions" "^7.25.9" + "@babel/plugin-transform-async-to-generator" "^7.25.9" + "@babel/plugin-transform-block-scoped-functions" "^7.25.9" + "@babel/plugin-transform-block-scoping" "^7.25.9" + "@babel/plugin-transform-class-properties" "^7.25.9" + "@babel/plugin-transform-class-static-block" "^7.26.0" + "@babel/plugin-transform-classes" "^7.25.9" + "@babel/plugin-transform-computed-properties" "^7.25.9" + "@babel/plugin-transform-destructuring" "^7.25.9" + "@babel/plugin-transform-dotall-regex" "^7.25.9" + "@babel/plugin-transform-duplicate-keys" "^7.25.9" + "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.25.9" + "@babel/plugin-transform-dynamic-import" "^7.25.9" + "@babel/plugin-transform-exponentiation-operator" "^7.25.9" + "@babel/plugin-transform-export-namespace-from" "^7.25.9" + "@babel/plugin-transform-for-of" "^7.25.9" + "@babel/plugin-transform-function-name" "^7.25.9" + "@babel/plugin-transform-json-strings" "^7.25.9" + "@babel/plugin-transform-literals" "^7.25.9" + "@babel/plugin-transform-logical-assignment-operators" "^7.25.9" + "@babel/plugin-transform-member-expression-literals" "^7.25.9" + "@babel/plugin-transform-modules-amd" "^7.25.9" + "@babel/plugin-transform-modules-commonjs" "^7.25.9" + "@babel/plugin-transform-modules-systemjs" "^7.25.9" + "@babel/plugin-transform-modules-umd" "^7.25.9" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.25.9" + "@babel/plugin-transform-new-target" "^7.25.9" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.25.9" + "@babel/plugin-transform-numeric-separator" "^7.25.9" + "@babel/plugin-transform-object-rest-spread" "^7.25.9" + "@babel/plugin-transform-object-super" "^7.25.9" + "@babel/plugin-transform-optional-catch-binding" "^7.25.9" + "@babel/plugin-transform-optional-chaining" "^7.25.9" + "@babel/plugin-transform-parameters" "^7.25.9" + "@babel/plugin-transform-private-methods" "^7.25.9" + "@babel/plugin-transform-private-property-in-object" "^7.25.9" + "@babel/plugin-transform-property-literals" "^7.25.9" + "@babel/plugin-transform-regenerator" "^7.25.9" + "@babel/plugin-transform-regexp-modifiers" "^7.26.0" + "@babel/plugin-transform-reserved-words" "^7.25.9" + "@babel/plugin-transform-shorthand-properties" "^7.25.9" + "@babel/plugin-transform-spread" "^7.25.9" + "@babel/plugin-transform-sticky-regex" "^7.25.9" + "@babel/plugin-transform-template-literals" "^7.25.9" + "@babel/plugin-transform-typeof-symbol" "^7.25.9" + "@babel/plugin-transform-unicode-escapes" "^7.25.9" + "@babel/plugin-transform-unicode-property-regex" "^7.25.9" + "@babel/plugin-transform-unicode-regex" "^7.25.9" + "@babel/plugin-transform-unicode-sets-regex" "^7.25.9" + "@babel/preset-modules" "0.1.6-no-external-plugins" + babel-plugin-polyfill-corejs2 "^0.4.10" + babel-plugin-polyfill-corejs3 "^0.10.6" + babel-plugin-polyfill-regenerator "^0.6.1" + core-js-compat "^3.38.1" + semver "^6.3.1" + +"@babel/preset-modules@0.1.6-no-external-plugins": + version "0.1.6-no-external-plugins" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz#ccb88a2c49c817236861fee7826080573b8a923a" + integrity sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/types" "^7.4.4" + esutils "^2.0.2" + +"@babel/runtime@^7.10.0", "@babel/runtime@^7.8.4": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" + integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== + dependencies: + regenerator-runtime "^0.14.0" + +"@babel/template@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.9.tgz#ecb62d81a8a6f5dc5fe8abfc3901fc52ddf15016" + integrity sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg== + dependencies: + "@babel/code-frame" "^7.25.9" + "@babel/parser" "^7.25.9" + "@babel/types" "^7.25.9" + +"@babel/traverse@^7.25.9": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.3.tgz#1ebfc75bd748d8f96b3cc63af5e82ebd4c37ba35" + integrity sha512-yTmc8J+Sj8yLzwr4PD5Xb/WF3bOYu2C2OoSZPzbuqRm4n98XirsbzaX+GloeO376UnSYIYJ4NCanwV5/ugZkwA== + dependencies: + "@babel/code-frame" "^7.26.2" + "@babel/generator" "^7.26.3" + "@babel/parser" "^7.26.3" + "@babel/template" "^7.25.9" + "@babel/types" "^7.26.3" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.26.3", "@babel/types@^7.4.4": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.3.tgz#37e79830f04c2b5687acc77db97fbc75fb81f3c0" + integrity sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA== + dependencies: + "@babel/helper-string-parser" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + +"@discoveryjs/json-ext@^0.5.0": + version "0.5.7" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" + integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== + +"@eclipse-theiacloud/monitor-theia@file:node/monitor-theia": + version "1.1.0-next" + +"@electron/get@^2.0.0": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@electron/get/-/get-2.0.3.tgz#fba552683d387aebd9f3fcadbcafc8e12ee4f960" + integrity sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ== + dependencies: + debug "^4.1.1" + env-paths "^2.2.0" + fs-extra "^8.1.0" + got "^11.8.5" + progress "^2.0.3" + semver "^6.2.0" + sumchecker "^3.0.1" + optionalDependencies: + global-agent "^3.0.0" + +"@gar/promisify@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" + integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + 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" + +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/source-map@^0.3.3": + version "0.3.6" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a" + integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + +"@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@malept/cross-spawn-promise@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz#d0772de1aa680a0bfb9ba2f32b4c828c7857cb9d" + integrity sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg== + dependencies: + cross-spawn "^7.0.1" + +"@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz#9edec61b22c3082018a79f6d1c30289ddf3d9d11" + integrity sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw== + +"@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz#33677a275204898ad8acbf62734fc4dc0b6a4855" + integrity sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw== + +"@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz#19edf7cdc2e7063ee328403c1d895a86dd28f4bb" + integrity sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg== + +"@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz#94fb0543ba2e28766c3fc439cabbe0440ae70159" + integrity sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw== + +"@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz#4a0609ab5fe44d07c9c60a11e4484d3c38bbd6e3" + integrity sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg== + +"@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz#0aa5502d547b57abfc4ac492de68e2006e417242" + integrity sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ== + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@npmcli/fs@^2.1.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-2.1.2.tgz#a9e2541a4a2fec2e69c29b35e6060973da79b865" + integrity sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ== + dependencies: + "@gar/promisify" "^1.1.3" + semver "^7.3.5" + +"@npmcli/move-file@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-2.0.1.tgz#26f6bdc379d87f75e55739bab89db525b06100e4" + integrity sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ== + dependencies: + mkdirp "^1.0.4" + rimraf "^3.0.2" + +"@parcel/watcher-android-arm64@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz#e32d3dda6647791ee930556aee206fcd5ea0fb7a" + integrity sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ== + +"@parcel/watcher-darwin-arm64@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz#0d9e680b7e9ec1c8f54944f1b945aa8755afb12f" + integrity sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw== + +"@parcel/watcher-darwin-x64@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz#f9f1d5ce9d5878d344f14ef1856b7a830c59d1bb" + integrity sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA== + +"@parcel/watcher-freebsd-x64@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz#2b77f0c82d19e84ff4c21de6da7f7d096b1a7e82" + integrity sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw== + +"@parcel/watcher-linux-arm-glibc@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz#92ed322c56dbafa3d2545dcf2803334aee131e42" + integrity sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA== + +"@parcel/watcher-linux-arm-musl@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz#cd48e9bfde0cdbbd2ecd9accfc52967e22f849a4" + integrity sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA== + +"@parcel/watcher-linux-arm64-glibc@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz#7b81f6d5a442bb89fbabaf6c13573e94a46feb03" + integrity sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA== + +"@parcel/watcher-linux-arm64-musl@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz#dcb8ff01077cdf59a18d9e0a4dff7a0cfe5fd732" + integrity sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q== + +"@parcel/watcher-linux-x64-glibc@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz#2e254600fda4e32d83942384d1106e1eed84494d" + integrity sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw== + +"@parcel/watcher-linux-x64-musl@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz#01fcea60fedbb3225af808d3f0a7b11229792eef" + integrity sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA== + +"@parcel/watcher-win32-arm64@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz#87cdb16e0783e770197e52fb1dc027bb0c847154" + integrity sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig== + +"@parcel/watcher-win32-ia32@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz#778c39b56da33e045ba21c678c31a9f9d7c6b220" + integrity sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA== + +"@parcel/watcher-win32-x64@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz#33873876d0bbc588aacce38e90d1d7480ce81cb7" + integrity sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw== + +"@parcel/watcher@^2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.5.0.tgz#5c88818b12b8de4307a9d3e6dc3e28eba0dfbd10" + integrity sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ== + dependencies: + detect-libc "^1.0.3" + is-glob "^4.0.3" + micromatch "^4.0.5" + node-addon-api "^7.0.0" + optionalDependencies: + "@parcel/watcher-android-arm64" "2.5.0" + "@parcel/watcher-darwin-arm64" "2.5.0" + "@parcel/watcher-darwin-x64" "2.5.0" + "@parcel/watcher-freebsd-x64" "2.5.0" + "@parcel/watcher-linux-arm-glibc" "2.5.0" + "@parcel/watcher-linux-arm-musl" "2.5.0" + "@parcel/watcher-linux-arm64-glibc" "2.5.0" + "@parcel/watcher-linux-arm64-musl" "2.5.0" + "@parcel/watcher-linux-x64-glibc" "2.5.0" + "@parcel/watcher-linux-x64-musl" "2.5.0" + "@parcel/watcher-win32-arm64" "2.5.0" + "@parcel/watcher-win32-ia32" "2.5.0" + "@parcel/watcher-win32-x64" "2.5.0" + +"@phosphor/algorithm@1", "@phosphor/algorithm@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@phosphor/algorithm/-/algorithm-1.2.0.tgz#4a19aa59261b7270be696672dc3f0663f7bef152" + integrity sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA== + +"@phosphor/collections@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@phosphor/collections/-/collections-1.2.0.tgz#a8cdd0edc0257de7c33306a91caf47910036307f" + integrity sha512-T9/0EjSuY6+ga2LIFRZ0xupciOR3Qnyy8Q95lhGTC0FXZUFwC8fl9e8On6IcwasCszS+1n8dtZUWSIynfgdpzw== + dependencies: + "@phosphor/algorithm" "^1.2.0" + +"@phosphor/commands@1", "@phosphor/commands@^1.7.2": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@phosphor/commands/-/commands-1.7.2.tgz#df724f2896ae43c4a3a9e2b5a6445a15e0d60487" + integrity sha512-iSyBIWMHsus323BVEARBhuVZNnVel8USo+FIPaAxGcq+icTSSe6+NtSxVQSmZblGN6Qm4iw6I6VtiSx0e6YDgQ== + dependencies: + "@phosphor/algorithm" "^1.2.0" + "@phosphor/coreutils" "^1.3.1" + "@phosphor/disposable" "^1.3.1" + "@phosphor/domutils" "^1.1.4" + "@phosphor/keyboard" "^1.1.3" + "@phosphor/signaling" "^1.3.1" + +"@phosphor/coreutils@1", "@phosphor/coreutils@^1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@phosphor/coreutils/-/coreutils-1.3.1.tgz#441e34f42340f7faa742a88b2a181947a88d7226" + integrity sha512-9OHCn8LYRcPU/sbHm5v7viCA16Uev3gbdkwqoQqlV+EiauDHl70jmeL7XVDXdigl66Dz0LI11C99XOxp+s3zOA== + +"@phosphor/disposable@^1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@phosphor/disposable/-/disposable-1.3.1.tgz#be98fe12bd8c9a4600741cb83b0a305df28628f3" + integrity sha512-0NGzoTXTOizWizK/brKKd5EjJhuuEH4903tLika7q6wl/u0tgneJlTh7R+MBVeih0iNxtuJAfBa3IEY6Qmj+Sw== + dependencies: + "@phosphor/algorithm" "^1.2.0" + "@phosphor/signaling" "^1.3.1" + +"@phosphor/domutils@1", "@phosphor/domutils@^1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@phosphor/domutils/-/domutils-1.1.4.tgz#4c6aecf7902d3793b45db325319340e0a0b5543b" + integrity sha512-ivwq5TWjQpKcHKXO8PrMl+/cKqbgxPClPiCKc1gwbMd+6hnW5VLwNG0WBzJTxCzXK43HxX18oH+tOZ3E04wc3w== + +"@phosphor/dragdrop@1", "@phosphor/dragdrop@^1.4.1": + version "1.4.1" + resolved "https://registry.yarnpkg.com/@phosphor/dragdrop/-/dragdrop-1.4.1.tgz#45887dfe8f5849db2b4d1c0329a377f0f0854464" + integrity sha512-77paMoubIWk7pdwA2GVFkqba1WP48hTZZvS17N30+KVOeWfSqBL3flPSnW2yC4y6FnOP2PFOCtuPIbQv+pYhCA== + dependencies: + "@phosphor/coreutils" "^1.3.1" + "@phosphor/disposable" "^1.3.1" + +"@phosphor/keyboard@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@phosphor/keyboard/-/keyboard-1.1.3.tgz#e5fd13af0479034ef0b5fffcf43ef2d4a266b5b6" + integrity sha512-dzxC/PyHiD6mXaESRy6PZTd9JeK+diwG1pyngkyUf127IXOEzubTIbu52VSdpGBklszu33ws05BAGDa4oBE4mQ== + +"@phosphor/messaging@1", "@phosphor/messaging@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@phosphor/messaging/-/messaging-1.3.0.tgz#a140e6dd28a496260779acf74860f738c654c65e" + integrity sha512-k0JE+BTMKlkM335S2AmmJxoYYNRwOdW5jKBqLgjJdGRvUQkM0+2i60ahM45+J23atGJDv9esKUUBINiKHFhLew== + dependencies: + "@phosphor/algorithm" "^1.2.0" + "@phosphor/collections" "^1.2.0" + +"@phosphor/properties@1", "@phosphor/properties@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@phosphor/properties/-/properties-1.1.3.tgz#63e4355be5e22a411c566fd1860207038f171598" + integrity sha512-GiglqzU77s6+tFVt6zPq9uuyu/PLQPFcqZt914ZhJ4cN/7yNI/SLyMzpYZ56IRMXvzK9TUgbRna6URE3XAwFUg== + +"@phosphor/signaling@1", "@phosphor/signaling@^1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@phosphor/signaling/-/signaling-1.3.1.tgz#1cd10b069bdb2c9adb3ba74245b30141e5afc2d7" + integrity sha512-Eq3wVCPQAhUd9+gUGaYygMr+ov7dhSGblSBXiDzpZlSIfa8OVD4P3cCvYXr/acDTNmZ/gHTcSFO8/n3rDkeXzg== + dependencies: + "@phosphor/algorithm" "^1.2.0" + +"@phosphor/virtualdom@1", "@phosphor/virtualdom@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@phosphor/virtualdom/-/virtualdom-1.2.0.tgz#6a233312f817eb02555a0359c4ae3e501fa62bca" + integrity sha512-L9mKNhK2XtVjzjuHLG2uYuepSz8uPyu6vhF4EgCP0rt0TiLYaZeHwuNu3XeFbul9DMOn49eBpye/tfQVd4Ks+w== + dependencies: + "@phosphor/algorithm" "^1.2.0" + +"@phosphor/widgets@1": + version "1.9.3" + resolved "https://registry.yarnpkg.com/@phosphor/widgets/-/widgets-1.9.3.tgz#b8b7ad69fd7cc7af8e8c312ebead0e0965a4cefd" + integrity sha512-61jsxloDrW/+WWQs8wOgsS5waQ/MSsXBuhONt0o6mtdeL93HVz7CYO5krOoot5owammfF6oX1z0sDaUYIYgcPA== + dependencies: + "@phosphor/algorithm" "^1.2.0" + "@phosphor/commands" "^1.7.2" + "@phosphor/coreutils" "^1.3.1" + "@phosphor/disposable" "^1.3.1" + "@phosphor/domutils" "^1.1.4" + "@phosphor/dragdrop" "^1.4.1" + "@phosphor/keyboard" "^1.1.3" + "@phosphor/messaging" "^1.3.0" + "@phosphor/properties" "^1.1.3" + "@phosphor/signaling" "^1.3.1" + "@phosphor/virtualdom" "^1.2.0" + +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + +"@puppeteer/browsers@2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@puppeteer/browsers/-/browsers-2.3.1.tgz#238200dbdce5c00ae28c8f2a55ac053c3be71668" + integrity sha512-uK7o3hHkK+naEobMSJ+2ySYyXtQkBxIH8Gn4MK9ciePjNV+Pf+PgY/W7iPzn2MTjl3stcYB5AlcTmPYw7AXDwA== + dependencies: + debug "^4.3.6" + extract-zip "^2.0.1" + progress "^2.0.3" + proxy-agent "^6.4.0" + semver "^7.6.3" + tar-fs "^3.0.6" + unbzip2-stream "^1.4.3" + yargs "^17.7.2" + +"@sindresorhus/df@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@sindresorhus/df/-/df-1.0.1.tgz#c69b66f52f6fcdd287c807df210305dbaf78500d" + integrity sha512-1Hyp7NQnD/u4DSxR2DGW78TF9k7R0wZ8ev0BpMAIzA6yTQSHqNb5wTuvtcPYf4FWbVse2rW7RgDsyL8ua2vXHw== + +"@sindresorhus/df@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@sindresorhus/df/-/df-3.1.1.tgz#94200f9277e4a7fdd35ce8ab8b6bc5b52b164d31" + integrity sha512-SME/vtXaJcnQ/HpeV6P82Egy+jThn11IKfwW8+/XVoRD0rmPHVTeKMtww1oWdVnMykzVPjmrDN9S8NBndPEHCQ== + dependencies: + execa "^2.0.1" + +"@sindresorhus/is@^4.0.0": + version "4.6.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f" + integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw== + +"@socket.io/component-emitter@~3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2" + integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA== + +"@stroncium/procfs@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@stroncium/procfs/-/procfs-1.2.1.tgz#6b9be6fd20fb0a4c20e99a8695e083c699bb2b45" + integrity sha512-X1Iui3FUNZP18EUvysTHxt+Avu2nlVzyf90YM8OYgP6SGzTzzX/0JgObfO1AQQDzuZtNNz29bVh8h5R97JrjxA== + +"@szmarczak/http-timer@^4.0.5": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz#b4a914bb62e7c272d4e5989fe4440f812ab1d807" + integrity sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w== + dependencies: + defer-to-connect "^2.0.0" + +"@theia/application-manager@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/application-manager/-/application-manager-1.56.0.tgz#331e24b354c31104a9cd395df6d8fe65e5823b53" + integrity sha512-ZnSj3snNGBC/+T65T0vDLxg7eOYLpiw4V064tZc21Ae8FRMzmmIcjeHJlodaWmodvnxtLw2CNLtFYiMSMTZYxg== + dependencies: + "@babel/core" "^7.10.0" + "@babel/plugin-transform-classes" "^7.10.0" + "@babel/plugin-transform-runtime" "^7.10.0" + "@babel/preset-env" "^7.10.0" + "@theia/application-package" "1.56.0" + "@theia/ffmpeg" "1.56.0" + "@theia/native-webpack-plugin" "1.56.0" + "@types/fs-extra" "^4.0.2" + "@types/semver" "^7.5.0" + babel-loader "^8.2.2" + buffer "^6.0.3" + compression-webpack-plugin "^9.0.0" + copy-webpack-plugin "^8.1.1" + css-loader "^6.2.0" + electron-rebuild "^3.2.7" + fs-extra "^4.0.2" + http-server "^14.1.1" + ignore-loader "^0.1.2" + less "^3.0.3" + mini-css-extract-plugin "^2.6.1" + node-abi "*" + node-loader "^2.0.0" + path-browserify "^1.0.1" + semver "^7.5.4" + setimmediate "^1.0.5" + source-map "^0.6.1" + source-map-loader "^2.0.1" + source-map-support "^0.5.19" + style-loader "^2.0.0" + tslib "^2.6.2" + umd-compat-loader "^2.1.2" + webpack "^5.76.0" + webpack-cli "4.7.0" + worker-loader "^3.0.8" + yargs "^15.3.1" + +"@theia/application-package@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/application-package/-/application-package-1.56.0.tgz#9fcc43a1888418d4dfbf4335a85065aad7ae5642" + integrity sha512-NRdrfAeAYgeS8tRgEwozk9hb8287Kz4ps5h0B/dJe97VGxV1pv7atZXf9MWU9ct/8P2fGHal1dJ7apgKsQG59A== + dependencies: + "@theia/request" "1.56.0" + "@types/fs-extra" "^4.0.2" + "@types/semver" "^7.5.0" + "@types/write-json-file" "^2.2.1" + deepmerge "^4.2.2" + fs-extra "^4.0.2" + is-electron "^2.1.0" + nano "^10.1.3" + resolve-package-path "^4.0.3" + semver "^7.5.4" + tslib "^2.6.2" + write-json-file "^2.2.0" + +"@theia/bulk-edit@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/bulk-edit/-/bulk-edit-1.56.0.tgz#89ffea433b29d85d5e64c8907f158e08dc063841" + integrity sha512-JPMU7uZerA7en1CcHKS9b+mbYzK2n2xYHV7G6Ybe0wKsCrTk3r435iJc/XudpC79MnVXPtu9RbNnKmmGdGrK5A== + dependencies: + "@theia/core" "1.56.0" + "@theia/editor" "1.56.0" + "@theia/filesystem" "1.56.0" + "@theia/monaco" "1.56.0" + "@theia/monaco-editor-core" "1.83.101" + "@theia/workspace" "1.56.0" + tslib "^2.6.2" + +"@theia/callhierarchy@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/callhierarchy/-/callhierarchy-1.56.0.tgz#a85e7784e1cf3688c1c983b397c44ac46ffe99a6" + integrity sha512-2QxJSEUcVn37dER6DXGanLjn1RazzUChbwnuWdKq7AR2GnHdX1APX/hG8HI7ncTofRRReauWolRBwhzYwqhJCQ== + dependencies: + "@theia/core" "1.56.0" + "@theia/editor" "1.56.0" + ts-md5 "^1.2.2" + tslib "^2.6.2" + +"@theia/cli@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/cli/-/cli-1.56.0.tgz#9fa28f77e6243af6121dbf7a779b334e70c3305d" + integrity sha512-CPMwNtaIhi0LhtXrYkIArkEsZqhAESrZOz1l9ol5p0Etk/j4yx80QxYziE9LxpdfTj2BF9fah5Y+SQdlb88gbQ== + dependencies: + "@theia/application-manager" "1.56.0" + "@theia/application-package" "1.56.0" + "@theia/ffmpeg" "1.56.0" + "@theia/localization-manager" "1.56.0" + "@theia/ovsx-client" "1.56.0" + "@theia/request" "1.56.0" + "@types/chai" "^4.2.7" + "@types/mocha" "^10.0.0" + "@types/node-fetch" "^2.5.7" + chai "^4.3.10" + chalk "4.0.0" + decompress "^4.2.1" + escape-string-regexp "4.0.0" + glob "^8.0.3" + http-server "^14.1.1" + limiter "^2.1.0" + log-update "^4.0.0" + mocha "^10.1.0" + patch-package "^8.0.0" + puppeteer "23.1.0" + puppeteer-core "23.1.0" + puppeteer-to-istanbul "1.4.0" + temp "^0.9.1" + tslib "^2.6.2" + yargs "^15.3.1" + +"@theia/console@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/console/-/console-1.56.0.tgz#5fed7929d58b1c9053fb9833fa5bf45e341dcd4f" + integrity sha512-A4P+usFZDcmR45kgc4ccxtLDTEqKQgUongpug6uwLD6Y21FXBWUYvi8DszpLlEA0/NmKG9jboKYs59bMIhc19w== + dependencies: + "@theia/core" "1.56.0" + "@theia/monaco" "1.56.0" + "@theia/monaco-editor-core" "1.83.101" + anser "^2.0.1" + tslib "^2.6.2" + +"@theia/core@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/core/-/core-1.56.0.tgz#54ee090206866e199584c96411d3c45576700558" + integrity sha512-IkaHnVIM3asqz+LL4h4Tk0QXTneh9TIZxdyTCv3orY6Mr3UDu+NrTKd3qe0zmcWmUyKlOIivJ/MONWMj6xQJSw== + dependencies: + "@babel/runtime" "^7.10.0" + "@parcel/watcher" "^2.5.0" + "@phosphor/algorithm" "1" + "@phosphor/commands" "1" + "@phosphor/coreutils" "1" + "@phosphor/domutils" "1" + "@phosphor/dragdrop" "1" + "@phosphor/messaging" "1" + "@phosphor/properties" "1" + "@phosphor/signaling" "1" + "@phosphor/virtualdom" "1" + "@phosphor/widgets" "1" + "@theia/application-package" "1.56.0" + "@theia/request" "1.56.0" + "@types/body-parser" "^1.16.4" + "@types/cookie" "^0.3.3" + "@types/dompurify" "^2.2.2" + "@types/express" "^4.17.21" + "@types/fs-extra" "^4.0.2" + "@types/lodash.debounce" "4.0.3" + "@types/lodash.throttle" "^4.1.3" + "@types/markdown-it" "^12.2.3" + "@types/react" "^18.0.15" + "@types/react-dom" "^18.0.6" + "@types/route-parser" "^0.1.1" + "@types/safer-buffer" "^2.1.0" + "@types/uuid" "^9.0.8" + "@types/ws" "^8.5.5" + "@types/yargs" "^15" + "@vscode/codicons" "*" + ajv "^6.5.3" + async-mutex "^0.4.0" + body-parser "^1.17.2" + cookie "^0.4.0" + dompurify "^2.2.9" + drivelist "^9.0.2" + es6-promise "^4.2.4" + express "^4.21.0" + fast-json-stable-stringify "^2.1.0" + file-icons-js "~1.0.3" + font-awesome "^4.7.0" + fs-extra "^4.0.2" + fuzzy "^0.1.3" + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.0" + iconv-lite "^0.6.0" + inversify "^6.1.3" + jschardet "^2.1.1" + keytar "7.2.0" + lodash.debounce "^4.0.8" + lodash.throttle "^4.1.1" + markdown-it "^12.3.2" + msgpackr "^1.10.2" + p-debounce "^2.1.0" + perfect-scrollbar "^1.3.0" + react "^18.2.0" + react-dom "^18.2.0" + react-tooltip "^4.2.21" + react-virtuoso "^2.17.0" + reflect-metadata "^0.1.10" + route-parser "^0.0.5" + safer-buffer "^2.1.2" + socket.io "^4.5.3" + socket.io-client "^4.5.3" + tslib "^2.6.2" + uuid "^9.0.1" + vscode-languageserver-protocol "^3.17.2" + vscode-uri "^2.1.1" + ws "^8.17.1" + yargs "^15.3.1" + +"@theia/debug@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/debug/-/debug-1.56.0.tgz#6afee26523f8f0f436a73cebe4802c48ae202e6e" + integrity sha512-gZLyx77cYdsou9PrniPe9xgI/SwGHDQmplBL0n4AczetWs+yn0lcBLsKQ7P+Ny7HXwLQBlRoXJkZpm68TN1Zfw== + dependencies: + "@theia/console" "1.56.0" + "@theia/core" "1.56.0" + "@theia/editor" "1.56.0" + "@theia/filesystem" "1.56.0" + "@theia/markers" "1.56.0" + "@theia/monaco" "1.56.0" + "@theia/monaco-editor-core" "1.83.101" + "@theia/output" "1.56.0" + "@theia/process" "1.56.0" + "@theia/task" "1.56.0" + "@theia/terminal" "1.56.0" + "@theia/test" "1.56.0" + "@theia/variable-resolver" "1.56.0" + "@theia/workspace" "1.56.0" + "@vscode/debugprotocol" "^1.51.0" + fast-deep-equal "^3.1.3" + jsonc-parser "^2.2.0" + p-debounce "^2.1.0" + tslib "^2.6.2" + +"@theia/editor-preview@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/editor-preview/-/editor-preview-1.56.0.tgz#66dafb123041a26d553832d9308c8e78681e2350" + integrity sha512-0oWtt3qaEXc/0WDwTrCvg7dc2+P5J97qreADPp8JI4KUGHuofMXHZqI0uemc3CxK9Pd/l7FpWBrXjCcEftEK5A== + dependencies: + "@theia/core" "1.56.0" + "@theia/editor" "1.56.0" + "@theia/navigator" "1.56.0" + tslib "^2.6.2" + +"@theia/editor@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/editor/-/editor-1.56.0.tgz#0ba15b7fa52c7a898175081162bc577ac91ef094" + integrity sha512-XxNmgCI67U1DOaKQmg33x/UTuLpePIEmqvL7rjeoILGtptSBP5Ah7wqcy3ZaJxmMKMsOtdHwmffMfGo0blgbcg== + dependencies: + "@theia/core" "1.56.0" + "@theia/variable-resolver" "1.56.0" + tslib "^2.6.2" + +"@theia/electron@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/electron/-/electron-1.56.0.tgz#b1986f934f24739f37eb68371eefa65537ec64cd" + integrity sha512-4ejcZvOnFfhtzzfxA3UWUV0HmAf3bAj7S7iMcG4lZcBQDcTp7YCfcmqIAI3zQwFTWxzfPP/26KGx/FH8ZAKAlw== + dependencies: + electron-store "^8.0.0" + fix-path "^3.0.0" + native-keymap "^2.2.1" + +"@theia/external-terminal@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/external-terminal/-/external-terminal-1.56.0.tgz#9f207864e9c9d01178b2db34a8ffb7e392657866" + integrity sha512-9rwHjD2Ck9eknlCpdPEr5KEK8zMuTWFIqQ+HO+j/EN+u/R0Zcjf+Z9FjoZ3Uy91oos9gwaJNuNSzquZ/4gm8yQ== + dependencies: + "@theia/core" "1.56.0" + "@theia/editor" "1.56.0" + "@theia/workspace" "1.56.0" + tslib "^2.6.2" + +"@theia/ffmpeg@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/ffmpeg/-/ffmpeg-1.56.0.tgz#f3f2d02c04155011a24b55a1ef67cba0fde6c1a3" + integrity sha512-/zrpqtqMJBu89KeRM/9NgPRisVZ323K0ZiBd9iqa/O4CTHJlD0+rEmt6Dv2Nb4okNNhpIhUQGYzvhMJFwiYfww== + dependencies: + "@electron/get" "^2.0.0" + tslib "^2.6.2" + unzipper "^0.9.11" + +"@theia/file-search@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/file-search/-/file-search-1.56.0.tgz#269cc7694864e6436d70d0398c4a7e9943ed0a66" + integrity sha512-+pHSXXNz0vUpd6I1BPfwI3LQfgRQ/EGULH07wVFxMm9V3x9gb+/WTMUujaEmHOAYP+nSYL5yTT7Bjhk+GGqeKw== + dependencies: + "@theia/core" "1.56.0" + "@theia/editor" "1.56.0" + "@theia/filesystem" "1.56.0" + "@theia/process" "1.56.0" + "@theia/workspace" "1.56.0" + "@vscode/ripgrep" "^1.14.2" + tslib "^2.6.2" + +"@theia/filesystem@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/filesystem/-/filesystem-1.56.0.tgz#c7108e1576713be243ff4bf27dbd26e193990d06" + integrity sha512-cE7JLSZS78A7e5V6Fv1dVVKyGiz0jg4qijtJ7fPhv/ehvczh3gv7vyQh8PX7Blkzri9PkT79SrFty8RErdMCOQ== + dependencies: + "@theia/core" "1.56.0" + "@types/body-parser" "^1.17.0" + "@types/multer" "^1.4.7" + "@types/tar-fs" "^1.16.1" + async-mutex "^0.3.1" + body-parser "^1.18.3" + http-status-codes "^1.3.0" + minimatch "^5.1.0" + multer "1.4.4-lts.1" + rimraf "^5.0.0" + stat-mode "^1.0.0" + tar-fs "^1.16.2" + trash "^7.2.0" + tslib "^2.6.2" + vscode-languageserver-textdocument "^1.0.1" + +"@theia/getting-started@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/getting-started/-/getting-started-1.56.0.tgz#43feee63771326a0b1bd31e15159702aa98eb628" + integrity sha512-yMlAAQCfXEDWQbkmCo/4RFu4KM99K3KqFuJe2OxV8BmCdKySNc1N1xQXsynAqn1YRP8tJ2nNo7kbCwqCt3EC8w== + dependencies: + "@theia/core" "1.56.0" + "@theia/editor" "1.56.0" + "@theia/filesystem" "1.56.0" + "@theia/keymaps" "1.56.0" + "@theia/preview" "1.56.0" + "@theia/workspace" "1.56.0" + tslib "^2.6.2" + +"@theia/keymaps@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/keymaps/-/keymaps-1.56.0.tgz#d2e01fc40b4ab12c0ade25af116f95be6d29352e" + integrity sha512-avVByqXkOKkPzpeRfcASfT5f0KQBkszp0zGxA4VRbRxLZCsZytwK4V3tuc9xrU1JmTLetHBAhH3Jrxe1aoWxsA== + dependencies: + "@theia/core" "1.56.0" + "@theia/monaco" "1.56.0" + "@theia/monaco-editor-core" "1.83.101" + "@theia/preferences" "1.56.0" + "@theia/userstorage" "1.56.0" + jsonc-parser "^2.2.0" + tslib "^2.6.2" + +"@theia/localization-manager@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/localization-manager/-/localization-manager-1.56.0.tgz#9148b6692c23738edc74c0dd114c120d3b05e021" + integrity sha512-Jt2K7H8PGqBinjuSb8pyoqxSguO0qU91dfGDj30UoaD68cjUaiVrmyH6N1N2qZGbsOOil5BcwAcbC3IcHYm1jQ== + dependencies: + "@types/bent" "^7.0.1" + "@types/fs-extra" "^4.0.2" + bent "^7.1.0" + chalk "4.0.0" + deepmerge "^4.2.2" + fs-extra "^4.0.2" + glob "^7.2.0" + tslib "^2.6.2" + typescript "~5.4.5" + +"@theia/markers@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/markers/-/markers-1.56.0.tgz#1954ea1d3e2ab3c9a7062c0739ccad7503e5245e" + integrity sha512-JnCe+cVbpKa1RVDba8fSnJ9K1wmVceG+dHwml403cNx+ALNFDEl2V9S189722X9px+Ex27cBcTSexH52FTw6RA== + dependencies: + "@theia/core" "1.56.0" + "@theia/filesystem" "1.56.0" + "@theia/workspace" "1.56.0" + tslib "^2.6.2" + +"@theia/messages@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/messages/-/messages-1.56.0.tgz#09a3889461cf5d4b1f76201720079c106ecf5686" + integrity sha512-deDdwK0O8RD7rynArDbBNekMsmxYbWy20ZDLqhJfZkYJD+nA0oCfnxIEAQ2dCtUvI4lMRfisFYv4vLR1fcHP/w== + dependencies: + "@theia/core" "1.56.0" + react-perfect-scrollbar "^1.5.3" + ts-md5 "^1.2.2" + tslib "^2.6.2" + +"@theia/metrics@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/metrics/-/metrics-1.56.0.tgz#d9220c844a98013744e9d4de298f361245bc16b9" + integrity sha512-fqPrZPVwodK4ue0fyrIWump7833hIwJz0L371oYILyNKBpveqBig8geYvfgk1o5I5hkYISTja/r7BR76NwLdCg== + dependencies: + "@theia/core" "1.56.0" + prom-client "^10.2.0" + tslib "^2.6.2" + +"@theia/mini-browser@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/mini-browser/-/mini-browser-1.56.0.tgz#9fb92c57bc139401325d267019dca7a06224c13c" + integrity sha512-eB3uZiB2lCgWLkyRjok3KfIT2THMwiZae3A6EqmVcYMUKb90SfaQ8ffsQrG2w2c7Y/HxiSsCnASggTYUads86A== + dependencies: + "@theia/core" "1.56.0" + "@theia/filesystem" "1.56.0" + "@types/mime-types" "^2.1.0" + mime-types "^2.1.18" + pdfobject "^2.0.201604172" + tslib "^2.6.2" + vhost "^3.0.2" + +"@theia/monaco-editor-core@1.83.101": + version "1.83.101" + resolved "https://registry.yarnpkg.com/@theia/monaco-editor-core/-/monaco-editor-core-1.83.101.tgz#a0577396fb4c69540536df2d7fed2de4399c4fde" + integrity sha512-UaAi6CEvain/qbGD3o6Ufe8plLyzAVQ53p9Ke+MoBYDhb391+r+MuK++JtITqIrXqoa8OCjbt8wQxEFSNNO0Mw== + +"@theia/monaco@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/monaco/-/monaco-1.56.0.tgz#94392bac50bf6916837b158ba270bd17af75978b" + integrity sha512-v+sXnxWZbMVVP9p74UBal3dxWEr8j3BrbYOkyznq0RDjSU3zO5dVkztINBLdICCAt60bRp1IwfavNUJKhyW8PQ== + dependencies: + "@theia/core" "1.56.0" + "@theia/editor" "1.56.0" + "@theia/filesystem" "1.56.0" + "@theia/markers" "1.56.0" + "@theia/monaco-editor-core" "1.83.101" + "@theia/outline-view" "1.56.0" + "@theia/workspace" "1.56.0" + fast-plist "^0.1.2" + idb "^4.0.5" + jsonc-parser "^2.2.0" + tslib "^2.6.2" + vscode-oniguruma "1.6.1" + vscode-textmate "^9.0.0" + +"@theia/native-webpack-plugin@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/native-webpack-plugin/-/native-webpack-plugin-1.56.0.tgz#3cc941268d3e03d070047e61ed71685aa8a8967d" + integrity sha512-+yhNQW74oZpTkDxD3hYjbiNNx3ROEIQPh4AEJ9N52GB2g4ZqCHugwOw7vssxRveCow05/OrCfffkdG4AVkz1CQ== + dependencies: + detect-libc "^2.0.2" + tslib "^2.6.2" + webpack "^5.76.0" + +"@theia/navigator@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/navigator/-/navigator-1.56.0.tgz#b05de893c07acbd6d708cec072163d3970be6c0e" + integrity sha512-TTD4HwCfAITAARB1qQWMIAe7+8is3OsWGfzuC+lFXNGeJZW3WOWd+jYj01C9KUlith0FvN4Z6QhhRlivqeGd+A== + dependencies: + "@theia/core" "1.56.0" + "@theia/filesystem" "1.56.0" + "@theia/workspace" "1.56.0" + minimatch "^5.1.0" + tslib "^2.6.2" + +"@theia/notebook@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/notebook/-/notebook-1.56.0.tgz#6970f5e549e9124f251741180cd246d6b4262d33" + integrity sha512-HuMq/c6LQP5xl7WxStc0fEBn+4y6IWIsHz/tv7x+VocIIZ6QWlBsMfcDzKuIDp2Ro6voGHKtJemgKHSL6T6Hag== + dependencies: + "@theia/core" "1.56.0" + "@theia/editor" "1.56.0" + "@theia/filesystem" "1.56.0" + "@theia/monaco" "1.56.0" + "@theia/monaco-editor-core" "1.83.101" + "@theia/outline-view" "1.56.0" + advanced-mark.js "^2.6.0" + react-perfect-scrollbar "^1.5.8" + tslib "^2.6.2" + +"@theia/outline-view@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/outline-view/-/outline-view-1.56.0.tgz#3702a660cf7231d398cc66228571d160cb9e4365" + integrity sha512-03SS7rmYCa03aVo6FHo9KW9Y+b9l9UvqhSCIW6UCQYxRCNraCuTtfP2EjcWsAWAO+lBVy2Tp8hf8+X7XgReZVQ== + dependencies: + "@theia/core" "1.56.0" + tslib "^2.6.2" + +"@theia/output@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/output/-/output-1.56.0.tgz#0ca20dda97c7a533950f50b00aa20ecc3638bb26" + integrity sha512-BNzkSWdU4K+Ak2Eli35mrBHhMCu9fHHddZt1LE1zebKcjvldg/KkgafZ5ALxr5ylmyz3yU6KwxzrZR+gqA9x5w== + dependencies: + "@theia/core" "1.56.0" + "@theia/editor" "1.56.0" + "@theia/monaco" "1.56.0" + "@theia/monaco-editor-core" "1.83.101" + "@types/p-queue" "^2.3.1" + p-queue "^2.4.2" + tslib "^2.6.2" + +"@theia/ovsx-client@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/ovsx-client/-/ovsx-client-1.56.0.tgz#6e46fa0c8b0c8b7da2b7f128dcd57c2073bb5243" + integrity sha512-kPtmnQpKF6+sN29pzJ1SNZSBcWcFv+807c0JvUUPXTRlbjIYtZObMxdf5FZ8iMcRLJmu31GzcaMeLK/qztOmuQ== + dependencies: + "@theia/request" "1.56.0" + limiter "^2.1.0" + semver "^7.5.4" + tslib "^2.6.2" + +"@theia/plugin-dev@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/plugin-dev/-/plugin-dev-1.56.0.tgz#57e92ab4787f8bbc5a1daa62f0783374176e4f05" + integrity sha512-/lTGIp8XxZc/g6mE0UWP9P1rNbopYk+hgbRlST5UwH648fHj1JzDNBpFMQ0BK4ynY8AxF1yHmolj0nDl3ohTgw== + dependencies: + "@theia/core" "1.56.0" + "@theia/debug" "1.56.0" + "@theia/filesystem" "1.56.0" + "@theia/output" "1.56.0" + "@theia/plugin-ext" "1.56.0" + "@theia/workspace" "1.56.0" + ps-tree "^1.2.0" + tslib "^2.6.2" + +"@theia/plugin-ext-vscode@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/plugin-ext-vscode/-/plugin-ext-vscode-1.56.0.tgz#bf243a153dffefe87bc6a58ee8ad1c466821c609" + integrity sha512-pGLZC2/J54k6s4iA4v5QsK+bxQx11F/YPJOFZ6kgD3ttU5fNC7bGoN7UWU1mS2njm6lNf4bKpvFxZqK0w4EmYw== + dependencies: + "@theia/callhierarchy" "1.56.0" + "@theia/core" "1.56.0" + "@theia/editor" "1.56.0" + "@theia/filesystem" "1.56.0" + "@theia/monaco" "1.56.0" + "@theia/monaco-editor-core" "1.83.101" + "@theia/navigator" "1.56.0" + "@theia/outline-view" "1.56.0" + "@theia/plugin" "1.56.0" + "@theia/plugin-ext" "1.56.0" + "@theia/terminal" "1.56.0" + "@theia/typehierarchy" "1.56.0" + "@theia/userstorage" "1.56.0" + "@theia/workspace" "1.56.0" + decompress "^4.2.1" + filenamify "^4.1.0" + tslib "^2.6.2" + +"@theia/plugin-ext@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/plugin-ext/-/plugin-ext-1.56.0.tgz#f5f060a2afd615f2e15e5f8e668e60f44e16824e" + integrity sha512-ETl1K7ZTIaWBHoP3KB4vEGHc88orxm8F57aPNz7rs0L4G/YC6yg52ptvGtUkwL+80p0jkSZC/LVoTyiwMiOvUQ== + dependencies: + "@theia/bulk-edit" "1.56.0" + "@theia/callhierarchy" "1.56.0" + "@theia/console" "1.56.0" + "@theia/core" "1.56.0" + "@theia/debug" "1.56.0" + "@theia/editor" "1.56.0" + "@theia/editor-preview" "1.56.0" + "@theia/file-search" "1.56.0" + "@theia/filesystem" "1.56.0" + "@theia/markers" "1.56.0" + "@theia/messages" "1.56.0" + "@theia/monaco" "1.56.0" + "@theia/monaco-editor-core" "1.83.101" + "@theia/navigator" "1.56.0" + "@theia/notebook" "1.56.0" + "@theia/output" "1.56.0" + "@theia/plugin" "1.56.0" + "@theia/preferences" "1.56.0" + "@theia/scm" "1.56.0" + "@theia/search-in-workspace" "1.56.0" + "@theia/task" "1.56.0" + "@theia/terminal" "1.56.0" + "@theia/test" "1.56.0" + "@theia/timeline" "1.56.0" + "@theia/typehierarchy" "1.56.0" + "@theia/variable-resolver" "1.56.0" + "@theia/workspace" "1.56.0" + "@types/mime" "^2.0.1" + "@vscode/debugprotocol" "^1.51.0" + "@vscode/proxy-agent" "^0.13.2" + async-mutex "^0.4.0" + decompress "^4.2.1" + escape-html "^1.0.3" + filenamify "^4.1.0" + is-electron "^2.2.0" + jsonc-parser "^2.2.0" + lodash.clonedeep "^4.5.0" + macaddress "^0.5.3" + mime "^2.4.4" + ps-tree "^1.2.0" + semver "^7.5.4" + tslib "^2.6.2" + vhost "^3.0.2" + vscode-textmate "^9.0.0" + +"@theia/plugin@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/plugin/-/plugin-1.56.0.tgz#fa9976750945ca21c5fbf47613b7d057240264e4" + integrity sha512-v/wrgRi7lJUpKSdWfBd4Qm1UDj2TQnBfYjUKTNyQNlUzLNXbjeY2BR3RZaMiGYrTBnDW4gt8KmmVLme12UKXGQ== + +"@theia/preferences@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/preferences/-/preferences-1.56.0.tgz#99a609990fef5efad0cf51d05cbccbd9af3b4079" + integrity sha512-QvDYWVPkxSkS45b6CcgejkeyNFCGcNCElMYn4EuaNfxgO2bqfFFOqkYtGsb+/6E8TKUb28G3hF89CwWAFHdaTw== + dependencies: + "@theia/core" "1.56.0" + "@theia/editor" "1.56.0" + "@theia/filesystem" "1.56.0" + "@theia/monaco" "1.56.0" + "@theia/monaco-editor-core" "1.83.101" + "@theia/userstorage" "1.56.0" + "@theia/workspace" "1.56.0" + async-mutex "^0.3.1" + fast-deep-equal "^3.1.3" + jsonc-parser "^2.2.0" + p-debounce "^2.1.0" + tslib "^2.6.2" + +"@theia/preview@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/preview/-/preview-1.56.0.tgz#4f3065643b9a9b7db86868c07df1b250311c1c61" + integrity sha512-8c83rGCx/3VCdzqdN/RiOYcqsBg2kY0M3dpklowBvgudlbx02Hk4SFqPl/wbkAbZTXjeZ4qludku7iddoSnvTg== + dependencies: + "@theia/core" "1.56.0" + "@theia/editor" "1.56.0" + "@theia/mini-browser" "1.56.0" + "@theia/monaco" "1.56.0" + "@types/highlight.js" "^10.1.0" + "@types/markdown-it-anchor" "^4.0.1" + highlight.js "10.4.1" + markdown-it-anchor "~5.0.0" + tslib "^2.6.2" + +"@theia/process@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/process/-/process-1.56.0.tgz#35be6d26fd80ac9584bebbae10cb3d028a1866e8" + integrity sha512-zCHql9/4OXicHA90Hxm6fNXmaCfdEa1O/VTaFEc2kp27gIk2IoPQa4U/wAMHZKp6QU2kcE1hRJTRVS/SE45FLA== + dependencies: + "@theia/core" "1.56.0" + node-pty "0.11.0-beta24" + string-argv "^0.1.1" + tslib "^2.6.2" + +"@theia/property-view@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/property-view/-/property-view-1.56.0.tgz#e85da61f5efa8a26b3d97ddc819d40330a5249b9" + integrity sha512-tdgn8A3nyCz+NpHnkBC3Esi0Ho+mi1hetk2bNsXnrPJ3Znar5f9I2n167pN002ayKIRAP1cZ/ZQ/Zg8SNRJvgA== + dependencies: + "@theia/core" "1.56.0" + "@theia/filesystem" "1.56.0" + tslib "^2.6.2" + +"@theia/request@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/request/-/request-1.56.0.tgz#b5e1b4516c1c6a50e4eaca64a3dbff683b142785" + integrity sha512-za3drcBr3UESGn26DrShZElw0+ecD4U8uSC6TtiiqmMtf156lYfElxf0CYnMAbHkveCu6V4scs3JqGTt9DUYaQ== + dependencies: + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.0" + tslib "^2.6.2" + +"@theia/scm-extra@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/scm-extra/-/scm-extra-1.56.0.tgz#069360a11a3fe8b4522e88fbf30f7ecfe5d3a510" + integrity sha512-Pmaso0pZcSGLegfHgvf1n3LoAgH/Nzyqj5c8GTU8plqdn45ot3bEIjEaZFPREk8YncNMgWhbR0PY0NhoXyEUYg== + dependencies: + "@theia/core" "1.56.0" + "@theia/editor" "1.56.0" + "@theia/filesystem" "1.56.0" + "@theia/navigator" "1.56.0" + "@theia/scm" "1.56.0" + tslib "^2.6.2" + +"@theia/scm@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/scm/-/scm-1.56.0.tgz#6417e730b0b0c06cb3f7c3440d242198ac3c97f3" + integrity sha512-/3l7JFRATQqXz2y/v0C/90uEltgcIvqQfnyfRmXLUbBkXn6LWQZDWtC36ZX+HcNEmdkQbuj6akaA8p6u/HV/aw== + dependencies: + "@theia/core" "1.56.0" + "@theia/editor" "1.56.0" + "@theia/filesystem" "1.56.0" + "@theia/monaco" "1.56.0" + "@theia/monaco-editor-core" "1.83.101" + "@types/diff" "^5.2.1" + diff "^5.2.0" + p-debounce "^2.1.0" + react-autosize-textarea "^7.0.0" + ts-md5 "^1.2.2" + tslib "^2.6.2" + +"@theia/search-in-workspace@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/search-in-workspace/-/search-in-workspace-1.56.0.tgz#f8bef2155f21b049b96016ca5ae0126313bd376d" + integrity sha512-lhA5c+gpjkznN3TPpI9xr9kwctIqO9JcpdFFtTfZnD1I66y/eEgaEAEI/G9fAlMpIMT5OrTK4xGwDDdMLogVPQ== + dependencies: + "@theia/core" "1.56.0" + "@theia/editor" "1.56.0" + "@theia/filesystem" "1.56.0" + "@theia/navigator" "1.56.0" + "@theia/process" "1.56.0" + "@theia/workspace" "1.56.0" + "@vscode/ripgrep" "^1.14.2" + minimatch "^5.1.0" + react-autosize-textarea "^7.0.0" + tslib "^2.6.2" + +"@theia/task@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/task/-/task-1.56.0.tgz#be8fbe2009fbba8723843113e3c5f1c8044bbee9" + integrity sha512-rApGOf8mtF7XAmGZsqXAqy71EBT/VMLJsOf7Nc2OrbC0j4SuroL5kR1v5uE14NCcJaqRPTLkQc0VaSrOe/IIFg== + dependencies: + "@theia/core" "1.56.0" + "@theia/editor" "1.56.0" + "@theia/filesystem" "1.56.0" + "@theia/markers" "1.56.0" + "@theia/monaco" "1.56.0" + "@theia/monaco-editor-core" "1.83.101" + "@theia/process" "1.56.0" + "@theia/terminal" "1.56.0" + "@theia/userstorage" "1.56.0" + "@theia/variable-resolver" "1.56.0" + "@theia/workspace" "1.56.0" + async-mutex "^0.3.1" + jsonc-parser "^2.2.0" + p-debounce "^2.1.0" + tslib "^2.6.2" + +"@theia/terminal@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/terminal/-/terminal-1.56.0.tgz#034d571f4823f84992c9f9564393ab9b1ab308e7" + integrity sha512-KruO7gQtWwzla3XfeRBXC/shVBGIZUe63QjIQJJezyEKBDq3nA4lGuE9XC/lFbqD0qqK0lmTnCn70Aa4ZRKiQA== + dependencies: + "@theia/core" "1.56.0" + "@theia/editor" "1.56.0" + "@theia/file-search" "1.56.0" + "@theia/filesystem" "1.56.0" + "@theia/process" "1.56.0" + "@theia/variable-resolver" "1.56.0" + "@theia/workspace" "1.56.0" + tslib "^2.6.2" + xterm "^5.3.0" + xterm-addon-fit "^0.8.0" + xterm-addon-search "^0.13.0" + +"@theia/test@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/test/-/test-1.56.0.tgz#87216e49cc41c99b2c3da2523ccb46c80d81c041" + integrity sha512-5wLHhBkxuip9V0fI2lDrCzN1AJWRyMkrgpzduzQcX/m3qtPWfhOrZWOHdRp6rVbyAIKrMrPQ3dqdQoI8/SwCpg== + dependencies: + "@theia/core" "1.56.0" + "@theia/editor" "1.56.0" + "@theia/filesystem" "1.56.0" + "@theia/navigator" "1.56.0" + "@theia/terminal" "1.56.0" + xterm "^4.16.0" + xterm-addon-fit "^0.5.0" + +"@theia/timeline@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/timeline/-/timeline-1.56.0.tgz#a2251288826c9a4fb8b5d4f10542de8d1f7749c7" + integrity sha512-jdpzTMtcoceu+zNLcwBvWi4Yq3P+xAoZac3v5QJbSkhTG005OETLP+4SJYAe9bHu+K/ibc9p4gDnqxPxrh0YHA== + dependencies: + "@theia/core" "1.56.0" + "@theia/navigator" "1.56.0" + tslib "^2.6.2" + +"@theia/toolbar@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/toolbar/-/toolbar-1.56.0.tgz#3890df7ff1b702a077be6e5409db80605c5ed36e" + integrity sha512-qFjPLyBur1mMhb8hj26/R5GgVx5WqCTsS8E94NNNE9LZdCJUAKcDufLDlNt3zJOZAW0HF9krcsiSuIYsfB0+dQ== + dependencies: + "@theia/core" "1.56.0" + "@theia/editor" "1.56.0" + "@theia/file-search" "1.56.0" + "@theia/filesystem" "1.56.0" + "@theia/monaco" "1.56.0" + "@theia/monaco-editor-core" "1.83.101" + "@theia/search-in-workspace" "1.56.0" + "@theia/userstorage" "1.56.0" + "@theia/workspace" "1.56.0" + ajv "^6.5.3" + jsonc-parser "^2.2.0" + perfect-scrollbar "^1.3.0" + tslib "^2.6.2" + +"@theia/typehierarchy@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/typehierarchy/-/typehierarchy-1.56.0.tgz#cb3fd1903225e43e870e00a1386e720855fe0728" + integrity sha512-B3FrRfQyahS17PyAb/NuwRAlenz9i6K7Pf4yOYfLao5K7H1+QHKKm7GIJLbuA8355jc3yaiasgij2clIMXAfsA== + dependencies: + "@theia/core" "1.56.0" + "@theia/editor" "1.56.0" + tslib "^2.6.2" + +"@theia/userstorage@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/userstorage/-/userstorage-1.56.0.tgz#dd9b5ee5085c89a4b80ff11f71fcb5fe4cb2fd5e" + integrity sha512-e2EPkPWKGgQsvUV5yXKkGLGC/POCgCW85O76/HhEuiRLDRpx7LCPH+uRaB9mXOOFgGGRtHPxtWSFGLMNpGKuNg== + dependencies: + "@theia/core" "1.56.0" + "@theia/filesystem" "1.56.0" + tslib "^2.6.2" + +"@theia/variable-resolver@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/variable-resolver/-/variable-resolver-1.56.0.tgz#1d4130898a61cd92a0ef0a0c7344dff2b87eb3e6" + integrity sha512-VIPL+17aOlH4OhJTSV0EssUGJCHN3G00WADQViRYPQQma7+lP/sivAr5U/IMahjh2nK8S3SvBDCSZ6wiKIjNvA== + dependencies: + "@theia/core" "1.56.0" + tslib "^2.6.2" + +"@theia/vsx-registry@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/vsx-registry/-/vsx-registry-1.56.0.tgz#a03c5ab5f2cee0d8c69c2c5f5ac28f911225d010" + integrity sha512-1f+P4lkBot3WX3Vtts6KDPr3bfimoQq135pZzyokO9hHUHg9S5jHkBLQ7LTAvgaF/wfBUtK85EzZRBTkCpuNew== + dependencies: + "@theia/core" "1.56.0" + "@theia/filesystem" "1.56.0" + "@theia/navigator" "1.56.0" + "@theia/ovsx-client" "1.56.0" + "@theia/plugin-ext" "1.56.0" + "@theia/plugin-ext-vscode" "1.56.0" + "@theia/preferences" "1.56.0" + "@theia/workspace" "1.56.0" + limiter "^2.1.0" + luxon "^2.4.0" + p-debounce "^2.1.0" + semver "^7.5.4" + tslib "^2.6.2" + +"@theia/workspace@1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@theia/workspace/-/workspace-1.56.0.tgz#2e7cbc56a31d3cdfd5997afe5a77b6982518a475" + integrity sha512-pmEfCgBY3G0eGYwEMdn7ZYaHV8T5uTmHdXB3GRxJFGFmWeKbScqlJPQzxBgNuWKNFufPqkO+EF0JkpupoLfjcQ== + dependencies: + "@theia/core" "1.56.0" + "@theia/filesystem" "1.56.0" + "@theia/variable-resolver" "1.56.0" + jsonc-parser "^2.2.0" + tslib "^2.6.2" + valid-filename "^2.0.1" + +"@tootallnate/once@1", "@tootallnate/once@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" + integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== + +"@tootallnate/once@2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" + integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== + +"@tootallnate/quickjs-emscripten@^0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c" + integrity sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA== + +"@types/bent@^7.0.1": + version "7.3.8" + resolved "https://registry.yarnpkg.com/@types/bent/-/bent-7.3.8.tgz#69c3ee49bf6593d831006794e7bd2f84bb573e58" + integrity sha512-yZ09JA1KsA5Fl6Oh/ahK00+H5bV0qCy2bYnyfiFY42wnaMK4n7IDC6HaFe3WW45Zhnak7iqJBKlWD0nVxzrGWg== + dependencies: + "@types/node" "*" + +"@types/body-parser@*", "@types/body-parser@^1.16.4", "@types/body-parser@^1.17.0": + version "1.19.5" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" + integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/cacheable-request@^6.0.1": + version "6.0.3" + resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.3.tgz#a430b3260466ca7b5ca5bfd735693b36e7a9d183" + integrity sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw== + dependencies: + "@types/http-cache-semantics" "*" + "@types/keyv" "^3.1.4" + "@types/node" "*" + "@types/responselike" "^1.0.0" + +"@types/chai@^4.2.7": + version "4.3.20" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.20.tgz#cb291577ed342ca92600430841a00329ba05cecc" + integrity sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ== + +"@types/connect@*": + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + +"@types/cookie@^0.3.3": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.3.3.tgz#85bc74ba782fb7aa3a514d11767832b0e3bc6803" + integrity sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow== + +"@types/cookie@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" + integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== + +"@types/cors@^2.8.12": + version "2.8.17" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b" + integrity sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA== + dependencies: + "@types/node" "*" + +"@types/diff@^5.2.1": + version "5.2.3" + resolved "https://registry.yarnpkg.com/@types/diff/-/diff-5.2.3.tgz#dcdcfa40df9f011f9465180e0196dfbd921971d9" + integrity sha512-K0Oqlrq3kQMaO2RhfrNQX5trmt+XLyom88zS0u84nnIcLvFnRUMRRHmrGny5GSM+kNO9IZLARsdQHDzkhAgmrQ== + +"@types/dompurify@^2.2.2": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.4.0.tgz#fd9706392a88e0e0e6d367f3588482d817df0ab9" + integrity sha512-IDBwO5IZhrKvHFUl+clZxgf3hn2b/lU6H1KaBShPkQyGJUQ0xwebezIPSuiyGwfz1UzJWQl4M7BDxtHtCCPlTg== + dependencies: + "@types/trusted-types" "*" + +"@types/eslint-scope@^3.7.7": + version "3.7.7" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" + integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "9.6.1" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-9.6.1.tgz#d5795ad732ce81715f27f75da913004a56751584" + integrity sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*", "@types/estree@^1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" + integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== + +"@types/express-serve-static-core@^4.17.33": + version "4.19.6" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz#e01324c2a024ff367d92c66f48553ced0ab50267" + integrity sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express-serve-static-core@^5.0.0": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.2.tgz#812d2871e5eea17fb0bd5214dda7a7b748c0e12a" + integrity sha512-vluaspfvWEtE4vcSDlKRNer52DvOGrB2xv6diXy6UKyKW0lqZiWHGNApSyxOv+8DE5Z27IzVvE7hNkxg7EXIcg== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@*": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.0.tgz#13a7d1f75295e90d19ed6e74cab3678488eaa96c" + integrity sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^5.0.0" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/express@^4.17.21": + version "4.17.21" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" + integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/fs-extra@^4.0.2": + version "4.0.15" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-4.0.15.tgz#82d70b4a2e5e3dd17474ce9e8fe951a03aeddd31" + integrity sha512-zU/EU2kZ1tv+p4pswQLntA7dFQq84wXrSCfmLjZvMbLjf4N46cPOWHg+WKfc27YnEOQ0chVFlBui55HRsvzHPA== + dependencies: + "@types/node" "*" + +"@types/highlight.js@^10.1.0": + version "10.1.0" + resolved "https://registry.yarnpkg.com/@types/highlight.js/-/highlight.js-10.1.0.tgz#89bb0c202997d7a90a07bd2ec1f7d00c56bb90b4" + integrity sha512-77hF2dGBsOgnvZll1vymYiNUtqJ8cJfXPD6GG/2M0aLRc29PkvB7Au6sIDjIEFcSICBhCh2+Pyq6WSRS7LUm6A== + dependencies: + highlight.js "*" + +"@types/http-cache-semantics@*": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4" + integrity sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA== + +"@types/http-errors@*": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" + integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== + +"@types/json-schema@*", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/keyv@^3.1.4": + version "3.1.4" + resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.4.tgz#3ccdb1c6751b0c7e52300bcdacd5bcbf8faa75b6" + integrity sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg== + dependencies: + "@types/node" "*" + +"@types/linkify-it@*", "@types/linkify-it@^5": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76" + integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q== + +"@types/lodash.debounce@4.0.3": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/lodash.debounce/-/lodash.debounce-4.0.3.tgz#d712aee9e6136be77f70523ed9f0fc049a6cf15a" + integrity sha512-/2RpcexzkSH16nENwuL/Gd3Y2xvdkNwX32KPESB/D8K2c6HBs7GdSnoj6ngyFWNT1UhXNrIpJd0lgSC3Rmt/3g== + dependencies: + "@types/lodash" "*" + +"@types/lodash.throttle@^4.1.3": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@types/lodash.throttle/-/lodash.throttle-4.1.9.tgz#f17a6ae084f7c0117bd7df145b379537bc9615c5" + integrity sha512-PCPVfpfueguWZQB7pJQK890F2scYKoDUL3iM522AptHWn7d5NQmeS/LTEHIcLr5PaTzl3dK2Z0xSUHHTHwaL5g== + dependencies: + "@types/lodash" "*" + +"@types/lodash@*": + version "4.17.13" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.13.tgz#786e2d67cfd95e32862143abe7463a7f90c300eb" + integrity sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg== + +"@types/markdown-it-anchor@^4.0.1": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@types/markdown-it-anchor/-/markdown-it-anchor-4.0.4.tgz#f36b67608d238d15024fb6508efd7ad3990209f6" + integrity sha512-3CFeYzh8q7DaQboo1g+nnyXjPIvvtzoM+iBQU25sfcWVnjXckAvZIwAmPSVwHxnIblcTVcGq1XjEJjiBZFKkRA== + dependencies: + "@types/markdown-it" "*" + +"@types/markdown-it@*": + version "14.1.2" + resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61" + integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog== + dependencies: + "@types/linkify-it" "^5" + "@types/mdurl" "^2" + +"@types/markdown-it@^12.2.3": + version "12.2.3" + resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-12.2.3.tgz#0d6f6e5e413f8daaa26522904597be3d6cd93b51" + integrity sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ== + dependencies: + "@types/linkify-it" "*" + "@types/mdurl" "*" + +"@types/mdurl@*", "@types/mdurl@^2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd" + integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg== + +"@types/mime-types@^2.1.0": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@types/mime-types/-/mime-types-2.1.4.tgz#93a1933e24fed4fb9e4adc5963a63efcbb3317a2" + integrity sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w== + +"@types/mime@^1": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + +"@types/mime@^2.0.1": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a" + integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q== + +"@types/mocha@^10.0.0": + version "10.0.10" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.10.tgz#91f62905e8d23cbd66225312f239454a23bebfa0" + integrity sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q== + +"@types/multer@^1.4.7": + version "1.4.12" + resolved "https://registry.yarnpkg.com/@types/multer/-/multer-1.4.12.tgz#da67bd0c809f3a63fe097c458c0d4af1fea50ab7" + integrity sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg== + dependencies: + "@types/express" "*" + +"@types/node-fetch@^2.5.7": + version "2.6.12" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.12.tgz#8ab5c3ef8330f13100a7479e2cd56d3386830a03" + integrity sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA== + dependencies: + "@types/node" "*" + form-data "^4.0.0" + +"@types/node@*", "@types/node@>=10.0.0": + version "22.10.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.1.tgz#41ffeee127b8975a05f8c4f83fb89bcb2987d766" + integrity sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ== + dependencies: + undici-types "~6.20.0" + +"@types/p-queue@^2.3.1": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@types/p-queue/-/p-queue-2.3.2.tgz#16bc5fece69ef85efaf2bce8b13f3ebe39c5a1c8" + integrity sha512-eKAv5Ql6k78dh3ULCsSBxX6bFNuGjTmof5Q/T6PiECDq0Yf8IIn46jCyp3RJvCi8owaEmm3DZH1PEImjBMd/vQ== + +"@types/prop-types@*": + version "15.7.13" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.13.tgz#2af91918ee12d9d32914feb13f5326658461b451" + integrity sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA== + +"@types/qs@*": + version "6.9.17" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.17.tgz#fc560f60946d0aeff2f914eb41679659d3310e1a" + integrity sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ== + +"@types/range-parser@*": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + +"@types/react-dom@^18.0.6": + version "18.3.1" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.1.tgz#1e4654c08a9cdcfb6594c780ac59b55aad42fe07" + integrity sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ== + dependencies: + "@types/react" "*" + +"@types/react@*", "@types/react@^18.0.15": + version "18.3.13" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.13.tgz#84c9690d9a271f548659760754ea8745701bfd82" + integrity sha512-ii/gswMmOievxAJed4PAHT949bpYjPKXvXo1v6cRB/kqc2ZR4n+SgyCyvyc5Fec5ez8VnUumI1Vk7j6fRyRogg== + dependencies: + "@types/prop-types" "*" + csstype "^3.0.2" + +"@types/responselike@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.3.tgz#cc29706f0a397cfe6df89debfe4bf5cea159db50" + integrity sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw== + dependencies: + "@types/node" "*" + +"@types/route-parser@^0.1.1": + version "0.1.7" + resolved "https://registry.yarnpkg.com/@types/route-parser/-/route-parser-0.1.7.tgz#76d324537c9f0aaf65c96588c6ab5f3b84ae1505" + integrity sha512-haO+3HVio/4w+yuMJTjqfSo0ivOV8WnXaOReVD6QN729UGBEyizWNGc2Jd0OLsJDucIod4aJSsPLBeLj2uzMCQ== + +"@types/safer-buffer@^2.1.0": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@types/safer-buffer/-/safer-buffer-2.1.3.tgz#901b21c2da54344cf73a205fb5d400592a43b5fd" + integrity sha512-5o3RcpBa7mUFnnnoMa9UIGOf9naD4DCKKMYzqkm9OSY7NNTd26k7adNC+fphcVRI9BUJglBs2yJQiRZYqBF/8w== + dependencies: + "@types/node" "*" + +"@types/semver@^7.5.0": + version "7.5.8" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" + integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== + +"@types/send@*": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" + integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-static@*": + version "1.15.7" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714" + integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + "@types/send" "*" + +"@types/tar-fs@^1.16.1": + version "1.16.3" + resolved "https://registry.yarnpkg.com/@types/tar-fs/-/tar-fs-1.16.3.tgz#425b2b817c405d13d051f36ec6ec6ebd25e31069" + integrity sha512-Y+fdeg11tb9J3UNIatNtrTPM1i8U+WLv2mMhZ3W13mtU19stCgrXJ4iXLkTpoF8jqHi3T/qTS8+fQ3IPzXxpuA== + dependencies: + "@types/node" "*" + "@types/tar-stream" "*" + +"@types/tar-stream@*": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@types/tar-stream/-/tar-stream-3.1.3.tgz#f61427229691eda1b7d5719f34acdc4fc8a558ce" + integrity sha512-Zbnx4wpkWBMBSu5CytMbrT5ZpMiF55qgM+EpHzR4yIDu7mv52cej8hTkOc6K+LzpkOAbxwn/m7j3iO+/l42YkQ== + dependencies: + "@types/node" "*" + +"@types/trusted-types@*": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" + integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== + +"@types/uuid@^9.0.8": + version "9.0.8" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba" + integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA== + +"@types/write-json-file@^2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@types/write-json-file/-/write-json-file-2.2.1.tgz#74155aaccbb0d532be21f9d66bebc4ea875a5a62" + integrity sha512-JdO/UpPm9RrtQBNVcZdt3M7j3mHO/kXaea9LBGx3UgWJd1f9BkIWP7jObLBG6ZtRyqp7KzLFEsaPhWcidVittA== + +"@types/ws@^8.5.5": + version "8.5.13" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.13.tgz#6414c280875e2691d0d1e080b05addbf5cb91e20" + integrity sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA== + dependencies: + "@types/node" "*" + +"@types/yargs-parser@*": + version "21.0.3" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" + integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== + +"@types/yargs@^15": + version "15.0.19" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.19.tgz#328fb89e46109ecbdb70c295d96ff2f46dfd01b9" + integrity sha512-2XUaGVmyQjgyAZldf0D0c14vvo/yv0MhQBSTJcejMMaitsn3nxCB6TmH4G0ZQf+uxROOa9mpanoSm8h6SG/1ZA== + dependencies: + "@types/yargs-parser" "*" + +"@types/yauzl@^2.9.1": + version "2.10.3" + resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.3.tgz#e9b2808b4f109504a03cda958259876f61017999" + integrity sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q== + dependencies: + "@types/node" "*" + +"@virtuoso.dev/react-urx@^0.2.12": + version "0.2.13" + resolved "https://registry.yarnpkg.com/@virtuoso.dev/react-urx/-/react-urx-0.2.13.tgz#e2cfc42d259d2a002695e7517d34cb97b64ee9c4" + integrity sha512-MY0ugBDjFb5Xt8v2HY7MKcRGqw/3gTpMlLXId2EwQvYJoC8sP7nnXjAxcBtTB50KTZhO0SbzsFimaZ7pSdApwA== + dependencies: + "@virtuoso.dev/urx" "^0.2.13" + +"@virtuoso.dev/urx@^0.2.12", "@virtuoso.dev/urx@^0.2.13": + version "0.2.13" + resolved "https://registry.yarnpkg.com/@virtuoso.dev/urx/-/urx-0.2.13.tgz#a65e7e8d923cb03397ac876bfdd45c7f71c8edf1" + integrity sha512-iirJNv92A1ZWxoOHHDYW/1KPoi83939o83iUBQHIim0i3tMeSKEh+bxhJdTHQ86Mr4uXx9xGUTq69cp52ZP8Xw== + +"@vscode/codicons@*": + version "0.0.36" + resolved "https://registry.yarnpkg.com/@vscode/codicons/-/codicons-0.0.36.tgz#ccdabfaef5db596b266644ab85fc25aa701058f0" + integrity sha512-wsNOvNMMJ2BY8rC2N2MNBG7yOowV3ov8KlvUE/AiVUlHKTfWsw3OgAOQduX7h0Un6GssKD3aoTVH+TF3DSQwKQ== + +"@vscode/debugprotocol@^1.51.0": + version "1.68.0" + resolved "https://registry.yarnpkg.com/@vscode/debugprotocol/-/debugprotocol-1.68.0.tgz#e558ba6affe1be7aff4ec824599f316b61d9a69d" + integrity sha512-2J27dysaXmvnfuhFGhfeuxfHRXunqNPxtBoR3koiTOA9rdxWNDTa1zIFLCFMSHJ9MPTPKFcBeblsyaCJCIlQxg== + +"@vscode/proxy-agent@^0.13.2": + version "0.13.2" + resolved "https://registry.yarnpkg.com/@vscode/proxy-agent/-/proxy-agent-0.13.2.tgz#0d289826c07faecc4ca07de80a8e5a9459d06119" + integrity sha512-BSUd0NTj44WvG4O9A6N+4R1XhxtPqCYltWeHyNkquX9T//a1US+cd8fxzcZCPd3z7dygdYIPkZAKM+CrefWWOA== + dependencies: + "@tootallnate/once" "^1.1.2" + agent-base "^6.0.2" + debug "^4.3.1" + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + socks-proxy-agent "^5.0.0" + optionalDependencies: + "@vscode/windows-ca-certs" "^0.3.1" + +"@vscode/ripgrep@^1.14.2": + version "1.15.9" + resolved "https://registry.yarnpkg.com/@vscode/ripgrep/-/ripgrep-1.15.9.tgz#92279f7f28e1e49ad9a89603e10b17a4c7f9f5f1" + integrity sha512-4q2PXRvUvr3bF+LsfrifmUZgSPmCNcUZo6SbEAZgArIChchkezaxLoIeQMJe/z3CCKStvaVKpBXLxN3Z8lQjFQ== + dependencies: + https-proxy-agent "^7.0.2" + proxy-from-env "^1.1.0" + yauzl "^2.9.2" + +"@vscode/windows-ca-certs@^0.3.1": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@vscode/windows-ca-certs/-/windows-ca-certs-0.3.3.tgz#7943d78db7d1d99bcd82eb8a0765a870b111d003" + integrity sha512-C0Iq5RcH+H31GUZ8bsMORsX3LySVkGAqe4kQfUSVcCqJ0QOhXkhgwUMU7oCiqYLXaQWyXFp6Fj6eMdt05uK7VA== + dependencies: + node-addon-api "^8.2.0" + +"@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.14.1.tgz#a9f6a07f2b03c95c8d38c4536a1fdfb521ff55b6" + integrity sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ== + dependencies: + "@webassemblyjs/helper-numbers" "1.13.2" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + +"@webassemblyjs/floating-point-hex-parser@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz#fcca1eeddb1cc4e7b6eed4fc7956d6813b21b9fb" + integrity sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA== + +"@webassemblyjs/helper-api-error@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz#e0a16152248bc38daee76dd7e21f15c5ef3ab1e7" + integrity sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ== + +"@webassemblyjs/helper-buffer@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz#822a9bc603166531f7d5df84e67b5bf99b72b96b" + integrity sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA== + +"@webassemblyjs/helper-numbers@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz#dbd932548e7119f4b8a7877fd5a8d20e63490b2d" + integrity sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.13.2" + "@webassemblyjs/helper-api-error" "1.13.2" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz#e556108758f448aae84c850e593ce18a0eb31e0b" + integrity sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA== + +"@webassemblyjs/helper-wasm-section@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz#9629dda9c4430eab54b591053d6dc6f3ba050348" + integrity sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/wasm-gen" "1.14.1" + +"@webassemblyjs/ieee754@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz#1c5eaace1d606ada2c7fd7045ea9356c59ee0dba" + integrity sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.13.2.tgz#57c5c3deb0105d02ce25fa3fd74f4ebc9fd0bbb0" + integrity sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.13.2.tgz#917a20e93f71ad5602966c2d685ae0c6c21f60f1" + integrity sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ== + +"@webassemblyjs/wasm-edit@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz#ac6689f502219b59198ddec42dcd496b1004d597" + integrity sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/helper-wasm-section" "1.14.1" + "@webassemblyjs/wasm-gen" "1.14.1" + "@webassemblyjs/wasm-opt" "1.14.1" + "@webassemblyjs/wasm-parser" "1.14.1" + "@webassemblyjs/wast-printer" "1.14.1" + +"@webassemblyjs/wasm-gen@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz#991e7f0c090cb0bb62bbac882076e3d219da9570" + integrity sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/ieee754" "1.13.2" + "@webassemblyjs/leb128" "1.13.2" + "@webassemblyjs/utf8" "1.13.2" + +"@webassemblyjs/wasm-opt@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz#e6f71ed7ccae46781c206017d3c14c50efa8106b" + integrity sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/wasm-gen" "1.14.1" + "@webassemblyjs/wasm-parser" "1.14.1" + +"@webassemblyjs/wasm-parser@1.14.1", "@webassemblyjs/wasm-parser@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz#b3e13f1893605ca78b52c68e54cf6a865f90b9fb" + integrity sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-api-error" "1.13.2" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/ieee754" "1.13.2" + "@webassemblyjs/leb128" "1.13.2" + "@webassemblyjs/utf8" "1.13.2" + +"@webassemblyjs/wast-printer@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz#3bb3e9638a8ae5fdaf9610e7a06b4d9f9aa6fe07" + integrity sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@xtuc/long" "4.2.2" + +"@webpack-cli/configtest@^1.0.3": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.2.0.tgz#7b20ce1c12533912c3b217ea68262365fa29a6f5" + integrity sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg== + +"@webpack-cli/info@^1.2.4": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-1.5.0.tgz#6c78c13c5874852d6e2dd17f08a41f3fe4c261b1" + integrity sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ== + dependencies: + envinfo "^7.7.3" + +"@webpack-cli/serve@^1.4.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.7.0.tgz#e1993689ac42d2b16e9194376cfb6753f6254db1" + integrity sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q== + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +"@yarnpkg/lockfile@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" + integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== + +abab@^2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" + integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== + +abbrev@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +accepts@~1.3.4, accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn@^8.14.0, acorn@^8.8.2: + version "8.14.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0" + integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== + +advanced-mark.js@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/advanced-mark.js/-/advanced-mark.js-2.6.0.tgz#86ea8b81152b543db91b1602df4e69282c3b0083" + integrity sha512-b6Q7iNkXk1BTUvmbvtig+/p3Z54qDQqOoOKPJUZXYtCdgeZPz/qj9xZs6GKKQA2/OIOrlNwSt/OvyIq06eYPVw== + +agent-base@6, agent-base@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +agent-base@^7.0.2, agent-base@^7.1.0, agent-base@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.1.tgz#bdbded7dfb096b751a2a087eeeb9664725b2e317" + integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA== + dependencies: + debug "^4.3.4" + +agentkeepalive@^4.2.1: + version "4.5.0" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.5.0.tgz#2673ad1389b3c418c5a20c5d7364f93ca04be923" + integrity sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew== + dependencies: + humanize-ms "^1.2.1" + +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv-keywords@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" + integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== + dependencies: + fast-deep-equal "^3.1.3" + +ajv@^6.12.4, ajv@^6.12.5, ajv@^6.5.3: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + 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" + +ajv@^8.0.0, ajv@^8.6.3, ajv@^8.9.0: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + +anser@^2.0.1: + version "2.3.0" + resolved "https://registry.yarnpkg.com/anser/-/anser-2.3.0.tgz#9b30c8cffe864e646eebd6cd578ba67f2dfe8b2e" + integrity sha512-pGGR7Nq1K/i9KGs29PvHDXA8AsfZ3OiYRMDClT3FIC085Kbns9CJ7ogq9MEiGnrjd9THOGoh7B+kWzePHzZyJQ== + +ansi-colors@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" + integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== + +ansi-escapes@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654" + integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +append-field@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" + integrity sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw== + +aproba@^1.0.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== + +"aproba@^1.0.3 || ^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" + integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== + +are-we-there-yet@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz#679df222b278c64f2cdba1175cdc00b0d96164bd" + integrity sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.0" + +are-we-there-yet@~1.1.2: + version "1.1.7" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz#b15474a932adab4ff8a50d9adfa7e4e926f21146" + integrity sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g== + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + integrity sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng== + dependencies: + array-uniq "^1.0.1" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + integrity sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q== + +assertion-error@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" + integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== + +ast-types@0.9.6: + version "0.9.6" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.6.tgz#102c9e9e9005d3e7e3829bf0c4fa24ee862ee9b9" + integrity sha512-qEdtR2UH78yyHX/AUNfXmJTlM48XoFZKBdwi1nzkI1mJL21cmbu0cvjxjpkXJ5NENMq42H+hNs8VLJcqXLerBQ== + +ast-types@^0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.4.tgz#ee0d77b343263965ecc3fb62da16e7222b2b6782" + integrity sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w== + dependencies: + tslib "^2.0.1" + +ast-types@^0.9.2: + version "0.9.14" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.14.tgz#d34ba5dffb9d15a44351fd2a9d82e4ab2838b5ba" + integrity sha512-Ebvx7/0lLboCdyEmAw/4GqwBeKIijPveXNiVGhCGCNxc7z26T5he7DC6ARxu8ByKuzUZZcLog+VP8GMyZrBzJw== + +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + +async-mutex@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.3.2.tgz#1485eda5bda1b0ec7c8df1ac2e815757ad1831df" + integrity sha512-HuTK7E7MT7jZEh1P9GtRW9+aTWiDWWi9InbZ5hjxrnRa39KS4BW04+xLBhYNS2aXhHUIKZSw3gj4Pn1pj+qGAA== + dependencies: + tslib "^2.3.1" + +async-mutex@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.4.1.tgz#bccf55b96f2baf8df90ed798cb5544a1f6ee4c2c" + integrity sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA== + dependencies: + tslib "^2.4.0" + +async@^2.6.4: + version "2.6.4" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" + integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA== + dependencies: + lodash "^4.17.14" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + +atomically@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/atomically/-/atomically-1.7.0.tgz#c07a0458432ea6dbc9a3506fffa424b48bccaafe" + integrity sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w== + +autosize@^4.0.2: + version "4.0.4" + resolved "https://registry.yarnpkg.com/autosize/-/autosize-4.0.4.tgz#924f13853a466b633b9309330833936d8bccce03" + integrity sha512-5yxLQ22O0fCRGoxGfeLSNt3J8LB1v+umtpMnPW6XjkTWXKoN0AmXAIhelJcDtFT/Y/wYWmfE+oqU10Q0b8FhaQ== + +axios@^1.7.4: + version "1.7.9" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.9.tgz#d7d071380c132a24accda1b2cfc1535b79ec650a" + integrity sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + +b4a@^1.6.4: + version "1.6.7" + resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.7.tgz#a99587d4ebbfbd5a6e3b21bdb5d5fa385767abe4" + integrity sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg== + +babel-loader@^8.2.2: + version "8.4.1" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.4.1.tgz#6ccb75c66e62c3b144e1c5f2eaec5b8f6c08c675" + integrity sha512-nXzRChX+Z1GoE6yWavBQg6jDslyFF3SDjl2paADuoQtQW10JqShJt62R6eJQ5m/pjJFDT8xgKIWSP85OY8eXeA== + dependencies: + find-cache-dir "^3.3.1" + loader-utils "^2.0.4" + make-dir "^3.1.0" + schema-utils "^2.6.5" + +babel-plugin-polyfill-corejs2@^0.4.10: + version "0.4.12" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz#ca55bbec8ab0edeeef3d7b8ffd75322e210879a9" + integrity sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og== + dependencies: + "@babel/compat-data" "^7.22.6" + "@babel/helper-define-polyfill-provider" "^0.6.3" + semver "^6.3.1" + +babel-plugin-polyfill-corejs3@^0.10.6: + version "0.10.6" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz#2deda57caef50f59c525aeb4964d3b2f867710c7" + integrity sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.2" + core-js-compat "^3.38.0" + +babel-plugin-polyfill-regenerator@^0.6.1: + version "0.6.3" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz#abeb1f3f1c762eace37587f42548b08b57789bc8" + integrity sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.3" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +bare-events@^2.0.0, bare-events@^2.2.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.5.0.tgz#305b511e262ffd8b9d5616b056464f8e1b3329cc" + integrity sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A== + +bare-fs@^2.1.1: + version "2.3.5" + resolved "https://registry.yarnpkg.com/bare-fs/-/bare-fs-2.3.5.tgz#05daa8e8206aeb46d13c2fe25a2cd3797b0d284a" + integrity sha512-SlE9eTxifPDJrT6YgemQ1WGFleevzwY+XAP1Xqgl56HtcrisC2CHCZ2tq6dBpcH2TnNxwUEUGhweo+lrQtYuiw== + dependencies: + bare-events "^2.0.0" + bare-path "^2.0.0" + bare-stream "^2.0.0" + +bare-os@^2.1.0: + version "2.4.4" + resolved "https://registry.yarnpkg.com/bare-os/-/bare-os-2.4.4.tgz#01243392eb0a6e947177bb7c8a45123d45c9b1a9" + integrity sha512-z3UiI2yi1mK0sXeRdc4O1Kk8aOa/e+FNWZcTiPB/dfTWyLypuE99LibgRaQki914Jq//yAWylcAt+mknKdixRQ== + +bare-path@^2.0.0, bare-path@^2.1.0: + version "2.1.3" + resolved "https://registry.yarnpkg.com/bare-path/-/bare-path-2.1.3.tgz#594104c829ef660e43b5589ec8daef7df6cedb3e" + integrity sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA== + dependencies: + bare-os "^2.1.0" + +bare-stream@^2.0.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/bare-stream/-/bare-stream-2.4.2.tgz#5a4241ff8a3bdd6d037fc459ab3e41189d2f2576" + integrity sha512-XZ4ln/KV4KT+PXdIWTKjsLY+quqCaEtqqtgGJVPw9AoM73By03ij64YjepK0aQvHSWDb6AfAZwqKaFu68qkrdA== + dependencies: + streamx "^2.20.0" + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +base64id@2.0.0, base64id@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" + integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== + +basic-auth@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" + integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== + dependencies: + safe-buffer "5.1.2" + +basic-ftp@^5.0.2: + version "5.0.5" + resolved "https://registry.yarnpkg.com/basic-ftp/-/basic-ftp-5.0.5.tgz#14a474f5fffecca1f4f406f1c26b18f800225ac0" + integrity sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg== + +bent@^7.1.0: + version "7.3.12" + resolved "https://registry.yarnpkg.com/bent/-/bent-7.3.12.tgz#e0a2775d4425e7674c64b78b242af4f49da6b035" + integrity sha512-T3yrKnVGB63zRuoco/7Ybl7BwwGZR0lceoVG5XmQyMIH9s19SV5m+a8qam4if0zQuAmOQTyPTPmsQBdAorGK3w== + dependencies: + bytesish "^0.4.1" + caseless "~0.12.0" + is-stream "^2.0.0" + +big-integer@^1.6.17: + version "1.6.52" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.52.tgz#60a887f3047614a8e1bffe5d7173490a97dc8c85" + integrity sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg== + +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +binary@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79" + integrity sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg== + dependencies: + buffers "~0.1.1" + chainsaw "~0.1.0" + +bindings@^1.3.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" + integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== + dependencies: + file-uri-to-path "1.0.0" + +bintrees@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bintrees/-/bintrees-1.0.2.tgz#49f896d6e858a4a499df85c38fb399b9aff840f8" + integrity sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw== + +bl@^1.0.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.3.tgz#1e8dd80142eac80d7158c9dccc047fb620e035e7" + integrity sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww== + dependencies: + readable-stream "^2.3.5" + safe-buffer "^5.1.1" + +bl@^4.0.3, bl@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + +bluebird@~3.4.1: + version "3.4.7" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" + integrity sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA== + +body-parser@1.20.3, body-parser@^1.17.2, body-parser@^1.18.3: + version "1.20.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.13.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +boolean@^3.0.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.2.0.tgz#9e5294af4e98314494cbb17979fa54ca159f116b" + integrity sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.3, braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browser-stdout@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +browserslist@^4.24.0, browserslist@^4.24.2: + version "4.24.2" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.2.tgz#f5845bc91069dbd55ee89faf9822e1d885d16580" + integrity sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg== + dependencies: + caniuse-lite "^1.0.30001669" + electron-to-chromium "^1.5.41" + node-releases "^2.0.18" + update-browserslist-db "^1.1.1" + +buffer-alloc-unsafe@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" + integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg== + +buffer-alloc@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec" + integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow== + dependencies: + buffer-alloc-unsafe "^1.1.0" + buffer-fill "^1.0.0" + +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== + +buffer-fill@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" + integrity sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ== + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer-indexof-polyfill@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz#d2732135c5999c64b277fcf9b1abe3498254729c" + integrity sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A== + +buffer@^5.2.1, buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + +buffers@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" + integrity sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ== + +busboy@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" + integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== + dependencies: + streamsearch "^1.1.0" + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +bytesish@^0.4.1: + version "0.4.4" + resolved "https://registry.yarnpkg.com/bytesish/-/bytesish-0.4.4.tgz#f3b535a0f1153747427aee27256748cff92347e6" + integrity sha512-i4uu6M4zuMUiyfZN4RU2+i9+peJh//pXhd9x1oSe1LBkZ3LEbCoygu8W0bXTukU1Jme2txKuotpCZRaC3FLxcQ== + +cacache@^16.1.0: + version "16.1.3" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-16.1.3.tgz#a02b9f34ecfaf9a78c9f4bc16fceb94d5d67a38e" + integrity sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ== + 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" + +cacheable-lookup@^5.0.3: + version "5.0.4" + resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz#5a6b865b2c44357be3d5ebc2a467b032719a7005" + integrity sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA== + +cacheable-request@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.4.tgz#7a33ebf08613178b403635be7b899d3e69bbe817" + integrity sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^4.0.0" + lowercase-keys "^2.0.0" + normalize-url "^6.0.1" + responselike "^2.0.0" + +call-bind@^1.0.5, call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase@^5.0.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +camelcase@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-lite@^1.0.30001669: + version "1.0.30001686" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001686.tgz#0e04b8d90de8753188e93c9989d56cb19d902670" + integrity sha512-Y7deg0Aergpa24M3qLC5xjNklnKnhsmSyR/V89dLZ1n0ucJIFNs7PgR2Yfa/Zf6W79SbBicgtGxZr2juHkEUIA== + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== + +chai@^4.3.10: + version "4.5.0" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.5.0.tgz#707e49923afdd9b13a8b0b47d33d732d13812fd8" + integrity sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw== + dependencies: + assertion-error "^1.1.0" + check-error "^1.0.3" + deep-eql "^4.1.3" + get-func-name "^2.0.2" + loupe "^2.3.6" + pathval "^1.1.1" + type-detect "^4.1.0" + +chainsaw@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98" + integrity sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ== + dependencies: + traverse ">=0.3.0 <0.4" + +chalk@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.0.0.tgz#6e98081ed2d17faab615eb52ac66ec1fe6209e72" + integrity sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +check-error@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.3.tgz#a6502e4312a7ee969f646e83bb3ddd56281bd694" + integrity sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg== + dependencies: + get-func-name "^2.0.2" + +chokidar@^3.5.3: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + 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" + optionalDependencies: + fsevents "~2.3.2" + +chownr@^1.0.1, chownr@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + +chrome-trace-event@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" + integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== + +chromium-bidi@0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/chromium-bidi/-/chromium-bidi-0.6.4.tgz#627d76bae2819d59b61a413babe9664e0a16b71d" + integrity sha512-8zoq6ogmhQQkAKZVKO2ObFTl4uOkqoX1PlKQX3hZQ5E9cbUotcAb7h4pTNVAGGv8Z36PF3CtdOriEp/Rz82JqQ== + dependencies: + mitt "3.0.1" + urlpattern-polyfill "10.0.0" + zod "3.23.8" + +ci-info@^3.7.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" + integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== + +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-spinners@^2.5.0: + version "2.9.2" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.2.tgz#1773a8f4b9c4d6ac31563df53b3fc1d79462fe41" + integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== + +cliui@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" + integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^6.2.0" + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +clone-response@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.3.tgz#af2032aa47816399cf5f0a1d0db902f517abb8c3" + integrity sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA== + dependencies: + mimic-response "^1.0.0" + +clone@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== + +clone@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" + integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w== + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + integrity sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA== + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-support@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + +colorette@^1.2.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40" + integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g== + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@^2.20.0, commander@^2.8.1: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@^7.0.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== + +compression-webpack-plugin@^9.0.0: + version "9.2.0" + resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-9.2.0.tgz#57fd539d17c5907eebdeb4e83dcfe2d7eceb9ef6" + integrity sha512-R/Oi+2+UHotGfu72fJiRoVpuRifZT0tTC6UqFD/DUo+mv8dbOow9rVOuTvDv5nPPm3GZhHL/fKkwxwIHnJ8Nyw== + dependencies: + schema-utils "^4.0.0" + serialize-javascript "^6.0.0" + +computed-style@~0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/computed-style/-/computed-style-0.1.4.tgz#7f344fd8584b2e425bedca4a1afc0e300bb05d74" + integrity sha512-WpAmaKbMNmS3OProfHIdJiNleNJdgUrJfbKArXua28QF7+0CoZjlLn0lp6vlc+dl5r2/X9GQiQRQQU4BzSa69w== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +concat-stream@^1.5.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +conf@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/conf/-/conf-10.2.0.tgz#838e757be963f1a2386dfe048a98f8f69f7b55d6" + integrity sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg== + dependencies: + ajv "^8.6.3" + ajv-formats "^2.1.1" + atomically "^1.7.0" + debounce-fn "^4.0.0" + dot-prop "^6.0.1" + env-paths "^2.2.1" + json-schema-typed "^7.0.3" + onetime "^5.1.2" + pkg-up "^3.1.0" + semver "^7.3.5" + +console-control-strings@^1.0.0, console-control-strings@^1.1.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" + integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== + +cookie@^0.4.0: + version "0.4.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== + +cookie@~0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" + integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== + +copy-anything@^2.0.1: + version "2.0.6" + resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-2.0.6.tgz#092454ea9584a7b7ad5573062b2a87f5900fc480" + integrity sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw== + dependencies: + is-what "^3.14.1" + +copy-webpack-plugin@^8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-8.1.1.tgz#3f697e162764925c2f0d235f380676125508fd26" + integrity sha512-rYM2uzRxrLRpcyPqGceRBDpxxUV8vcDqIKxAUKfcnFpcrPxT5+XvhTxv7XLjo5AvEJFPdAE3zCogG2JVahqgSQ== + dependencies: + fast-glob "^3.2.5" + glob-parent "^5.1.1" + globby "^11.0.3" + normalize-path "^3.0.0" + p-limit "^3.1.0" + schema-utils "^3.0.0" + serialize-javascript "^5.0.1" + +core-js-compat@^3.38.0, core-js-compat@^3.38.1: + version "3.39.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.39.0.tgz#b12dccb495f2601dc860bdbe7b4e3ffa8ba63f61" + integrity sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw== + dependencies: + browserslist "^4.24.2" + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cors@~2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +corser@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/corser/-/corser-2.0.1.tgz#8eda252ecaab5840dcd975ceb90d9370c819ff87" + integrity sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ== + +cosmiconfig@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-9.0.0.tgz#34c3fc58287b915f3ae905ab6dc3de258b55ad9d" + integrity sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg== + dependencies: + env-paths "^2.2.1" + import-fresh "^3.3.0" + js-yaml "^4.1.0" + parse-json "^5.2.0" + +cross-spawn@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41" + integrity sha512-yAXz/pA1tD8Gtg2S98Ekf/sewp3Lcp3YoFKJ4Hkp5h5yLWnKVTDU0kwjKJ8NDCYcfTLfyGkzTikst+jWypT1iA== + dependencies: + lru-cache "^4.0.1" + which "^1.2.9" + +cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.3: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +css-loader@^6.2.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.11.0.tgz#33bae3bf6363d0a7c2cf9031c96c744ff54d85ba" + integrity sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g== + dependencies: + icss-utils "^5.1.0" + postcss "^8.4.33" + postcss-modules-extract-imports "^3.1.0" + postcss-modules-local-by-default "^4.0.5" + postcss-modules-scope "^3.2.0" + postcss-modules-values "^4.0.0" + postcss-value-parser "^4.2.0" + semver "^7.5.4" + +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + +csstype@^3.0.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + +data-uri-to-buffer@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz#8a58bb67384b261a38ef18bea1810cb01badd28b" + integrity sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw== + +debounce-fn@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/debounce-fn/-/debounce-fn-4.0.0.tgz#ed76d206d8a50e60de0dd66d494d82835ffe61c7" + integrity sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ== + dependencies: + mimic-fn "^3.0.0" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.3, debug@^4.3.4, debug@^4.3.5, debug@^4.3.6, debug@~4.3.1, debug@~4.3.2, debug@~4.3.4: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + +debug@^3.1.0, debug@^3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== + +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== + +decompress-response@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986" + integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw== + dependencies: + mimic-response "^2.0.0" + +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== + dependencies: + mimic-response "^3.1.0" + +decompress-tar@^4.0.0, decompress-tar@^4.1.0, decompress-tar@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/decompress-tar/-/decompress-tar-4.1.1.tgz#718cbd3fcb16209716e70a26b84e7ba4592e5af1" + integrity sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ== + dependencies: + file-type "^5.2.0" + is-stream "^1.1.0" + tar-stream "^1.5.2" + +decompress-tarbz2@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz#3082a5b880ea4043816349f378b56c516be1a39b" + integrity sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A== + dependencies: + decompress-tar "^4.1.0" + file-type "^6.1.0" + is-stream "^1.1.0" + seek-bzip "^1.0.5" + unbzip2-stream "^1.0.9" + +decompress-targz@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/decompress-targz/-/decompress-targz-4.1.1.tgz#c09bc35c4d11f3de09f2d2da53e9de23e7ce1eee" + integrity sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w== + dependencies: + decompress-tar "^4.1.1" + file-type "^5.2.0" + is-stream "^1.1.0" + +decompress-unzip@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/decompress-unzip/-/decompress-unzip-4.0.1.tgz#deaaccdfd14aeaf85578f733ae8210f9b4848f69" + integrity sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw== + dependencies: + file-type "^3.8.0" + get-stream "^2.2.0" + pify "^2.3.0" + yauzl "^2.4.2" + +decompress@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/decompress/-/decompress-4.2.1.tgz#007f55cc6a62c055afa37c07eb6a4ee1b773f118" + integrity sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ== + dependencies: + decompress-tar "^4.0.0" + decompress-tarbz2 "^4.0.0" + decompress-targz "^4.0.0" + decompress-unzip "^4.0.1" + graceful-fs "^4.1.10" + make-dir "^1.0.0" + pify "^2.3.0" + strip-dirs "^2.0.0" + +deep-eql@^4.1.3: + version "4.1.4" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.4.tgz#d0d3912865911bb8fac5afb4e3acfa6a28dc72b7" + integrity sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg== + dependencies: + type-detect "^4.0.0" + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +deepmerge@^4.2.2: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + +default-shell@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/default-shell/-/default-shell-1.0.1.tgz#752304bddc6174f49eb29cb988feea0b8813c8bc" + integrity sha512-/Os8tTMPSriNHCsVj3VLjMZblIl1sIg8EXz3qg7C5K+y9calfTA/qzlfPvCQ+LEgLWmtZ9wCnzE1w+S6TPPFyQ== + +defaults@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a" + integrity sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A== + dependencies: + clone "^1.0.2" + +defer-to-connect@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" + integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== + +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + +degenerator@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/degenerator/-/degenerator-5.0.1.tgz#9403bf297c6dad9a1ece409b37db27954f91f2f5" + integrity sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ== + dependencies: + ast-types "^0.13.4" + escodegen "^2.1.0" + esprima "^4.0.1" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-indent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" + integrity sha512-rlpvsxUtM0PQvy9iZe640/IWwWYyBsTApREbA1pHOpmOUIl9MkP/U4z7vTtg4Oaojvqhxt7sdufnT0EzGaR31g== + +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== + +detect-libc@^2.0.1, detect-libc@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" + integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== + +detect-node@^2.0.4: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" + integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== + +devtools-protocol@0.0.1312386: + version "0.0.1312386" + resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz#5ab824d6f1669ec6c6eb0fba047e73601d969052" + integrity sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA== + +diff@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" + integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== + +dir-glob@^2.0.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4" + integrity sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw== + dependencies: + path-type "^3.0.0" + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +dompurify@^2.2.9: + version "2.5.7" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.5.7.tgz#6e0d36b9177db5a99f18ade1f28579db5ab839d7" + integrity sha512-2q4bEI+coQM8f5ez7kt2xclg1XsecaV9ASJk/54vwlfRRNQfDqJz2pzQ8t0Ix/ToBpXlVjrRIx7pFC/o8itG2Q== + +dot-prop@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-6.0.1.tgz#fc26b3cf142b9e59b74dbd39ed66ce620c681083" + integrity sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA== + dependencies: + is-obj "^2.0.0" + +drivelist@^9.0.2: + version "9.2.4" + resolved "https://registry.yarnpkg.com/drivelist/-/drivelist-9.2.4.tgz#f89d2233d86c9fcdbc0daacae257d7b27a658d20" + integrity sha512-F36yn+qXwiOGZM16FYPKcIRjC7qXDIA0SBZ0vvTEe01ai788Se8z78acYdgXC8NAsghiO+9c/GYXgU7E9hhUpg== + dependencies: + bindings "^1.3.0" + debug "^3.1.0" + nan "^2.14.0" + prebuild-install "^5.2.4" + +duplexer2@~0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" + integrity sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA== + dependencies: + readable-stream "^2.0.2" + +duplexer@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" + integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +electron-rebuild@^3.2.7: + version "3.2.9" + resolved "https://registry.yarnpkg.com/electron-rebuild/-/electron-rebuild-3.2.9.tgz#ea372be15f591f8d6d978ee9bca6526dadbcf20f" + integrity sha512-FkEZNFViUem3P0RLYbZkUjC8LUFIK+wKq09GHoOITSJjfDAVQv964hwaNseTTWt58sITQX3/5fHNYcTefqaCWw== + dependencies: + "@malept/cross-spawn-promise" "^2.0.0" + chalk "^4.0.0" + debug "^4.1.1" + detect-libc "^2.0.1" + fs-extra "^10.0.0" + got "^11.7.0" + lzma-native "^8.0.5" + node-abi "^3.0.0" + node-api-version "^0.1.4" + node-gyp "^9.0.0" + ora "^5.1.0" + semver "^7.3.5" + tar "^6.0.5" + yargs "^17.0.1" + +electron-store@^8.0.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/electron-store/-/electron-store-8.2.0.tgz#114e6e453e8bb746ab4ccb542424d8c881ad2ca1" + integrity sha512-ukLL5Bevdil6oieAOXz3CMy+OgaItMiVBg701MNlG6W5RaC0AHN7rvlqTCmeb6O7jP0Qa1KKYTE0xV0xbhF4Hw== + dependencies: + conf "^10.2.0" + type-fest "^2.17.0" + +electron-to-chromium@^1.5.41: + version "1.5.70" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.70.tgz#8be2f92e54a3d069c0eefcf766a48bf0f78d93e0" + integrity sha512-P6FPqAWIZrC3sHDAwBitJBs7N7IF58m39XVny7DFseQXK2eiMn7nNQizFf63mWDDUnFvaqsM8FI0+ZZfLkdUGA== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +emojis-list@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" + integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + +encoding@^0.1.13: + version "0.1.13" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" + integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== + dependencies: + iconv-lite "^0.6.2" + +end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +engine.io-client@~6.6.1: + version "6.6.2" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.6.2.tgz#e0a09e1c90effe5d6264da1c56d7281998f1e50b" + integrity sha512-TAr+NKeoVTjEVW8P3iHguO1LO6RlUz9O5Y8o7EY0fU+gY1NYqas7NN3slpFtbXEsLMHk0h90fJMfKjRkQ0qUIw== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + engine.io-parser "~5.2.1" + ws "~8.17.1" + xmlhttprequest-ssl "~2.1.1" + +engine.io-parser@~5.2.1: + version "5.2.3" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz#00dc5b97b1f233a23c9398d0209504cf5f94d92f" + integrity sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q== + +engine.io@~6.6.0: + version "6.6.2" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.6.2.tgz#32bd845b4db708f8c774a4edef4e5c8a98b3da72" + integrity sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw== + dependencies: + "@types/cookie" "^0.4.1" + "@types/cors" "^2.8.12" + "@types/node" ">=10.0.0" + accepts "~1.3.4" + base64id "2.0.0" + cookie "~0.7.2" + cors "~2.8.5" + debug "~4.3.1" + engine.io-parser "~5.2.1" + ws "~8.17.1" + +enhanced-resolve@^5.17.1: + version "5.17.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" + integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +entities@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" + integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== + +env-paths@^2.2.0, env-paths@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" + integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== + +envinfo@^7.7.3: + version "7.14.0" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.14.0.tgz#26dac5db54418f2a4c1159153a0b2ae980838aae" + integrity sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg== + +err-code@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" + integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== + +errno@^0.1.1: + version "0.1.8" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" + integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A== + dependencies: + prr "~1.0.1" + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-module-lexer@^1.2.1: + version "1.5.4" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" + integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== + +es6-error@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" + integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== + +es6-promise@^4.2.4: + version "4.2.8" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + +escalade@^3.1.1, escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-html@^1.0.3, escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +escape-string-regexp@^1.0.2: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +escodegen@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.1.0.tgz#ba93bbb7a43986d29d6041f99f5262da773e2e17" + integrity sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w== + dependencies: + esprima "^4.0.1" + estraverse "^5.2.0" + esutils "^2.0.2" + optionalDependencies: + source-map "~0.6.1" + +eslint-scope@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +esprima@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esprima@~3.1.0: + version "3.1.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" + integrity sha512-AWwVMNxwhN8+NIPQzAQZCm7RkLC4RbM3B1OobMuyp3i+w73X57KCKaVIxaRZb+DYCojq7rspo+fmuQfAboyhFg== + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +event-stream@=3.3.4: + version "3.3.4" + resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571" + integrity sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g== + dependencies: + duplexer "~0.1.1" + from "~0" + map-stream "~0.1.0" + pause-stream "0.0.11" + split "0.3" + stream-combiner "~0.0.4" + through "~2.3.1" + +eventemitter3@^4.0.0: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +execa@^0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.5.1.tgz#de3fb85cb8d6e91c85bcbceb164581785cb57b36" + integrity sha512-R66dW/hW3I8yV77Wg4xn6zMguRPUgt59VLm5e85NrOF05ZdPn7YOfPBSw0E9epJDvuzwVWEG+HmEaQ4muYuWKQ== + dependencies: + cross-spawn "^4.0.0" + get-stream "^2.2.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +execa@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-2.1.0.tgz#e5d3ecd837d2a60ec50f3da78fd39767747bbe99" + integrity sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw== + dependencies: + cross-spawn "^7.0.0" + get-stream "^5.0.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^3.0.0" + onetime "^5.1.0" + p-finally "^2.0.0" + signal-exit "^3.0.2" + strip-final-newline "^2.0.0" + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +expand-template@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" + integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== + +exponential-backoff@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6" + integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw== + +express@^4.21.0: + version "4.21.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.21.1.tgz#9dae5dda832f16b4eec941a4e44aa89ec481b281" + integrity sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.3" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.7.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~2.0.0" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.3.1" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.3" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.10" + proxy-addr "~2.0.7" + qs "6.13.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.19.0" + serve-static "1.16.2" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +extract-zip@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" + integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== + dependencies: + debug "^4.1.1" + get-stream "^5.1.0" + yauzl "^2.10.0" + optionalDependencies: + "@types/yauzl" "^2.9.1" + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-fifo@^1.2.0, fast-fifo@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c" + integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ== + +fast-glob@^3.2.5, fast-glob@^3.2.9: + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + 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" + +fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-plist@^0.1.2: + version "0.1.3" + resolved "https://registry.yarnpkg.com/fast-plist/-/fast-plist-0.1.3.tgz#328cd9335e93a2479ac90814a1302437574ea925" + integrity sha512-d9cEfo/WcOezgPLAC/8t8wGb6YOD6JTCPMw2QcG2nAdFmyY+9rTUizCTaGjIZAloWENTEUMAPpkUAIJJJ0i96A== + +fast-uri@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.3.tgz#892a1c91802d5d7860de728f18608a0573142241" + integrity sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw== + +fastest-levenshtein@^1.0.12: + version "1.0.16" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" + integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== + +fastq@^1.6.0: + version "1.17.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" + integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== + dependencies: + reusify "^1.0.4" + +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g== + dependencies: + pend "~1.2.0" + +file-icons-js@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/file-icons-js/-/file-icons-js-1.0.3.tgz#d0765dc1d86aba4b2d7664a39e4ef7af9f12c5af" + integrity sha512-n4zoKEpMaAxBTUB7wtgrFBa4dM3b7mBLLA1VI/Q5Cdk/k2UA8S8oaxvnECp3QOzg0Dn+KKRzfIHF7qSdRkA65Q== + +file-type@^3.8.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9" + integrity sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA== + +file-type@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-5.2.0.tgz#2ddbea7c73ffe36368dfae49dc338c058c2b8ad6" + integrity sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ== + +file-type@^6.1.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-6.2.0.tgz#e50cd75d356ffed4e306dc4f5bcf52a79903a919" + integrity sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg== + +file-uri-to-path@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" + integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== + +filename-reserved-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz#abf73dfab735d045440abfea2d91f389ebbfa229" + integrity sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ== + +filenamify@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/filenamify/-/filenamify-4.3.0.tgz#62391cb58f02b09971c9d4f9d63b3cf9aba03106" + integrity sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg== + dependencies: + filename-reserved-regex "^2.0.0" + strip-outer "^1.0.1" + trim-repeated "^1.0.0" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" + integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== + dependencies: + debug "2.6.9" + encodeurl "~2.0.0" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +find-cache-dir@^3.3.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" + integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== + dependencies: + commondir "^1.0.1" + make-dir "^3.0.2" + pkg-dir "^4.1.0" + +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +find-up@^4.0.0, find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +find-yarn-workspace-root@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz#f47fb8d239c900eb78179aa81b66673eac88f7bd" + integrity sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ== + dependencies: + micromatch "^4.0.2" + +fix-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/fix-path/-/fix-path-3.0.0.tgz#c6b82fd5f5928e520b392a63565ebfef0ddf037e" + integrity sha512-opGAl4+ip5jUikHR2C8Jo7czZ80pz8EK/0gMlAZu7xgDmBqIynlX8SMYg9KowYjAU6HT0nxsSJEWru0u+n+N2Q== + dependencies: + shell-path "^2.1.0" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +follow-redirects@^1.0.0, follow-redirects@^1.15.6: + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + +font-awesome@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/font-awesome/-/font-awesome-4.7.0.tgz#8fa8cf0411a1a31afd07b06d2902bb9fc815a133" + integrity sha512-U6kGnykA/6bFmg1M/oT9EkFeIYv7JlX3bozwQJWiiLz6L0w3F5vBVPxHlwyX/vtNq1ckcpRKOB9f2Qal/VtFpg== + +foreground-child@^3.1.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77" + integrity sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + +form-data@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.1.tgz#ba1076daaaa5bfd7e99c1a6cb02aa0a5cff90d48" + integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +from@~0: + version "0.1.7" + resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" + integrity sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g== + +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + +fs-extra@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-extra@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94" + integrity sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg== + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-extra@^9.0.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-minipass@^2.0.0, fs-minipass@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +fstream@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" + integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg== + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +fuzzy@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/fuzzy/-/fuzzy-0.1.3.tgz#4c76ec2ff0ac1a36a9dccf9a00df8623078d4ed8" + integrity sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w== + +gauge@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.4.tgz#52ff0652f2bbf607a989793d53b751bef2328dce" + integrity sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg== + 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" + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + integrity sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg== + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-caller-file@^2.0.1, get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-func-name@^2.0.1, get-func-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" + integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== + +get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +get-stream@^2.2.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-2.3.1.tgz#5f38f93f346009666ee0150a054167f91bdd95de" + integrity sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA== + dependencies: + object-assign "^4.0.1" + pinkie-promise "^2.0.0" + +get-stream@^5.0.0, get-stream@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +get-uri@^6.0.1: + version "6.0.4" + resolved "https://registry.yarnpkg.com/get-uri/-/get-uri-6.0.4.tgz#6daaee9e12f9759e19e55ba313956883ef50e0a7" + integrity sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ== + dependencies: + basic-ftp "^5.0.2" + data-uri-to-buffer "^6.0.2" + debug "^4.3.4" + +github-from-package@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" + integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw== + +glob-parent@^5.1.1, glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@^10.3.7: + version "10.4.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" + integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" + +glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.2.0: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + 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" + +glob@^8.0.1, glob@^8.0.3, glob@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + +global-agent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/global-agent/-/global-agent-3.0.0.tgz#ae7cd31bd3583b93c5a16437a1afe27cc33a1ab6" + integrity sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q== + dependencies: + boolean "^3.0.1" + es6-error "^4.1.1" + matcher "^3.0.0" + roarr "^2.15.3" + semver "^7.3.2" + serialize-error "^7.0.1" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globalthis@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" + integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== + dependencies: + define-properties "^1.2.1" + gopd "^1.0.1" + +globby@^11.0.3: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + 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" + +globby@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/globby/-/globby-7.1.1.tgz#fb2ccff9401f8600945dfada97440cca972b8680" + integrity sha512-yANWAN2DUcBtuus5Cpd+SKROzXHs2iVXFZt/Ykrfz6SAXqacLX25NZpltE+39ceMexYF4TtEadjuSTw8+3wX4g== + dependencies: + array-union "^1.0.1" + dir-glob "^2.0.0" + glob "^7.1.2" + ignore "^3.3.5" + pify "^3.0.0" + slash "^1.0.0" + +gopd@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + +got@^11.7.0, got@^11.8.5: + version "11.8.6" + resolved "https://registry.yarnpkg.com/got/-/got-11.8.6.tgz#276e827ead8772eddbcfc97170590b841823233a" + integrity sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g== + dependencies: + "@sindresorhus/is" "^4.0.0" + "@szmarczak/http-timer" "^4.0.5" + "@types/cacheable-request" "^6.0.1" + "@types/responselike" "^1.0.0" + cacheable-lookup "^5.0.3" + cacheable-request "^7.0.2" + decompress-response "^6.0.0" + http2-wrapper "^1.0.0-beta.5.2" + lowercase-keys "^2.0.0" + p-cancelable "^2.0.0" + responselike "^2.0.0" + +graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.1.0.tgz#deb10494cbbe8809bce168a3b961f42969f5ed43" + integrity sha512-QLdzI9IIO1Jg7f9GT1gXpPpXArAn6cS31R1eEZqz08Gc+uQ8/XiqHWt17Fiw+2p6oTTIq5GXEpQkAlA88YRl/Q== + dependencies: + call-bind "^1.0.7" + +has-symbols@^1.0.3: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + +has-unicode@^2.0.0, has-unicode@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== + +hasown@^2.0.0, hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +he@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +highlight.js@*: + version "11.10.0" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.10.0.tgz#6e3600dc4b33d6dc23d5bd94fbf72405f5892b92" + integrity sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ== + +highlight.js@10.4.1: + version "10.4.1" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.4.1.tgz#d48fbcf4a9971c4361b3f95f302747afe19dbad0" + integrity sha512-yR5lWvNz7c85OhVAEAeFhVCc/GV4C30Fjzc/rCP0aCWzc1UUOPUk55dK/qdwTZHBvMZo+eZ2jpk62ndX/xMFlg== + +html-encoding-sniffer@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9" + integrity sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA== + dependencies: + whatwg-encoding "^2.0.0" + +http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" + integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-proxy-agent@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" + integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== + dependencies: + "@tootallnate/once" "1" + agent-base "6" + debug "4" + +http-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" + integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== + dependencies: + "@tootallnate/once" "2" + agent-base "6" + debug "4" + +http-proxy-agent@^7.0.0, http-proxy-agent@^7.0.1: + version "7.0.2" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e" + integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== + dependencies: + agent-base "^7.1.0" + debug "^4.3.4" + +http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +http-server@^14.1.1: + version "14.1.1" + resolved "https://registry.yarnpkg.com/http-server/-/http-server-14.1.1.tgz#d60fbb37d7c2fdff0f0fbff0d0ee6670bd285e2e" + integrity sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A== + dependencies: + basic-auth "^2.0.1" + chalk "^4.1.2" + corser "^2.0.1" + he "^1.2.0" + html-encoding-sniffer "^3.0.0" + http-proxy "^1.18.1" + mime "^1.6.0" + minimist "^1.2.6" + opener "^1.5.1" + portfinder "^1.0.28" + secure-compare "3.0.1" + union "~0.5.0" + url-join "^4.0.1" + +http-status-codes@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-1.4.0.tgz#6e4c15d16ff3a9e2df03b89f3a55e1aae05fb477" + integrity sha512-JrT3ua+WgH8zBD3HEJYbeEgnuQaAnUeRRko/YojPAJjGmIfGD3KPU/asLdsLwKjfxOmQe5nXMQ0pt/7MyapVbQ== + +http2-wrapper@^1.0.0-beta.5.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d" + integrity sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg== + dependencies: + quick-lru "^5.1.1" + resolve-alpn "^1.0.0" + +https-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + +https-proxy-agent@^7.0.2, https-proxy-agent@^7.0.3, https-proxy-agent@^7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz#9e8b5013873299e11fab6fd548405da2d6c602b2" + integrity sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw== + dependencies: + agent-base "^7.0.2" + debug "4" + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" + integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== + dependencies: + ms "^2.0.0" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +iconv-lite@0.6.3, iconv-lite@^0.6.0, iconv-lite@^0.6.2: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +icss-utils@^5.0.0, icss-utils@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" + integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== + +idb@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/idb/-/idb-4.0.5.tgz#23b930fbb0abce391e939c35b7b31a669e74041f" + integrity sha512-P+Fk9HT2h1DhXoE1YNK183SY+CRh2GHNh28de94sGwhe0bUA75JJeVJWt3SenE5p0BXK7maflIq29dl6UZHrFw== + +ieee754@^1.1.13, ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +ignore-loader@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ignore-loader/-/ignore-loader-0.1.2.tgz#d81f240376d0ba4f0d778972c3ad25874117a463" + integrity sha512-yOJQEKrNwoYqrWLS4DcnzM7SEQhRKis5mB+LdKKh4cPmGYlLPR0ozRzHV5jmEk2IxptqJNQA5Cc0gw8Fj12bXA== + +ignore@^3.3.5: + version "3.3.10" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" + integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug== + +ignore@^5.2.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + +image-size@~0.5.0: + version "0.5.5" + resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" + integrity sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ== + +import-fresh@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +import-local@^3.0.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" + integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +infer-owner@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" + integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@~1.3.0: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +interpret@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" + integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== + +inversify@6.0.3, inversify@^6.1.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/inversify/-/inversify-6.0.3.tgz#b09e44dad0aaf79bdf92fe788f37507274da21b7" + integrity sha512-s/svzcRQ/scaGUUyaVtFSL1dvOaRgyvE7VvpGcJwXmFz7CCzfSfxC/Uyl7iSHDEmBabJ2gbDES72DaygtMmwvg== + +ip-address@^9.0.5: + version "9.0.5" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a" + integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g== + dependencies: + jsbn "1.1.0" + sprintf-js "^1.1.3" + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.13.0: + version "2.15.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" + integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== + dependencies: + hasown "^2.0.2" + +is-docker@^2.0.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + +is-electron@^2.1.0, is-electron@^2.2.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/is-electron/-/is-electron-2.2.2.tgz#3778902a2044d76de98036f5dc58089ac4d80bb9" + integrity sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + integrity sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw== + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-interactive@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" + integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== + +is-lambda@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" + integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ== + +is-natural-number@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-natural-number/-/is-natural-number-4.0.1.tgz#ab9d76e1db4ced51e35de0c72ebecf09f734cde8" + integrity sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ== + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" + integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== + +is-path-inside@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-plain-obj@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + integrity sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg== + +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + +is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ== + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +is-what@^3.14.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.14.1.tgz#e1222f46ddda85dead0fd1c9df131760e77755c1" + integrity sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA== + +is-wsl@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== + +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + +jest-worker@^27.4.5: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +jsbn@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" + integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== + +jschardet@^2.1.1: + version "2.3.0" + resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-2.3.0.tgz#06e2636e16c8ada36feebbdc08aa34e6a9b3ff75" + integrity sha512-6I6xT7XN/7sBB7q8ObzKbmv5vN+blzLcboDE1BNEsEfmRXJValMxO6OIRT69ylPBRemS3rw6US+CMCar0OBc9g== + +jsesc@^3.0.2, jsesc@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" + integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== + +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json-schema-typed@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/json-schema-typed/-/json-schema-typed-7.0.3.tgz#23ff481b8b4eebcd2ca123b4fa0409e66469a2d9" + integrity sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A== + +json-stable-stringify@^1.0.2: + version "1.1.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.1.1.tgz#52d4361b47d49168bcc4e564189a42e5a7439454" + integrity sha512-SU/971Kt5qVQfJpyDveVhQ/vya+5hvrjClFOcr8c0Fq5aODJjMwutrOfCU+eCnVD5gpx1Q3fEqkyom77zH1iIg== + dependencies: + call-bind "^1.0.5" + isarray "^2.0.5" + jsonify "^0.0.1" + object-keys "^1.1.1" + +json-stringify-safe@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== + +json5@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== + dependencies: + minimist "^1.2.0" + +json5@^2.1.2, json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +jsonc-parser@^2.2.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.3.1.tgz#59549150b133f2efacca48fe9ce1ec0659af2342" + integrity sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg== + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== + optionalDependencies: + graceful-fs "^4.1.6" + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonify@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978" + integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg== + +just-performance@4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/just-performance/-/just-performance-4.3.0.tgz#cc2bc8c9227f09e97b6b1df4cd0de2df7ae16db1" + integrity sha512-L7RjvtJsL0QO8xFs5wEoDDzzJwoiowRw6Rn/GnvldlchS2JQr9wFYPiwZcDfrbbujEKqKN0tvENdbjXdYhDp5Q== + +keytar@7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/keytar/-/keytar-7.2.0.tgz#4db2bec4f9700743ffd9eda22eebb658965c8440" + integrity sha512-ECSaWvoLKI5SI0pGpZQeUV1/lpBYfkaxvoSp3zkiPOz05VavwSfLi8DdEaa9N2ekQZv3Chy+o7aP6n9mairBgw== + dependencies: + node-addon-api "^3.0.0" + prebuild-install "^6.0.0" + +keyv@^4.0.0: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +klaw-sync@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/klaw-sync/-/klaw-sync-6.0.0.tgz#1fd2cfd56ebb6250181114f0a581167099c2b28c" + integrity sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ== + dependencies: + graceful-fs "^4.1.11" + +less@^3.0.3: + version "3.13.1" + resolved "https://registry.yarnpkg.com/less/-/less-3.13.1.tgz#0ebc91d2a0e9c0c6735b83d496b0ab0583077909" + integrity sha512-SwA1aQXGUvp+P5XdZslUOhhLnClSLIjWvJhmd+Vgib5BFIr9lMNlQwmwUNOjXThF/A0x+MCYYPeWEfeWiLRnTw== + dependencies: + copy-anything "^2.0.1" + tslib "^1.10.0" + optionalDependencies: + errno "^0.1.1" + graceful-fs "^4.1.2" + image-size "~0.5.0" + make-dir "^2.1.0" + mime "^1.4.1" + native-request "^1.0.5" + source-map "~0.6.0" + +limiter@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/limiter/-/limiter-2.1.0.tgz#d38d7c5b63729bb84fb0c4d8594b7e955a5182a2" + integrity sha512-361TYz6iay6n+9KvUUImqdLuFigK+K79qrUtBsXhJTLdH4rIt/r1y8r1iozwh8KbZNpujbFTSh74mJ7bwbAMOw== + dependencies: + just-performance "4.3.0" + +line-height@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/line-height/-/line-height-0.3.1.tgz#4b1205edde182872a5efa3c8f620b3187a9c54c9" + integrity sha512-YExecgqPwnp5gplD2+Y8e8A5+jKpr25+DzMbFdI1/1UAr0FJrTFv4VkHLf8/6B590i1wUPJWMKKldkd/bdQ//w== + dependencies: + computed-style "~0.1.3" + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +linkify-it@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.3.tgz#a98baf44ce45a550efb4d49c769d07524cc2fa2e" + integrity sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ== + dependencies: + uc.micro "^1.0.1" + +listenercount@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/listenercount/-/listenercount-1.0.1.tgz#84c8a72ab59c4725321480c975e6508342e70937" + integrity sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ== + +loader-runner@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== + +loader-utils@^1.0.3: + version "1.4.2" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.2.tgz#29a957f3a63973883eb684f10ffd3d151fec01a3" + integrity sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^1.0.1" + +loader-utils@^2.0.0, loader-utils@^2.0.3, loader-utils@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" + integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^2.1.2" + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ== + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + +lodash.throttle@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" + integrity sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ== + +lodash@^4.17.14: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +log-update@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" + integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== + dependencies: + ansi-escapes "^4.3.0" + cli-cursor "^3.1.0" + slice-ansi "^4.0.0" + wrap-ansi "^6.2.0" + +loose-envify@^1.1.0, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +loupe@^2.3.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.7.tgz#6e69b7d4db7d3ab436328013d37d1c8c3540c697" + integrity sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA== + dependencies: + get-func-name "^2.0.1" + +lowercase-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" + integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== + +lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + +lru-cache@^4.0.1: + version "4.1.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" + integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +lru-cache@^7.14.1, lru-cache@^7.7.1: + version "7.18.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" + integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== + +luxon@^2.4.0: + version "2.5.2" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-2.5.2.tgz#17ed497f0277e72d58a4756d6a9abee4681457b6" + integrity sha512-Yg7/RDp4nedqmLgyH0LwgGRvMEKVzKbUdkBYyCosbHgJ+kaOUx0qzSiSatVc3DFygnirTPYnMM2P5dg2uH1WvA== + +lzma-native@^8.0.5: + version "8.0.6" + resolved "https://registry.yarnpkg.com/lzma-native/-/lzma-native-8.0.6.tgz#3ea456209d643bafd9b5d911781bdf0b396b2665" + integrity sha512-09xfg67mkL2Lz20PrrDeNYZxzeW7ADtpYFbwSQh9U8+76RIzx5QsJBMy8qikv3hbUPfpy6hqwxt6FcGK81g9AA== + dependencies: + node-addon-api "^3.1.0" + node-gyp-build "^4.2.1" + readable-stream "^3.6.0" + +macaddress@^0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.5.3.tgz#2b9d6832be934cb775749f30f57d6537184a2bda" + integrity sha512-vGBKTA+jwM4KgjGZ+S/8/Mkj9rWzePyGY6jManXPGhiWu63RYwW8dKPyk5koP+8qNVhPhHgFa1y/MJ4wrjsNrg== + +make-dir@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" + integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== + dependencies: + pify "^3.0.0" + +make-dir@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" + integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== + dependencies: + pify "^4.0.1" + semver "^5.6.0" + +make-dir@^3.0.2, make-dir@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +make-fetch-happen@^10.0.3: + version "10.2.1" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz#f5e3835c5e9817b617f2770870d9492d28678164" + integrity sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w== + 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" + +map-stream@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194" + integrity sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g== + +markdown-it-anchor@~5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/markdown-it-anchor/-/markdown-it-anchor-5.0.2.tgz#cdd917a05b7bf92fb736a6dae3385c6d0d0fa552" + integrity sha512-AFM/woBI8QDJMS/9+MmsBMT5/AR+ImfOsunQZTZhzcTmna3rIzAzbOh5E0l6mlFM/i9666BpUtkqQ9bS7WApCg== + +markdown-it@^12.3.2: + version "12.3.2" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.3.2.tgz#bf92ac92283fe983fe4de8ff8abfb5ad72cd0c90" + integrity sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg== + dependencies: + argparse "^2.0.1" + entities "~2.1.0" + linkify-it "^3.0.1" + mdurl "^1.0.1" + uc.micro "^1.0.5" + +matcher@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/matcher/-/matcher-3.0.0.tgz#bd9060f4c5b70aa8041ccc6f80368760994f30ca" + integrity sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng== + dependencies: + escape-string-regexp "^4.0.0" + +mdurl@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" + integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@^2.1.18, mime-types@^2.1.27, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0, mime@^1.4.1, mime@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mime@^2.4.4: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +mimic-fn@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-3.1.0.tgz#65755145bbf3e36954b949c16450427451d5ca74" + integrity sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ== + +mimic-response@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" + integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== + +mimic-response@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" + integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== + +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== + +mini-css-extract-plugin@^2.6.1: + version "2.9.2" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz#966031b468917a5446f4c24a80854b2947503c5b" + integrity sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w== + dependencies: + schema-utils "^4.0.0" + tapable "^2.2.1" + +minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^5.0.1, minimatch@^5.1.0, minimatch@^5.1.6: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + +minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +minipass-collect@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" + integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== + dependencies: + minipass "^3.0.0" + +minipass-fetch@^2.0.3: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-2.1.2.tgz#95560b50c472d81a3bc76f20ede80eaed76d8add" + integrity sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA== + dependencies: + minipass "^3.1.6" + minipass-sized "^1.0.3" + minizlib "^2.1.2" + optionalDependencies: + encoding "^0.1.13" + +minipass-flush@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" + integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== + dependencies: + minipass "^3.0.0" + +minipass-pipeline@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" + integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== + dependencies: + minipass "^3.0.0" + +minipass-sized@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/minipass-sized/-/minipass-sized-1.0.3.tgz#70ee5a7c5052070afacfbc22977ea79def353b70" + integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== + dependencies: + minipass "^3.0.0" + +minipass@^3.0.0, minipass@^3.1.1, minipass@^3.1.6: + version "3.3.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" + integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== + dependencies: + yallist "^4.0.0" + +minipass@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" + integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== + +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + +minizlib@^2.1.1, minizlib@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mitt@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1" + integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw== + +mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + +"mkdirp@>=0.5 0", mkdirp@^0.5.1, mkdirp@^0.5.4, mkdirp@^0.5.6: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mkdirp@^1.0.3, mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +mocha@^10.1.0: + version "10.8.2" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.8.2.tgz#8d8342d016ed411b12a429eb731b825f961afb96" + integrity sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg== + dependencies: + ansi-colors "^4.1.3" + browser-stdout "^1.3.1" + chokidar "^3.5.3" + debug "^4.3.5" + diff "^5.2.0" + escape-string-regexp "^4.0.0" + find-up "^5.0.0" + glob "^8.1.0" + he "^1.2.0" + js-yaml "^4.1.0" + log-symbols "^4.1.0" + minimatch "^5.1.6" + ms "^2.1.3" + serialize-javascript "^6.0.2" + strip-json-comments "^3.1.1" + supports-color "^8.1.1" + workerpool "^6.5.1" + yargs "^16.2.0" + yargs-parser "^20.2.9" + yargs-unparser "^2.0.0" + +mount-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mount-point/-/mount-point-3.0.0.tgz#665cb9edebe80d110e658db56c31d0aef51a8f97" + integrity sha512-jAhfD7ZCG+dbESZjcY1SdFVFqSJkh/yGbdsifHcPkvuLRO5ugK0Ssmd9jdATu29BTd4JiN+vkpMzVvsUgP3SZA== + dependencies: + "@sindresorhus/df" "^1.0.1" + pify "^2.3.0" + pinkie-promise "^2.0.1" + +move-file@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/move-file/-/move-file-2.1.0.tgz#3bec9d34fbe4832df6865f112cda4492b56e8507" + integrity sha512-i9qLW6gqboJ5Ht8bauZi7KlTnQ3QFpBCvMvFfEcHADKgHGeJ9BZMO7SFCTwHPV9Qa0du9DYY1Yx3oqlGt30nXA== + dependencies: + path-exists "^4.0.0" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.3, ms@^2.0.0, ms@^2.1.1, ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +msgpackr-extract@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz#e9d87023de39ce714872f9e9504e3c1996d61012" + integrity sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA== + dependencies: + node-gyp-build-optional-packages "5.2.2" + optionalDependencies: + "@msgpackr-extract/msgpackr-extract-darwin-arm64" "3.0.3" + "@msgpackr-extract/msgpackr-extract-darwin-x64" "3.0.3" + "@msgpackr-extract/msgpackr-extract-linux-arm" "3.0.3" + "@msgpackr-extract/msgpackr-extract-linux-arm64" "3.0.3" + "@msgpackr-extract/msgpackr-extract-linux-x64" "3.0.3" + "@msgpackr-extract/msgpackr-extract-win32-x64" "3.0.3" + +msgpackr@^1.10.2: + version "1.11.2" + resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.11.2.tgz#4463b7f7d68f2e24865c395664973562ad24473d" + integrity sha512-F9UngXRlPyWCDEASDpTf6c9uNhGPTqnTeLVt7bN+bU1eajoR/8V9ys2BRaV5C/e5ihE6sJ9uPIKaYt6bFuO32g== + optionalDependencies: + msgpackr-extract "^3.0.2" + +multer@1.4.4-lts.1: + version "1.4.4-lts.1" + resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.4-lts.1.tgz#24100f701a4611211cfae94ae16ea39bb314e04d" + integrity sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg== + dependencies: + append-field "^1.0.0" + busboy "^1.0.0" + concat-stream "^1.5.2" + mkdirp "^0.5.4" + object-assign "^4.1.1" + type-is "^1.6.4" + xtend "^4.0.0" + +nan@^2.14.0, nan@^2.17.0: + version "2.22.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.22.0.tgz#31bc433fc33213c97bad36404bb68063de604de3" + integrity sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw== + +nano@^10.1.3: + version "10.1.4" + resolved "https://registry.yarnpkg.com/nano/-/nano-10.1.4.tgz#cb4cabd677733ddb81c88c1b8635101e2d84e926" + integrity sha512-bJOFIPLExIbF6mljnfExXX9Cub4W0puhDjVMp+qV40xl/DBvgKao7St4+6/GB6EoHZap7eFnrnx4mnp5KYgwJA== + dependencies: + axios "^1.7.4" + node-abort-controller "^3.1.1" + qs "^6.13.0" + +nanoid@^3.3.7: + version "3.3.8" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" + integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== + +napi-build-utils@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" + integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== + +native-keymap@^2.2.1: + version "2.5.0" + resolved "https://registry.yarnpkg.com/native-keymap/-/native-keymap-2.5.0.tgz#4d567497efb0d2efbfd53048f14093f25984c2b4" + integrity sha512-EfdMpTcX40mlHBJSWidFV4WLpwwaebK3D3JFuO/42voOAnG2WHgDdg6JerbqcxXvRhvIg934GV+9PjB3jzfu9A== + +native-request@^1.0.5: + version "1.1.2" + resolved "https://registry.yarnpkg.com/native-request/-/native-request-1.1.2.tgz#b677952757429db6cd41972a29c3b781977413ed" + integrity sha512-/etjwrK0J4Ebbcnt35VMWnfiUX/B04uwGJxyJInagxDqf2z5drSt/lsOvEMWGYunz1kaLZAFrV4NDAbOoDKvAQ== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +negotiator@^0.6.3: + version "0.6.4" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.4.tgz#777948e2452651c570b712dd01c23e262713fff7" + integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +netmask@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/netmask/-/netmask-2.0.2.tgz#8b01a07644065d536383835823bc52004ebac5e7" + integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg== + +node-abi@*, node-abi@^3.0.0: + version "3.71.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.71.0.tgz#52d84bbcd8575efb71468fbaa1f9a49b2c242038" + integrity sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw== + dependencies: + semver "^7.3.5" + +node-abi@^2.21.0, node-abi@^2.7.0: + version "2.30.1" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.30.1.tgz#c437d4b1fe0e285aaf290d45b45d4d7afedac4cf" + integrity sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w== + dependencies: + semver "^5.4.1" + +node-abort-controller@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548" + integrity sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ== + +node-addon-api@^3.0.0, node-addon-api@^3.1.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" + integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== + +node-addon-api@^7.0.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" + integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== + +node-addon-api@^8.2.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-8.3.0.tgz#ec3763f18befc1cdf66d11e157ce44d5eddc0603" + integrity sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg== + +node-api-version@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/node-api-version/-/node-api-version-0.1.4.tgz#1ed46a485e462d55d66b5aa1fe2821720dedf080" + integrity sha512-KGXihXdUChwJAOHO53bv9/vXcLmdUsZ6jIptbvYvkpKfth+r7jw44JkVxQFA3kX5nQjzjmGu1uAu/xNNLNlI5g== + dependencies: + semver "^7.3.5" + +node-gyp-build-optional-packages@5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz#522f50c2d53134d7f3a76cd7255de4ab6c96a3a4" + integrity sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw== + dependencies: + detect-libc "^2.0.1" + +node-gyp-build@^4.2.1: + version "4.8.4" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz#8a70ee85464ae52327772a90d66c6077a900cfc8" + integrity sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ== + +node-gyp@^9.0.0: + version "9.4.1" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.4.1.tgz#8a1023e0d6766ecb52764cc3a734b36ff275e185" + integrity sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ== + dependencies: + env-paths "^2.2.0" + exponential-backoff "^3.1.1" + glob "^7.1.4" + graceful-fs "^4.2.6" + make-fetch-happen "^10.0.3" + nopt "^6.0.0" + npmlog "^6.0.0" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.2" + which "^2.0.2" + +node-loader@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/node-loader/-/node-loader-2.1.0.tgz#8c4eb926e8bdcacb7349d17b40ebcc49fd2458d5" + integrity sha512-OwjPkyh8+7jW8DMd/iq71uU1Sspufr/C2+c3t0p08J3CrM9ApZ4U53xuisNrDXOHyGi5OYHgtfmmh+aK9zJA6g== + dependencies: + loader-utils "^2.0.3" + +node-pty@0.11.0-beta24: + version "0.11.0-beta24" + resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.11.0-beta24.tgz#084841017187656edaf14b459946c4a1d7cf8392" + integrity sha512-CzItw3hitX+wnpw9dHA/A+kcbV7ETNKrsyQJ+s0ZGzsu70+CSGuIGPLPfMnAc17vOrQktxjyRQfaqij75GVJFw== + dependencies: + nan "^2.17.0" + +node-releases@^2.0.18: + version "2.0.18" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" + integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== + +noop-logger@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2" + integrity sha512-6kM8CLXvuW5crTxsAtva2YLrRrDaiTIkIePWs9moLHqbFWT94WpNFjwS/5dfLfECg5i/lkmw3aoqVidxt23TEQ== + +nopt@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-6.0.0.tgz#245801d8ebf409c6df22ab9d95b65e1309cdb16d" + integrity sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g== + dependencies: + abbrev "^1.0.0" + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +normalize-url@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" + integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + integrity sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw== + dependencies: + path-key "^2.0.0" + +npm-run-path@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-3.1.0.tgz#7f91be317f6a466efed3c9f2980ad8a4ee8b0fa5" + integrity sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg== + dependencies: + path-key "^3.0.0" + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +npmlog@^4.0.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +npmlog@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.2.tgz#c8166017a42f2dea92d6453168dd865186a70830" + integrity sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg== + dependencies: + are-we-there-yet "^3.0.0" + console-control-strings "^1.1.0" + gauge "^4.0.3" + set-blocking "^2.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ== + +object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.13.1: + version "1.13.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.3.tgz#f14c183de51130243d6d18ae149375ff50ea488a" + integrity sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA== + +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +onetime@^5.1.0, onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +open@^7.4.2: + version "7.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321" + integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q== + dependencies: + is-docker "^2.0.0" + is-wsl "^2.1.1" + +opener@^1.5.1: + version "1.5.2" + resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" + integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== + +ora@^5.1.0: + version "5.4.1" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" + integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== + dependencies: + bl "^4.1.0" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-spinners "^2.5.0" + is-interactive "^1.0.0" + is-unicode-supported "^0.1.0" + log-symbols "^4.1.0" + strip-ansi "^6.0.0" + wcwidth "^1.0.1" + +os-homedir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + integrity sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ== + +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== + +p-cancelable@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf" + integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg== + +p-debounce@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-debounce/-/p-debounce-2.1.0.tgz#e79f70c6e325cbb9bddbcbec0b81025084671ad3" + integrity sha512-M9bMt62TTnozdZhqFgs+V7XD2MnuKCaz+7fZdlu2/T7xruI3uIE5CicQ0vx1hV7HIUYF0jF+4/R1AgfOkl74Qw== + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== + +p-finally@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-2.0.1.tgz#bd6fcaa9c559a096b680806f4d657b3f0f240561" + integrity sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw== + +p-limit@^2.0.0, p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2, p-limit@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + +p-queue@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-2.4.2.tgz#03609826682b743be9a22dba25051bd46724fc34" + integrity sha512-n8/y+yDJwBjoLQe1GSJbbaYQLTI7QHNZI2+rpmCDbe++WLf9HC3gf6iqj5yfPAV71W4UF3ql5W1+UBPXoXTxng== + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +pac-proxy-agent@^7.0.1: + version "7.0.2" + resolved "https://registry.yarnpkg.com/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz#0fb02496bd9fb8ae7eb11cfd98386daaac442f58" + integrity sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg== + dependencies: + "@tootallnate/quickjs-emscripten" "^0.23.0" + agent-base "^7.0.2" + debug "^4.3.4" + get-uri "^6.0.1" + http-proxy-agent "^7.0.0" + https-proxy-agent "^7.0.5" + pac-resolver "^7.0.1" + socks-proxy-agent "^8.0.4" + +pac-resolver@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/pac-resolver/-/pac-resolver-7.0.1.tgz#54675558ea368b64d210fd9c92a640b5f3b8abb6" + integrity sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg== + dependencies: + degenerator "^5.0.0" + netmask "^2.0.2" + +package-json-from-dist@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" + integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + 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" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +patch-package@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-8.0.0.tgz#d191e2f1b6e06a4624a0116bcb88edd6714ede61" + integrity sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA== + dependencies: + "@yarnpkg/lockfile" "^1.1.0" + chalk "^4.1.2" + ci-info "^3.7.0" + cross-spawn "^7.0.3" + find-yarn-workspace-root "^2.0.0" + fs-extra "^9.0.0" + json-stable-stringify "^1.0.2" + klaw-sync "^6.0.0" + minimist "^1.2.6" + open "^7.4.2" + rimraf "^2.6.3" + semver "^7.5.3" + slash "^2.0.0" + tmp "^0.0.33" + yaml "^2.2.2" + +path-browserify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" + integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw== + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-root-regex@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/path-root-regex/-/path-root-regex-0.1.2.tgz#bfccdc8df5b12dc52c8b43ec38d18d72c04ba96d" + integrity sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ== + +path-root@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/path-root/-/path-root-0.1.1.tgz#9a4a6814cac1c0cd73360a95f32083c8ea4745b7" + integrity sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg== + dependencies: + path-root-regex "^0.1.0" + +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + +path-to-regexp@0.1.10: + version "0.1.10" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" + integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== + +path-type@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" + integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== + dependencies: + pify "^3.0.0" + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +pathval@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" + integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== + +pause-stream@0.0.11: + version "0.0.11" + resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" + integrity sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A== + dependencies: + through "~2.3" + +pdfobject@^2.0.201604172: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pdfobject/-/pdfobject-2.3.0.tgz#467b4ffcd621518aa0e66fc191fdd669f3118b06" + integrity sha512-w/9pXDXTDs3IDmOri/w8lM/w6LHR0/F4fcBLLzH+4csSoyshQ5su0TE7k0FLHZO7aOjVLDGecqd1M89+PVpVAA== + +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== + +perfect-scrollbar@^1.3.0, perfect-scrollbar@^1.5.0: + version "1.5.6" + resolved "https://registry.yarnpkg.com/perfect-scrollbar/-/perfect-scrollbar-1.5.6.tgz#f1aead2588ba896435ee41b246812b2080573b7c" + integrity sha512-rixgxw3SxyJbCaSpo1n35A/fwI1r2rdwMKOTCg/AcG+xOEyZcE8UHVjpZMFCVImzsFoCZeJTT+M/rdEIQYO2nw== + +picocolors@^1.0.0, picocolors@^1.1.0, picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg== + +pify@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" + integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + +pinkie-promise@^2.0.0, pinkie-promise@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + integrity sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw== + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + integrity sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg== + +pkg-dir@^4.1.0, pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +pkg-up@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" + integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA== + dependencies: + find-up "^3.0.0" + +portfinder@^1.0.28: + version "1.0.32" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.32.tgz#2fe1b9e58389712429dc2bea5beb2146146c7f81" + integrity sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg== + dependencies: + async "^2.6.4" + debug "^3.2.7" + mkdirp "^0.5.6" + +postcss-modules-extract-imports@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz#b4497cb85a9c0c4b5aabeb759bb25e8d89f15002" + integrity sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q== + +postcss-modules-local-by-default@^4.0.5: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.1.0.tgz#b0db6bc81ffc7bdc52eb0f84d6ca0bedf0e36d21" + integrity sha512-rm0bdSv4jC3BDma3s9H19ZddW0aHX6EoqwDYU2IfZhRN+53QrufTRo2IdkAbRqLx4R2IYbZnbjKKxg4VN5oU9Q== + dependencies: + icss-utils "^5.0.0" + postcss-selector-parser "^7.0.0" + postcss-value-parser "^4.1.0" + +postcss-modules-scope@^3.2.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz#1bbccddcb398f1d7a511e0a2d1d047718af4078c" + integrity sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA== + dependencies: + postcss-selector-parser "^7.0.0" + +postcss-modules-values@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" + integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== + dependencies: + icss-utils "^5.0.0" + +postcss-selector-parser@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz#41bd8b56f177c093ca49435f65731befe25d6b9c" + integrity sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + +postcss@^8.4.33: + version "8.4.49" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.49.tgz#4ea479048ab059ab3ae61d082190fabfd994fe19" + integrity sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA== + dependencies: + nanoid "^3.3.7" + picocolors "^1.1.1" + source-map-js "^1.2.1" + +prebuild-install@^5.2.4: + version "5.3.6" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-5.3.6.tgz#7c225568d864c71d89d07f8796042733a3f54291" + integrity sha512-s8Aai8++QQGi4sSbs/M1Qku62PFK49Jm1CbgXklGz4nmHveDq0wzJkg7Na5QbnO1uNH8K7iqx2EQ/mV0MZEmOg== + dependencies: + detect-libc "^1.0.3" + expand-template "^2.0.3" + github-from-package "0.0.0" + minimist "^1.2.3" + mkdirp-classic "^0.5.3" + napi-build-utils "^1.0.1" + node-abi "^2.7.0" + noop-logger "^0.1.1" + npmlog "^4.0.1" + pump "^3.0.0" + rc "^1.2.7" + simple-get "^3.0.3" + tar-fs "^2.0.0" + tunnel-agent "^0.6.0" + which-pm-runs "^1.0.0" + +prebuild-install@^6.0.0: + version "6.1.4" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-6.1.4.tgz#ae3c0142ad611d58570b89af4986088a4937e00f" + integrity sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ== + dependencies: + detect-libc "^1.0.3" + expand-template "^2.0.3" + github-from-package "0.0.0" + minimist "^1.2.3" + mkdirp-classic "^0.5.3" + napi-build-utils "^1.0.1" + node-abi "^2.21.0" + npmlog "^4.0.1" + pump "^3.0.0" + rc "^1.2.7" + simple-get "^3.0.3" + tar-fs "^2.0.0" + tunnel-agent "^0.6.0" + +private@~0.1.5: + version "0.1.8" + resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" + integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg== + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +progress@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +prom-client@^10.2.0: + version "10.2.3" + resolved "https://registry.yarnpkg.com/prom-client/-/prom-client-10.2.3.tgz#a51bf21c239c954a6c5be4b1361fdd380218bb41" + integrity sha512-Xboq5+TdUwuQtSSDRZRNnb5NprINlgQN999VqUjZxnLKydUNLeIPx6Eiahg6oJua3XBg2TGnh5Cth1s4I6+r7g== + dependencies: + tdigest "^0.1.1" + +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g== + +promise-retry@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" + integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== + dependencies: + err-code "^2.0.2" + retry "^0.12.0" + +prop-types@^15.5.6, prop-types@^15.6.1, prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +proxy-agent@^6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/proxy-agent/-/proxy-agent-6.4.0.tgz#b4e2dd51dee2b377748aef8d45604c2d7608652d" + integrity sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ== + dependencies: + agent-base "^7.0.2" + debug "^4.3.4" + http-proxy-agent "^7.0.1" + https-proxy-agent "^7.0.3" + lru-cache "^7.14.1" + pac-proxy-agent "^7.0.1" + proxy-from-env "^1.1.0" + socks-proxy-agent "^8.0.2" + +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +prr@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" + integrity sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw== + +ps-tree@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/ps-tree/-/ps-tree-1.2.0.tgz#5e7425b89508736cdd4f2224d028f7bb3f722ebd" + integrity sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA== + dependencies: + event-stream "=3.3.4" + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + integrity sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ== + +pump@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.3.tgz#5dfe8311c33bbf6fc18261f9f34702c47c08a954" + integrity sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pump@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.2.tgz#836f3edd6bc2ee599256c924ffe0d88573ddcbf8" + integrity sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +puppeteer-core@23.1.0: + version "23.1.0" + resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-23.1.0.tgz#50703d2e27c1d73d523c25b807f6e6d95a6b1c47" + integrity sha512-SvAsu+xnLN2FMXE/59bp3s3WXp8ewqUGzVV4AQtml/2xmsciZnU/bXcCW+eETHPWQ6Agg2vTI7QzWXPpEARK2g== + dependencies: + "@puppeteer/browsers" "2.3.1" + chromium-bidi "0.6.4" + debug "^4.3.6" + devtools-protocol "0.0.1312386" + typed-query-selector "^2.12.0" + ws "^8.18.0" + +puppeteer-to-istanbul@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/puppeteer-to-istanbul/-/puppeteer-to-istanbul-1.4.0.tgz#451dced6f42652448f55cf0bc780b35512c8d1b0" + integrity sha512-dzW8u/PMqMZppvoXCFod8IkCTI2JL0yP2YUBbaALnX+iJJ6gqjk77fIoK9MqnMqRZAcoa81GLFfZExakWg/Q4Q== + dependencies: + clone "^2.1.2" + mkdirp "^1.0.4" + v8-to-istanbul "^1.2.1" + yargs "^15.3.1" + +puppeteer@23.1.0: + version "23.1.0" + resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-23.1.0.tgz#3abe4980670f214c8edfe689012e83418f81f9aa" + integrity sha512-m+CyicDlGN1AVUeOsCa6/+KQydJzxfsPowL7fQy+VGNeaWafB0m8G5aGfXdfZztKMxzCsdz7KNNzbJPeG9wwFw== + dependencies: + "@puppeteer/browsers" "2.3.1" + chromium-bidi "0.6.4" + cosmiconfig "^9.0.0" + devtools-protocol "0.0.1312386" + puppeteer-core "23.1.0" + typed-query-selector "^2.12.0" + +qs@6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== + dependencies: + side-channel "^1.0.6" + +qs@^6.13.0, qs@^6.4.0: + version "6.13.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.1.tgz#3ce5fc72bd3a8171b85c99b93c65dd20b7d1b16e" + integrity sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg== + dependencies: + side-channel "^1.0.6" + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +queue-tick@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/queue-tick/-/queue-tick-1.0.1.tgz#f6f07ac82c1fd60f82e098b417a80e52f1f4c142" + integrity sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag== + +quick-lru@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" + integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +rc@^1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +react-autosize-textarea@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/react-autosize-textarea/-/react-autosize-textarea-7.1.0.tgz#902c84fc395a689ca3a484dfb6bc2be9ba3694d1" + integrity sha512-BHpjCDkuOlllZn3nLazY2F8oYO1tS2jHnWhcjTWQdcKiiMU6gHLNt/fzmqMSyerR0eTdKtfSIqtSeTtghNwS+g== + dependencies: + autosize "^4.0.2" + line-height "^0.3.1" + prop-types "^15.5.6" + +react-dom@^18.2.0: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" + integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.2" + +react-is@^16.13.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-perfect-scrollbar@^1.5.3, react-perfect-scrollbar@^1.5.8: + version "1.5.8" + resolved "https://registry.yarnpkg.com/react-perfect-scrollbar/-/react-perfect-scrollbar-1.5.8.tgz#380959387a325c5c9d0268afc08b3f73ed5b3078" + integrity sha512-bQ46m70gp/HJtiBOF3gRzBISSZn8FFGNxznTdmTG8AAwpxG1bJCyn7shrgjEvGSQ5FJEafVEiosY+ccER11OSA== + dependencies: + perfect-scrollbar "^1.5.0" + prop-types "^15.6.1" + +react-tooltip@^4.2.21: + version "4.5.1" + resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-4.5.1.tgz#77eccccdf16adec804132e558ec20ca5783b866a" + integrity sha512-Zo+CSFUGXar1uV+bgXFFDe7VeS2iByeIp5rTgTcc2HqtuOS5D76QapejNNfx320MCY91TlhTQat36KGFTqgcvw== + dependencies: + prop-types "^15.8.1" + uuid "^7.0.3" + +react-virtuoso@^2.17.0: + version "2.19.1" + resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-2.19.1.tgz#a660a5c3cafcc7a84b59dfc356e1916e632c1e3a" + integrity sha512-zF6MAwujNGy2nJWCx/Df92ay/RnV2Kj4glUZfdyadI4suAn0kAZHB1BeI7yPFVp2iSccLzFlszhakWyr+fJ4Dw== + dependencies: + "@virtuoso.dev/react-urx" "^0.2.12" + "@virtuoso.dev/urx" "^0.2.12" + +react@^18.2.0: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" + integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== + dependencies: + loose-envify "^1.1.0" + +readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.5, readable-stream@~2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + 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" + +readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +recast@^0.11.17: + version "0.11.23" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.23.tgz#451fd3004ab1e4df9b4e4b66376b2a21912462d3" + integrity sha512-+nixG+3NugceyR8O1bLU45qs84JgI3+8EauyRZafLgC9XbdAOIVgwV1Pe2da0YzGo62KzWoZwUpVEQf6qNAXWA== + dependencies: + ast-types "0.9.6" + esprima "~3.1.0" + private "~0.1.5" + source-map "~0.5.0" + +rechoir@^0.7.0: + version "0.7.1" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.7.1.tgz#9478a96a1ca135b5e88fc027f03ee92d6c645686" + integrity sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg== + dependencies: + resolve "^1.9.0" + +reflect-metadata@^0.1.10: + version "0.1.14" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.14.tgz#24cf721fe60677146bb77eeb0e1f9dece3d65859" + integrity sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A== + +regenerate-unicode-properties@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz#626e39df8c372338ea9b8028d1f99dc3fd9c3db0" + integrity sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA== + dependencies: + regenerate "^1.4.2" + +regenerate@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" + integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== + +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + +regenerator-transform@^0.15.2: + version "0.15.2" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.2.tgz#5bbae58b522098ebdf09bca2f83838929001c7a4" + integrity sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg== + dependencies: + "@babel/runtime" "^7.8.4" + +regexpu-core@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-6.2.0.tgz#0e5190d79e542bf294955dccabae04d3c7d53826" + integrity sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA== + dependencies: + regenerate "^1.4.2" + regenerate-unicode-properties "^10.2.0" + regjsgen "^0.8.0" + regjsparser "^0.12.0" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.1.0" + +regjsgen@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.8.0.tgz#df23ff26e0c5b300a6470cad160a9d090c3a37ab" + integrity sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q== + +regjsparser@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.12.0.tgz#0e846df6c6530586429377de56e0475583b088dc" + integrity sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ== + dependencies: + jsesc "~3.0.2" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + +resolve-alpn@^1.0.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9" + integrity sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve-package-path@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/resolve-package-path/-/resolve-package-path-4.0.3.tgz#31dab6897236ea6613c72b83658d88898a9040aa" + integrity sha512-SRpNAPW4kewOaNUt8VPqhJ0UMxawMwzJD8V7m1cJfdSTK9ieZwS6K7Dabsm4bmLFM96Z5Y/UznrpG5kt1im8yA== + dependencies: + path-root "^0.1.1" + +resolve@^1.14.2, resolve@^1.9.0: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +responselike@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.1.tgz#9a0bc8fdc252f3fb1cca68b016591059ba1422bc" + integrity sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw== + dependencies: + lowercase-keys "^2.0.0" + +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@2, rimraf@^2.6.3: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +rimraf@^5.0.0: + version "5.0.10" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.10.tgz#23b9843d3dc92db71f96e1a2ce92e39fd2a8221c" + integrity sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ== + dependencies: + glob "^10.3.7" + +rimraf@~2.6.2: + version "2.6.3" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" + integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== + dependencies: + glob "^7.1.3" + +roarr@^2.15.3: + version "2.15.4" + resolved "https://registry.yarnpkg.com/roarr/-/roarr-2.15.4.tgz#f5fe795b7b838ccfe35dc608e0282b9eba2e7afd" + integrity sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A== + dependencies: + boolean "^3.0.1" + detect-node "^2.0.4" + globalthis "^1.0.1" + json-stringify-safe "^5.0.1" + semver-compare "^1.0.0" + sprintf-js "^1.1.2" + +route-parser@^0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/route-parser/-/route-parser-0.0.5.tgz#7d1d09d335e49094031ea16991a4a79b01bbe1f4" + integrity sha512-nsii+MXoNb7NyF05LP9kaktx6AoBVT/7zUgDnzIb5IoYAvYkbZOAuoLJjVdsyEVxWv0swCxWkKDK4cMva+WDBA== + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +scheduler@^0.23.2: + version "0.23.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" + integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== + dependencies: + loose-envify "^1.1.0" + +schema-utils@^2.6.5: + version "2.7.1" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" + integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== + dependencies: + "@types/json-schema" "^7.0.5" + ajv "^6.12.4" + ajv-keywords "^3.5.2" + +schema-utils@^3.0.0, schema-utils@^3.1.1, schema-utils@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +schema-utils@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.2.0.tgz#70d7c93e153a273a805801882ebd3bff20d89c8b" + integrity sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw== + dependencies: + "@types/json-schema" "^7.0.9" + ajv "^8.9.0" + ajv-formats "^2.1.1" + ajv-keywords "^5.1.0" + +secure-compare@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/secure-compare/-/secure-compare-3.0.1.tgz#f1a0329b308b221fae37b9974f3d578d0ca999e3" + integrity sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw== + +seek-bzip@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/seek-bzip/-/seek-bzip-1.0.6.tgz#35c4171f55a680916b52a07859ecf3b5857f21c4" + integrity sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ== + dependencies: + commander "^2.8.1" + +semver-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" + integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow== + +semver@^5.4.1, semver@^5.6.0: + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + +semver@^6.0.0, semver@^6.2.0, semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.3.2, semver@^7.3.5, semver@^7.5.3, semver@^7.5.4, semver@^7.6.3: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + +send@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serialize-error@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-7.0.1.tgz#f1360b0447f61ffb483ec4157c737fab7d778e18" + integrity sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw== + dependencies: + type-fest "^0.13.1" + +serialize-javascript@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4" + integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA== + dependencies: + randombytes "^2.1.0" + +serialize-javascript@^6.0.0, serialize-javascript@^6.0.1, serialize-javascript@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== + dependencies: + randombytes "^2.1.0" + +serve-static@1.16.2: + version "1.16.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" + integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== + dependencies: + encodeurl "~2.0.0" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.19.0" + +set-blocking@^2.0.0, set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +setimmediate@^1.0.5, setimmediate@~1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shell-env@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/shell-env/-/shell-env-0.3.0.tgz#2250339022989165bda4eb7bf383afeaaa92dc34" + integrity sha512-VrC6OSm5riGAFWvlYExA80Rrlfi4STsztNXjyet9Jf20hbiVeeKvJIesb92gJk7zlmpQjB0wOZpy8ClzVdPVWQ== + dependencies: + default-shell "^1.0.0" + execa "^0.5.0" + strip-ansi "^3.0.0" + +shell-path@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/shell-path/-/shell-path-2.1.0.tgz#ea7d06ae1070874a1bac5c65bb9bdd62e4f67a38" + integrity sha512-w+mbrnpA+r5jSFS4MgFfxZJ1Wx8qMKkR4gvQ+wgaZEoZCMMYZ6Yl/dcNjW/zLMfmx5a9IVIFwGAtUJcnDMmFrg== + dependencies: + shell-env "^0.3.0" + +side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +simple-concat@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" + integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== + +simple-get@^3.0.3: + version "3.1.1" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.1.tgz#cc7ba77cfbe761036fbfce3d021af25fc5584d55" + integrity sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA== + dependencies: + decompress-response "^4.2.0" + once "^1.3.1" + simple-concat "^1.0.0" + +slash@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" + integrity sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg== + +slash@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" + integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +smart-buffer@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + +socket.io-adapter@~2.5.2: + version "2.5.5" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz#c7a1f9c703d7756844751b6ff9abfc1780664082" + integrity sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg== + dependencies: + debug "~4.3.4" + ws "~8.17.1" + +socket.io-client@^4.5.3: + version "4.8.1" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.8.1.tgz#1941eca135a5490b94281d0323fe2a35f6f291cb" + integrity sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.2" + engine.io-client "~6.6.1" + socket.io-parser "~4.2.4" + +socket.io-parser@~4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" + integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + +socket.io@^4.5.3: + version "4.8.1" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.8.1.tgz#fa0eaff965cc97fdf4245e8d4794618459f7558a" + integrity sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg== + dependencies: + accepts "~1.3.4" + base64id "~2.0.0" + cors "~2.8.5" + debug "~4.3.2" + engine.io "~6.6.0" + socket.io-adapter "~2.5.2" + socket.io-parser "~4.2.4" + +socks-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-5.0.1.tgz#032fb583048a29ebffec2e6a73fca0761f48177e" + integrity sha512-vZdmnjb9a2Tz6WEQVIurybSwElwPxMZaIc7PzqbJTrezcKNznv6giT7J7tZDZ1BojVaa1jvO/UiUdhDVB0ACoQ== + dependencies: + agent-base "^6.0.2" + debug "4" + socks "^2.3.3" + +socks-proxy-agent@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz#dc069ecf34436621acb41e3efa66ca1b5fed15b6" + integrity sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww== + dependencies: + agent-base "^6.0.2" + debug "^4.3.3" + socks "^2.6.2" + +socks-proxy-agent@^8.0.2, socks-proxy-agent@^8.0.4: + version "8.0.4" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz#9071dca17af95f483300316f4b063578fa0db08c" + integrity sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw== + dependencies: + agent-base "^7.1.1" + debug "^4.3.4" + socks "^2.8.3" + +socks@^2.3.3, socks@^2.6.2, socks@^2.8.3: + version "2.8.3" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.3.tgz#1ebd0f09c52ba95a09750afe3f3f9f724a800cb5" + integrity sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw== + dependencies: + ip-address "^9.0.5" + smart-buffer "^4.2.0" + +sort-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128" + integrity sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg== + dependencies: + is-plain-obj "^1.0.0" + +source-map-js@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e" + integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug== + +source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +source-map-loader@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-2.0.2.tgz#4ab634f8f8bac4f406b61383f87a2bc804c24697" + integrity sha512-yIYkFOsKn+OdOirRJUPQpnZiMkF74raDVQjj5ni3SzbOiA57SabeX80R5zyMQAKpvKySA3Z4a85vFX3bvpC6KQ== + dependencies: + abab "^2.0.5" + iconv-lite "^0.6.2" + source-map-js "^0.6.2" + +source-map-support@^0.5.19, source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +source-map@~0.5.0: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== + +split@0.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f" + integrity sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA== + dependencies: + through "2" + +sprintf-js@^1.1.2, sprintf-js@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" + integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== + +ssri@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-9.0.1.tgz#544d4c357a8d7b71a19700074b6883fcb4eae057" + integrity sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q== + dependencies: + minipass "^3.1.1" + +stat-mode@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stat-mode/-/stat-mode-1.0.0.tgz#68b55cb61ea639ff57136f36b216a291800d1465" + integrity sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg== + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +stream-combiner@~0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14" + integrity sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw== + dependencies: + duplexer "~0.1.1" + +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + +streamx@^2.15.0, streamx@^2.20.0: + version "2.21.0" + resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.21.0.tgz#ef42a3b3fada6887ba06964443adbbbec60c5851" + integrity sha512-Qz6MsDZXJ6ur9u+b+4xCG18TluU7PGlRfXVAAjNiGsFrBUt/ioyLkxbFaKJygoPs+/kW4VyBj0bSj89Qu0IGyg== + dependencies: + fast-fifo "^1.3.2" + queue-tick "^1.0.1" + text-decoder "^1.1.0" + optionalDependencies: + bare-events "^2.2.0" + +string-argv@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.1.2.tgz#c5b7bc03fb2b11983ba3a72333dd0559e77e4738" + integrity sha512-mBqPGEOMNJKXRo7z0keX0wlAhbBAjilUdPW13nN0PecVryZxdHIeM7TqbsSUA7VYuS00HGC6mojP7DlQzfa9ZA== + +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + integrity sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw== + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg== + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + +strip-dirs@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/strip-dirs/-/strip-dirs-2.1.0.tgz#4987736264fc344cf20f6c34aca9d13d1d4ed6c5" + integrity sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g== + dependencies: + is-natural-number "^4.0.1" + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + integrity sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q== + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== + +strip-outer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-outer/-/strip-outer-1.0.1.tgz#b2fd2abf6604b9d1e6013057195df836b8a9d631" + integrity sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg== + dependencies: + escape-string-regexp "^1.0.2" + +style-loader@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-2.0.0.tgz#9669602fd4690740eaaec137799a03addbbc393c" + integrity sha512-Z0gYUJmzZ6ZdRUqpg1r8GsaFKypE+3xAzuFeMuoHgjc9KZv3wMyCRjQIWEbhoFSq7+7yoHXySDJyyWQaPajeiQ== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + +sumchecker@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-3.0.1.tgz#6377e996795abb0b6d348e9b3e1dfb24345a8e42" + integrity sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg== + dependencies: + debug "^4.1.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.0.0, supports-color@^8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +tar-fs@^1.16.2: + version "1.16.3" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-1.16.3.tgz#966a628841da2c4010406a82167cbd5e0c72d509" + integrity sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw== + dependencies: + chownr "^1.0.1" + mkdirp "^0.5.1" + pump "^1.0.0" + tar-stream "^1.1.2" + +tar-fs@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" + integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.1.4" + +tar-fs@^3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-3.0.6.tgz#eaccd3a67d5672f09ca8e8f9c3d2b89fa173f217" + integrity sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w== + dependencies: + pump "^3.0.0" + tar-stream "^3.1.5" + optionalDependencies: + bare-fs "^2.1.1" + bare-path "^2.1.0" + +tar-stream@^1.1.2, tar-stream@^1.5.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555" + integrity sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A== + dependencies: + bl "^1.0.0" + buffer-alloc "^1.2.0" + end-of-stream "^1.0.0" + fs-constants "^1.0.0" + readable-stream "^2.3.0" + to-buffer "^1.1.1" + xtend "^4.0.0" + +tar-stream@^2.1.4: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== + dependencies: + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + +tar-stream@^3.1.5: + version "3.1.7" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-3.1.7.tgz#24b3fb5eabada19fe7338ed6d26e5f7c482e792b" + integrity sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ== + dependencies: + b4a "^1.6.4" + fast-fifo "^1.2.0" + streamx "^2.15.0" + +tar@^6.0.5, tar@^6.1.11, tar@^6.1.2: + version "6.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" + integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== + 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" + +tdigest@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/tdigest/-/tdigest-0.1.2.tgz#96c64bac4ff10746b910b0e23b515794e12faced" + integrity sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA== + dependencies: + bintrees "1.0.2" + +temp@^0.9.1: + version "0.9.4" + resolved "https://registry.yarnpkg.com/temp/-/temp-0.9.4.tgz#cd20a8580cb63635d0e4e9d4bd989d44286e7620" + integrity sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA== + dependencies: + mkdirp "^0.5.1" + rimraf "~2.6.2" + +terser-webpack-plugin@^5.3.10: + version "5.3.10" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199" + integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w== + dependencies: + "@jridgewell/trace-mapping" "^0.3.20" + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.1" + terser "^5.26.0" + +terser@^5.26.0: + version "5.36.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.36.0.tgz#8b0dbed459ac40ff7b4c9fd5a3a2029de105180e" + integrity sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" + commander "^2.20.0" + source-map-support "~0.5.20" + +text-decoder@^1.1.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/text-decoder/-/text-decoder-1.2.1.tgz#e173f5121d97bfa3ff8723429ad5ba92e1ead67e" + integrity sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ== + +through@2, through@^2.3.8, through@~2.3, through@~2.3.1: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== + +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + +to-buffer@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80" + integrity sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +trash@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/trash/-/trash-7.2.0.tgz#c5ad0c9b13d7e7cad0b4187b3cfe38cd8b39abe2" + integrity sha512-3bR8Z5aWO8b9qybS6skBoaavH/hX9Onb1RrdIIhJxv9VpH3aBtpbKuAX4rIh/0xpDZ7K4ga36wONk/okbhjTlA== + dependencies: + "@stroncium/procfs" "^1.2.1" + globby "^7.1.1" + is-path-inside "^3.0.2" + make-dir "^3.1.0" + move-file "^2.0.0" + p-map "^4.0.0" + uuid "^8.3.2" + xdg-trashdir "^3.1.0" + +"traverse@>=0.3.0 <0.4": + version "0.3.9" + resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" + integrity sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ== + +trim-repeated@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-repeated/-/trim-repeated-1.0.0.tgz#e3646a2ea4e891312bf7eace6cfb05380bc01c21" + integrity sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg== + dependencies: + escape-string-regexp "^1.0.2" + +ts-md5@^1.2.2: + version "1.3.1" + resolved "https://registry.yarnpkg.com/ts-md5/-/ts-md5-1.3.1.tgz#f5b860c0d5241dd9bb4e909dd73991166403f511" + integrity sha512-DiwiXfwvcTeZ5wCE0z+2A9EseZsztaiZtGrtSaY5JOD7ekPnR/GoIVD5gXZAlK9Na9Kvpo9Waz5rW64WKAWApg== + +tslib@^1.10.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tslib@^2.0.1, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.6.2: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== + dependencies: + safe-buffer "^5.0.1" + +type-detect@^4.0.0, type-detect@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.1.0.tgz#deb2453e8f08dcae7ae98c626b13dddb0155906c" + integrity sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw== + +type-fest@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934" + integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +type-fest@^2.17.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" + integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== + +type-is@^1.6.4, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typed-query-selector@^2.12.0: + version "2.12.0" + resolved "https://registry.yarnpkg.com/typed-query-selector/-/typed-query-selector-2.12.0.tgz#92b65dbc0a42655fccf4aeb1a08b1dddce8af5f2" + integrity sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg== + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== + +typescript@~5.4.5: + version "5.4.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" + integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== + +uc.micro@^1.0.1, uc.micro@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" + integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== + +umd-compat-loader@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/umd-compat-loader/-/umd-compat-loader-2.1.2.tgz#abf89be1591940a236cf8fa87f88d6d6f5a8da35" + integrity sha512-RkTlsfrCxUISWqiTtYFFJank7b2Hhl4V2pc29nl0xOEGvvuVkpy1xnufhXfTituxgpW0HSrDk0JHlvPYZxEXKQ== + dependencies: + ast-types "^0.9.2" + loader-utils "^1.0.3" + recast "^0.11.17" + +unbzip2-stream@^1.0.9, unbzip2-stream@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" + integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== + dependencies: + buffer "^5.2.1" + through "^2.3.8" + +undici-types@~6.20.0: + version "6.20.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" + integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== + +unicode-canonical-property-names-ecmascript@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz#cb3173fe47ca743e228216e4a3ddc4c84d628cc2" + integrity sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg== + +unicode-match-property-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" + integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== + dependencies: + unicode-canonical-property-names-ecmascript "^2.0.0" + unicode-property-aliases-ecmascript "^2.0.0" + +unicode-match-property-value-ecmascript@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz#a0401aee72714598f739b68b104e4fe3a0cb3c71" + integrity sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg== + +unicode-property-aliases-ecmascript@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" + integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== + +union@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/union/-/union-0.5.0.tgz#b2c11be84f60538537b846edb9ba266ba0090075" + integrity sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA== + dependencies: + qs "^6.4.0" + +unique-filename@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-2.0.1.tgz#e785f8675a9a7589e0ac77e0b5c34d2eaeac6da2" + integrity sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A== + dependencies: + unique-slug "^3.0.0" + +unique-slug@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-3.0.0.tgz#6d347cf57c8a7a7a6044aabd0e2d74e4d76dc7c9" + integrity sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w== + dependencies: + imurmurhash "^0.1.4" + +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +unzipper@^0.9.11: + version "0.9.15" + resolved "https://registry.yarnpkg.com/unzipper/-/unzipper-0.9.15.tgz#97d99203dad17698ee39882483c14e4845c7549c" + integrity sha512-2aaUvO4RAeHDvOCuEtth7jrHFaCKTSXPqUkXwADaLBzGbgZGzUDccoEdJ5lW+3RmfpOZYNx0Rw6F6PUzM6caIA== + dependencies: + big-integer "^1.6.17" + binary "~0.3.0" + bluebird "~3.4.1" + buffer-indexof-polyfill "~1.0.0" + duplexer2 "~0.1.4" + fstream "^1.0.12" + listenercount "~1.0.1" + readable-stream "~2.3.6" + setimmediate "~1.0.4" + +update-browserslist-db@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz#80846fba1d79e82547fb661f8d141e0945755fe5" + integrity sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +url-join@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7" + integrity sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA== + +urlpattern-polyfill@10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz#f0a03a97bfb03cdf33553e5e79a2aadd22cac8ec" + integrity sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg== + +user-home@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f" + integrity sha512-KMWqdlOcjCYdtIJpicDSFBQ8nFwS2i9sslAd6f4+CBGcU4gist2REnr2fxj2YocvJFxSF3ZOHLYLVZnUxv4BZQ== + dependencies: + os-homedir "^1.0.0" + +util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b" + integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg== + +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +uuid@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== + +v8-compile-cache@^2.2.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz#cdada8bec61e15865f05d097c5f4fd30e94dc128" + integrity sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw== + +v8-to-istanbul@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-1.2.1.tgz#8f63a94b7f91243f5dcc6a495540b5653beb1279" + integrity sha512-NglPycIwSQeSJj7VJ6L8vTsPKC9MG5Lcx4n3SvYqNHzklbMI4dGcLJnkLPEPJ3uB8UyTdWviMhM0Ptq+xD5UFQ== + +valid-filename@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/valid-filename/-/valid-filename-2.0.1.tgz#0768d6f364b1ed3bdf68f0d15abffb0d9d6cecaf" + integrity sha512-7eF/iUZ5SPd3FighoKgatSjXDJ25Vopo/6yvEKGyX4FIeZVHcLjHmyvbQ1WdFD9RQZ9PoBA7nrSxxAz/oC64SQ== + dependencies: + filename-reserved-regex "^2.0.0" + +vary@^1, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +vhost@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/vhost/-/vhost-3.0.2.tgz#2fb1decd4c466aa88b0f9341af33dc1aff2478d5" + integrity sha512-S3pJdWrpFWrKMboRU4dLYgMrTgoPALsmYwOvyebK2M6X95b9kQrjZy5rwl3uzzpfpENe/XrNYu/2U+e7/bmT5g== + +vscode-jsonrpc@8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9" + integrity sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA== + +vscode-languageserver-protocol@^3.17.2: + version "3.17.5" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz#864a8b8f390835572f4e13bd9f8313d0e3ac4bea" + integrity sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg== + dependencies: + vscode-jsonrpc "8.2.0" + vscode-languageserver-types "3.17.5" + +vscode-languageserver-textdocument@^1.0.1: + version "1.0.12" + resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz#457ee04271ab38998a093c68c2342f53f6e4a631" + integrity sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA== + +vscode-languageserver-types@3.17.5: + version "3.17.5" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a" + integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== + +vscode-oniguruma@1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.6.1.tgz#2bf4dfcfe3dd2e56eb549a3068c8ee39e6c30ce5" + integrity sha512-vc4WhSIaVpgJ0jJIejjYxPvURJavX6QG41vu0mGhqywMkQqulezEqEQ3cO3gc8GvcOpX6ycmKGqRoROEMBNXTQ== + +vscode-textmate@^9.0.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-9.1.0.tgz#656a6aa163a9578397ba810733952bedb2b47202" + integrity sha512-lxKSVp2DkFOx9RDAvpiYUrB9/KT1fAfi1aE8CBGstP8N7rLF+Seifj8kDA198X0mYj1CjQUC+81+nQf8CO0nVA== + +vscode-uri@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-2.1.2.tgz#c8d40de93eb57af31f3c715dd650e2ca2c096f1c" + integrity sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A== + +watchpack@^2.4.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.2.tgz#2feeaed67412e7c33184e5a79ca738fbd38564da" + integrity sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +wcwidth@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" + integrity sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg== + dependencies: + defaults "^1.0.3" + +webpack-cli@4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.7.0.tgz#3195a777f1f802ecda732f6c95d24c0004bc5a35" + integrity sha512-7bKr9182/sGfjFm+xdZSwgQuFjgEcy0iCTIBxRUeteJ2Kr8/Wz0qNJX+jw60LU36jApt4nmMkep6+W5AKhok6g== + dependencies: + "@discoveryjs/json-ext" "^0.5.0" + "@webpack-cli/configtest" "^1.0.3" + "@webpack-cli/info" "^1.2.4" + "@webpack-cli/serve" "^1.4.0" + colorette "^1.2.1" + commander "^7.0.0" + execa "^5.0.0" + fastest-levenshtein "^1.0.12" + import-local "^3.0.2" + interpret "^2.2.0" + rechoir "^0.7.0" + v8-compile-cache "^2.2.0" + webpack-merge "^5.7.3" + +webpack-merge@^5.7.3: + version "5.10.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.10.0.tgz#a3ad5d773241e9c682803abf628d4cd62b8a4177" + integrity sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA== + dependencies: + clone-deep "^4.0.1" + flat "^5.0.2" + wildcard "^2.0.0" + +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack@^5.76.0: + version "5.97.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.97.0.tgz#1c5e3b9319f8c6decb19b142e776d90e629d5c40" + integrity sha512-CWT8v7ShSfj7tGs4TLRtaOLmOCPWhoKEvp+eA7FVx8Xrjb3XfT0aXdxDItnRZmE8sHcH+a8ayDrJCOjXKxVFfQ== + dependencies: + "@types/eslint-scope" "^3.7.7" + "@types/estree" "^1.0.6" + "@webassemblyjs/ast" "^1.14.1" + "@webassemblyjs/wasm-edit" "^1.14.1" + "@webassemblyjs/wasm-parser" "^1.14.1" + acorn "^8.14.0" + browserslist "^4.24.0" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.17.1" + es-module-lexer "^1.2.1" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.11" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.2.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.3.10" + watchpack "^2.4.1" + webpack-sources "^3.2.3" + +whatwg-encoding@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53" + integrity sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg== + dependencies: + iconv-lite "0.6.3" + +which-module@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" + integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== + +which-pm-runs@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.1.0.tgz#35ccf7b1a0fce87bd8b92a478c9d045785d3bf35" + integrity sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA== + +which@^1.2.9: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1, which@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wide-align@^1.1.0, wide-align@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" + integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== + dependencies: + string-width "^1.0.2 || 2 || 3 || 4" + +wildcard@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" + integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== + +worker-loader@^3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-3.0.8.tgz#5fc5cda4a3d3163d9c274a4e3a811ce8b60dbb37" + integrity sha512-XQyQkIFeRVC7f7uRhFdNMe/iJOdO6zxAaR3EWbDp45v3mDhrTi+++oswKNxShUNjPC/1xUp5DB29YKLhFo129g== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + +workerpool@^6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" + integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +write-file-atomic@^2.0.0: + version "2.4.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.3.tgz#1fd2e9ae1df3e75b8d8c367443c692d4ca81f481" + integrity sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ== + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + signal-exit "^3.0.2" + +write-json-file@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/write-json-file/-/write-json-file-2.3.0.tgz#2b64c8a33004d54b8698c76d585a77ceb61da32f" + integrity sha512-84+F0igFp2dPD6UpAQjOUX3CdKUOqUzn6oE9sDBNzUXINR5VceJ1rauZltqQB/bcYsx3EpKys4C7/PivKUAiWQ== + dependencies: + detect-indent "^5.0.0" + graceful-fs "^4.1.2" + make-dir "^1.0.0" + pify "^3.0.0" + sort-keys "^2.0.0" + write-file-atomic "^2.0.0" + +ws@^8.17.1, ws@^8.18.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + +ws@~8.17.1: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== + +xdg-basedir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" + integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== + +xdg-trashdir@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/xdg-trashdir/-/xdg-trashdir-3.1.0.tgz#7294262d5793eb5488c2f529fba883ec32a24ea0" + integrity sha512-N1XQngeqMBoj9wM4ZFadVV2MymImeiFfYD+fJrNlcVcOHsJFFQe7n3b+aBoTPwARuq2HQxukfzVpQmAk1gN4sQ== + dependencies: + "@sindresorhus/df" "^3.1.1" + mount-point "^3.0.0" + user-home "^2.0.0" + xdg-basedir "^4.0.0" + +xmlhttprequest-ssl@~2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz#e9e8023b3f29ef34b97a859f584c5e6c61418e23" + integrity sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ== + +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +xterm-addon-fit@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz#2d51b983b786a97dcd6cde805e700c7f913bc596" + integrity sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ== + +xterm-addon-fit@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.8.0.tgz#48ca99015385141918f955ca7819e85f3691d35f" + integrity sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw== + +xterm-addon-search@^0.13.0: + version "0.13.0" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.13.0.tgz#21286f4db48aa949fbefce34bb8bc0c9d3cec627" + integrity sha512-sDUwG4CnqxUjSEFh676DlS3gsh3XYCzAvBPSvJ5OPgF3MRL3iHLPfsb06doRicLC2xXNpeG2cWk8x1qpESWJMA== + +xterm@^4.16.0: + version "4.19.0" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0.tgz#c0f9d09cd61de1d658f43ca75f992197add9ef6d" + integrity sha512-c3Cp4eOVsYY5Q839dR5IejghRPpxciGmLWWaP9g+ppfMeBChMeLa1DCA+pmX/jyDZ+zxFOmlJL/82qVdayVoGQ== + +xterm@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.3.0.tgz#867daf9cc826f3d45b5377320aabd996cb0fce46" + integrity sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg== + +y18n@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" + integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + integrity sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@^2.2.2: + version "2.6.1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.6.1.tgz#42f2b1ba89203f374609572d5349fb8686500773" + integrity sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg== + +yargs-parser@^18.1.2: + version "18.1.3" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" + integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-parser@^20.2.2, yargs-parser@^20.2.9: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs-unparser@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" + integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== + dependencies: + camelcase "^6.0.0" + decamelize "^4.0.0" + flat "^5.0.2" + is-plain-obj "^2.1.0" + +yargs@^15.3.1: + version "15.4.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" + integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== + dependencies: + cliui "^6.0.0" + decamelize "^1.2.0" + find-up "^4.1.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^4.2.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^18.1.2" + +yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yargs@^17.0.1, yargs@^17.7.2: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + 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" + +yauzl@^2.10.0, yauzl@^2.4.2, yauzl@^2.9.2: + version "2.10.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zod@3.23.8: + version "3.23.8" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" + integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== diff --git a/demo/dockerfiles/demo-theia-monitor-vscode/.project b/demo/dockerfiles/demo-theia-monitor-vscode/.project deleted file mode 100644 index 655e42dfc..000000000 --- a/demo/dockerfiles/demo-theia-monitor-vscode/.project +++ /dev/null @@ -1,11 +0,0 @@ - - - demo-theia-docker - - - - - - - - diff --git a/demo/dockerfiles/demo-theia-monitor-vscode/Dockerfile b/demo/dockerfiles/demo-theia-monitor-vscode/Dockerfile index e30ca90a9..3e1267d6d 100644 --- a/demo/dockerfiles/demo-theia-monitor-vscode/Dockerfile +++ b/demo/dockerfiles/demo-theia-monitor-vscode/Dockerfile @@ -1,10 +1,10 @@ -FROM node:18-bullseye-slim as build-stage +FROM node:18-bullseye-slim AS build-stage # Copy and unzip the vsix file RUN apt-get update && apt-get install -y unzip && rm -rf /var/lib/apt/lists/* -COPY theiacloud-monitor-0.12.0-next.vsix /tmp/theiacloud-monitor.vsix -RUN mkdir /tmp/extracted && unzip /tmp/theiacloud-monitor.vsix -d /tmp/extracted +COPY theia-cloud-monitor-1.2.0-next.vsix /tmp/theia-cloud-monitor.vsix +RUN mkdir /tmp/extracted && unzip /tmp/theia-cloud-monitor.vsix -d /tmp/extracted -FROM theiacloud/theia-cloud-demo:0.12.0-next as production-stage +FROM theiacloud/theia-cloud-demo:1.2.0-next AS production-stage -COPY --chown=theia:theia --from=build-stage /tmp/extracted /home/theia/plugins \ No newline at end of file +COPY --chown=theia:theia --from=build-stage /tmp/extracted /home/theia/plugins diff --git a/demo/dockerfiles/demo-theia-monitor-vscode/package.json b/demo/dockerfiles/demo-theia-monitor-vscode/package.json deleted file mode 100644 index 920515cbf..000000000 --- a/demo/dockerfiles/demo-theia-monitor-vscode/package.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "private": true, - "theia": { - "frontend": { - "config": { - "applicationName": "Theia Cloud Example", - "warnOnPotentiallyInsecureHostPattern": false, - "preferences": { - "files.enableTrash": false - } - } - }, - "backend": { - "config": { - "warnOnPotentiallyInsecureHostPattern": false - } - } - }, - "dependencies": { - "@theia/bulk-edit": "1.43.1", - "@theia/callhierarchy": "1.43.1", - "@theia/console": "1.43.1", - "@theia/core": "1.43.1", - "@theia/debug": "1.43.1", - "@theia/editor": "1.43.1", - "@theia/editor-preview": "1.43.1", - "@theia/electron": "1.43.1", - "@theia/external-terminal": "1.43.1", - "@theia/file-search": "1.43.1", - "@theia/filesystem": "1.43.1", - "@theia/getting-started": "1.43.1", - "@theia/keymaps": "1.43.1", - "@theia/markers": "1.43.1", - "@theia/messages": "1.43.1", - "@theia/metrics": "1.43.1", - "@theia/mini-browser": "1.43.1", - "@theia/monaco": "1.43.1", - "@theia/navigator": "1.43.1", - "@theia/outline-view": "1.43.1", - "@theia/output": "1.43.1", - "@theia/plugin-dev": "1.43.1", - "@theia/plugin-ext": "1.43.1", - "@theia/plugin-ext-vscode": "1.43.1", - "@theia/preferences": "1.43.1", - "@theia/process": "1.43.1", - "@theia/property-view": "1.43.1", - "@theia/scm": "1.43.1", - "@theia/scm-extra": "1.43.1", - "@theia/search-in-workspace": "1.43.1", - "@theia/task": "1.43.1", - "@theia/terminal": "1.43.1", - "@theia/timeline": "1.43.1", - "@theia/toolbar": "1.43.1", - "@theia/typehierarchy": "1.43.1", - "@theia/userstorage": "1.43.1", - "@theia/variable-resolver": "1.43.1", - "@theia/vsx-registry": "1.43.1", - "@theia/workspace": "1.43.1" - }, - "devDependencies": { - "@theia/cli": "1.43.1" - }, - "scripts": { - "preinstall": "node-gyp install" - }, - "theiaPluginsDir": "plugins", - "theiaPlugins": { - "vscode.git": "https://open-vsx.org/api/vscode/git/1.52.1/file/vscode.git-1.52.1.vsix", - "vscode.markdown-language-features": "https://open-vsx.org/api/vscode/markdown-language-features/1.39.2/file/vscode.markdown-language-features-1.39.2.vsix", - "vscode-builtin-extensions-pack": "https://open-vsx.org/api/eclipse-theia/builtin-extension-pack/1.50.1/file/eclipse-theia.builtin-extension-pack-1.50.1.vsix", - "redhat.java": "https://open-vsx.org/api/redhat/java/0.73.0/file/redhat.java-0.73.0.vsix", - "vscjava.vscode-java-debug": "https://open-vsx.org/api/vscjava/vscode-java-debug/0.30.0/file/vscjava.vscode-java-debug-0.30.0.vsix", - "vscjava.vscode-java-test": "https://open-vsx.org/api/vscjava/vscode-java-test/0.26.1/file/vscjava.vscode-java-test-0.26.1.vsix", - "vscjava.vscode-maven": "https://open-vsx.org/api/vscjava/vscode-maven/0.21.2/file/vscjava.vscode-maven-0.21.2.vsix", - "vscjava.vscode-java-dependency": "https://open-vsx.org/api/vscjava/vscode-java-dependency/0.16.0/file/vscjava.vscode-java-dependency-0.16.0.vsix", - "redhat.vscode-xml": "https://open-vsx.org/api/redhat/vscode-xml/0.20.0/file/redhat.vscode-xml-0.20.0.vsix", - "cdt-gdb-vscode": "https://open-vsx.org/api/eclipse-cdt/cdt-gdb-vscode/0.0.92/file/eclipse-cdt.cdt-gdb-vscode-0.0.92.vsix", - "vscode-builtin-cpp": "https://open-vsx.org/api/vscode/cpp/1.62.3/file/vscode.cpp-1.62.3.vsix", - "vscode-clangd": "https://open-vsx.org/api/llvm-vs-code-extensions/vscode-clangd/0.1.15/file/llvm-vs-code-extensions.vscode-clangd-0.1.15.vsix", - "vscode-cmake": "https://open-vsx.org/api/ms-vscode/cmake-tools/1.9.2/file/ms-vscode.cmake-tools-1.9.2.vsix" - }, - "theiaPluginsExcludeIds": [ - "vscode.extension-editing", - "vscode.microsoft-authentication", - "vscode.html-language-features" - ] -} diff --git a/demo/dockerfiles/demo-theia-monitor-vscode/project/.vscode/launch.json b/demo/dockerfiles/demo-theia-monitor-vscode/project/.vscode/launch.json deleted file mode 100644 index a2ea02c46..000000000 --- a/demo/dockerfiles/demo-theia-monitor-vscode/project/.vscode/launch.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - "version": "0.2.0", - "configurations": [] -} diff --git a/demo/dockerfiles/demo-theia-monitor-vscode/project/.vscode/settings.json b/demo/dockerfiles/demo-theia-monitor-vscode/project/.vscode/settings.json deleted file mode 100644 index 8aa63df4a..000000000 --- a/demo/dockerfiles/demo-theia-monitor-vscode/project/.vscode/settings.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "files.exclude": { - "**/.classpath": true, - "**/.project": true, - "**/.settings": true, - "**/.factorypath": true - }, - "java.configuration.updateBuildConfiguration": "automatic", - "xml.downloadExternalResources.enabled": true, - "cmake.sourceDirectory": "${workspaceFolder}/cpp", - "cmake.buildDirectory": "${workspaceFolder}/cpp/build", - "java.project.importOnFirstTimeStartup": "automatic", - "cmake.configureOnOpen": true -} diff --git a/demo/dockerfiles/demo-theia-monitor-vscode/project/README.md b/demo/dockerfiles/demo-theia-monitor-vscode/project/README.md deleted file mode 100644 index 295def8ad..000000000 --- a/demo/dockerfiles/demo-theia-monitor-vscode/project/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Theia Examples - -This workspace contains some sample projects for Java-, C++-, and Web-Development. - -## Web - -The `web` directory contains a simple HTML+CSS+Javascript+Typescript website. You may build this by running `npm install && npm run build` in the `web` directory. The build results are present in the `web/bin` directory. You may open the `web/bin/index.html` in Theia's Preview-Browser via right clicking the `index.html` -> `Open With...` -> `Preview` - -## Java - -The `java` directory contains a simple Java Problem. Opening a Java file activates the Java support for this project. After activation you may Run/Debug the main method/test cases. - -## XML, JSON - -The `files` directory contains sample XML and JSON files with schema-files. diff --git a/demo/dockerfiles/demo-theia-monitor-vscode/project/files/template.json b/demo/dockerfiles/demo-theia-monitor-vscode/project/files/template.json deleted file mode 100644 index 189377147..000000000 --- a/demo/dockerfiles/demo-theia-monitor-vscode/project/files/template.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "$schema": "./template.schema.json", - "name": "theia-cloud.demo", - "image": "my.docker.registry/theia-cloud-demo", - "limits": { - "cpu": "1", - "memory": "2000M" - } -} diff --git a/demo/dockerfiles/demo-theia-monitor-vscode/project/files/template.schema.json b/demo/dockerfiles/demo-theia-monitor-vscode/project/files/template.schema.json deleted file mode 100644 index 5aa28de35..000000000 --- a/demo/dockerfiles/demo-theia-monitor-vscode/project/files/template.schema.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "image": { - "type": "string" - }, - "limits": { - "type": "object", - "properties": { - "cpu": { - "type": "string" - }, - "memory": { - "type": "string" - } - }, - "required": ["cpu", "memory"] - } - }, - "required": ["name", "image", "limits"] -} diff --git a/demo/dockerfiles/demo-theia-monitor-vscode/project/files/template.xml b/demo/dockerfiles/demo-theia-monitor-vscode/project/files/template.xml deleted file mode 100644 index 4dff323e4..000000000 --- a/demo/dockerfiles/demo-theia-monitor-vscode/project/files/template.xml +++ /dev/null @@ -1,9 +0,0 @@ - - \ No newline at end of file diff --git a/demo/dockerfiles/demo-theia-monitor-vscode/project/files/template.xsd b/demo/dockerfiles/demo-theia-monitor-vscode/project/files/template.xsd deleted file mode 100644 index 16f58acad..000000000 --- a/demo/dockerfiles/demo-theia-monitor-vscode/project/files/template.xsd +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/demo/dockerfiles/demo-theia-monitor-vscode/project/java/.classpath b/demo/dockerfiles/demo-theia-monitor-vscode/project/java/.classpath deleted file mode 100644 index 9ba41a249..000000000 --- a/demo/dockerfiles/demo-theia-monitor-vscode/project/java/.classpath +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/demo/dockerfiles/demo-theia-monitor-vscode/project/java/.gitignore b/demo/dockerfiles/demo-theia-monitor-vscode/project/java/.gitignore deleted file mode 100644 index 1de565933..000000000 --- a/demo/dockerfiles/demo-theia-monitor-vscode/project/java/.gitignore +++ /dev/null @@ -1 +0,0 @@ -target \ No newline at end of file diff --git a/demo/dockerfiles/demo-theia-monitor-vscode/project/java/.project b/demo/dockerfiles/demo-theia-monitor-vscode/project/java/.project deleted file mode 100644 index bf3a5a7b7..000000000 --- a/demo/dockerfiles/demo-theia-monitor-vscode/project/java/.project +++ /dev/null @@ -1,34 +0,0 @@ - - - demo - - - - - - org.eclipse.jdt.core.javabuilder - - - - - org.eclipse.m2e.core.maven2Builder - - - - - - org.eclipse.jdt.core.javanature - org.eclipse.m2e.core.maven2Nature - - - - 1648557854974 - - 30 - - org.eclipse.core.resources.regexFilterMatcher - node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ - - - - diff --git a/demo/dockerfiles/demo-theia-monitor-vscode/project/java/.settings/org.eclipse.core.resources.prefs b/demo/dockerfiles/demo-theia-monitor-vscode/project/java/.settings/org.eclipse.core.resources.prefs deleted file mode 100644 index f9fe34593..000000000 --- a/demo/dockerfiles/demo-theia-monitor-vscode/project/java/.settings/org.eclipse.core.resources.prefs +++ /dev/null @@ -1,4 +0,0 @@ -eclipse.preferences.version=1 -encoding//src/main/java=UTF-8 -encoding//src/test/java=UTF-8 -encoding/=UTF-8 diff --git a/demo/dockerfiles/demo-theia-monitor-vscode/project/java/.settings/org.eclipse.jdt.apt.core.prefs b/demo/dockerfiles/demo-theia-monitor-vscode/project/java/.settings/org.eclipse.jdt.apt.core.prefs deleted file mode 100644 index d4313d4b2..000000000 --- a/demo/dockerfiles/demo-theia-monitor-vscode/project/java/.settings/org.eclipse.jdt.apt.core.prefs +++ /dev/null @@ -1,2 +0,0 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.apt.aptEnabled=false diff --git a/demo/dockerfiles/demo-theia-monitor-vscode/project/java/.settings/org.eclipse.jdt.core.prefs b/demo/dockerfiles/demo-theia-monitor-vscode/project/java/.settings/org.eclipse.jdt.core.prefs deleted file mode 100644 index 46235dc07..000000000 --- a/demo/dockerfiles/demo-theia-monitor-vscode/project/java/.settings/org.eclipse.jdt.core.prefs +++ /dev/null @@ -1,9 +0,0 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 -org.eclipse.jdt.core.compiler.compliance=11 -org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled -org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning -org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore -org.eclipse.jdt.core.compiler.processAnnotations=disabled -org.eclipse.jdt.core.compiler.release=disabled -org.eclipse.jdt.core.compiler.source=11 diff --git a/demo/dockerfiles/demo-theia-monitor-vscode/project/java/.settings/org.eclipse.m2e.core.prefs b/demo/dockerfiles/demo-theia-monitor-vscode/project/java/.settings/org.eclipse.m2e.core.prefs deleted file mode 100644 index f897a7f1c..000000000 --- a/demo/dockerfiles/demo-theia-monitor-vscode/project/java/.settings/org.eclipse.m2e.core.prefs +++ /dev/null @@ -1,4 +0,0 @@ -activeProfiles= -eclipse.preferences.version=1 -resolveWorkspaceProjects=true -version=1 diff --git a/demo/dockerfiles/demo-theia-monitor-vscode/project/java/pom.xml b/demo/dockerfiles/demo-theia-monitor-vscode/project/java/pom.xml deleted file mode 100644 index c4ff1c434..000000000 --- a/demo/dockerfiles/demo-theia-monitor-vscode/project/java/pom.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - 4.0.0 - - com.example - demo - 1.0-SNAPSHOT - - demo - - - UTF-8 - 11 - 11 - - - - - junit - junit - 4.11 - test - - - \ No newline at end of file diff --git a/demo/dockerfiles/demo-theia-monitor-vscode/project/java/src/main/java/com/example/App.java b/demo/dockerfiles/demo-theia-monitor-vscode/project/java/src/main/java/com/example/App.java deleted file mode 100644 index ccc602e25..000000000 --- a/demo/dockerfiles/demo-theia-monitor-vscode/project/java/src/main/java/com/example/App.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example; - -public class App { - - private String name; - - public App(String name) { - this.name = name; - } - - public String getName() { - return name; - } - - public static void main(String[] args) { - App app = new App("Hello World!"); - System.out.println(app.getName()); - } -} diff --git a/demo/dockerfiles/demo-theia-monitor-vscode/project/java/src/test/java/com/example/AppTest.java b/demo/dockerfiles/demo-theia-monitor-vscode/project/java/src/test/java/com/example/AppTest.java deleted file mode 100644 index bca4bf744..000000000 --- a/demo/dockerfiles/demo-theia-monitor-vscode/project/java/src/test/java/com/example/AppTest.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example; - -import static org.junit.Assert.assertEquals; - -import org.junit.Test; - -public class AppTest { - - @Test - public void shouldAnswerWithTrue() { - String name = "foo"; - App app = new App(name); - assertEquals(name, app.getName()); - } -} diff --git a/demo/dockerfiles/demo-theia-monitor-vscode/project/web/README.md b/demo/dockerfiles/demo-theia-monitor-vscode/project/web/README.md deleted file mode 100644 index 92bda5246..000000000 --- a/demo/dockerfiles/demo-theia-monitor-vscode/project/web/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Web Example - -## Building - -Run `npm run build` in the `web` directory - -## Running - -Open `bin/index.html` and select `Open Preview to the Side` from the editor's toolbar diff --git a/demo/dockerfiles/demo-theia-monitor-vscode/project/web/package-lock.json b/demo/dockerfiles/demo-theia-monitor-vscode/project/web/package-lock.json deleted file mode 100644 index 2f218557a..000000000 --- a/demo/dockerfiles/demo-theia-monitor-vscode/project/web/package-lock.json +++ /dev/null @@ -1,469 +0,0 @@ -{ - "name": "web-example", - "version": "0.0.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "web-example", - "version": "0.0.1", - "devDependencies": { - "copyfiles": "^2.4.1", - "rimraf": "^3.0.2", - "typescript": "^5.3.3" - } - }, - "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-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/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "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/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "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/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/copyfiles": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz", - "integrity": "sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==", - "dev": true, - "dependencies": { - "glob": "^7.0.5", - "minimatch": "^3.0.3", - "mkdirp": "^1.0.4", - "noms": "0.0.0", - "through2": "^2.0.1", - "untildify": "^4.0.0", - "yargs": "^16.1.0" - }, - "bin": { - "copyfiles": "copyfiles", - "copyup": "copyfiles" - } - }, - "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/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/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/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "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/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "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/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", - "dev": true - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/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/noms": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz", - "integrity": "sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==", - "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "readable-stream": "~1.0.31" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/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/readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "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/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/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/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", - "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/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/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "node_modules/through2/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/through2/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/through2/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/typescript": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", - "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/untildify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", - "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "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/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, - "engines": { - "node": ">=0.4" - } - }, - "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/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "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" - } - } - } -} diff --git a/demo/dockerfiles/demo-theia-monitor-vscode/project/web/package.json b/demo/dockerfiles/demo-theia-monitor-vscode/project/web/package.json deleted file mode 100644 index b51adf165..000000000 --- a/demo/dockerfiles/demo-theia-monitor-vscode/project/web/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "web-example", - "version": "0.0.1", - "scripts": { - "build": "rimraf bin && tsc && copyfiles -u 1 \"src/**/*.js\" \"src/**/*.css\" \"src/**/*.html\" bin" - }, - "devDependencies": { - "copyfiles": "^2.4.1", - "rimraf": "^3.0.2", - "typescript": "^5.3.3" - } -} diff --git a/demo/dockerfiles/demo-theia-monitor-vscode/project/web/src/contentgenerator.ts b/demo/dockerfiles/demo-theia-monitor-vscode/project/web/src/contentgenerator.ts deleted file mode 100644 index 87c297600..000000000 --- a/demo/dockerfiles/demo-theia-monitor-vscode/project/web/src/contentgenerator.ts +++ /dev/null @@ -1,23 +0,0 @@ -interface Config { - name: string; - time: Date; -} - -function generateContents(config: Config) { - return ( - "Hello " + - config.name + - "! It's " + - config.time.getHours() + - " o'clock and " + - config.time.getMinutes() + - " minutes." - ); -} - -var config: Config = { - name: "Sasha", - time: new Date(), -}; - -document.body.innerHTML = generateContents(config); diff --git a/demo/dockerfiles/demo-theia-monitor-vscode/project/web/src/index.html b/demo/dockerfiles/demo-theia-monitor-vscode/project/web/src/index.html deleted file mode 100644 index ea996bdde..000000000 --- a/demo/dockerfiles/demo-theia-monitor-vscode/project/web/src/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - Theia Cloud - - - - - -
- diff --git a/demo/dockerfiles/demo-theia-monitor-vscode/project/web/src/styles.css b/demo/dockerfiles/demo-theia-monitor-vscode/project/web/src/styles.css deleted file mode 100644 index 1ffaa593f..000000000 --- a/demo/dockerfiles/demo-theia-monitor-vscode/project/web/src/styles.css +++ /dev/null @@ -1,16 +0,0 @@ -body { - background: rgb(255, 218, 218); - background: linear-gradient( - 90deg, - rgba(255, 218, 218, 1) 0%, - rgba(255, 218, 218, 1) 3%, - rgba(215, 248, 255, 1) 100% - ); -} - -html { - font-family: Arial, Helvetica, sans-serif; - text-align: center; - font-size: large; - color: darkslategray; -} diff --git a/demo/dockerfiles/demo-theia-monitor-vscode/project/web/src/updatecontents.js b/demo/dockerfiles/demo-theia-monitor-vscode/project/web/src/updatecontents.js deleted file mode 100644 index 788176406..000000000 --- a/demo/dockerfiles/demo-theia-monitor-vscode/project/web/src/updatecontents.js +++ /dev/null @@ -1,3 +0,0 @@ -setTimeout(function () { - document.body.innerHTML = "Bye bye!"; -}, 10000); diff --git a/demo/dockerfiles/demo-theia-monitor-vscode/project/web/tsconfig.json b/demo/dockerfiles/demo-theia-monitor-vscode/project/web/tsconfig.json deleted file mode 100644 index f181a4d22..000000000 --- a/demo/dockerfiles/demo-theia-monitor-vscode/project/web/tsconfig.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "compilerOptions": { - "skipLibCheck": true, - "declaration": true, - "declarationMap": true, - "noImplicitAny": true, - "noEmitOnError": false, - "noImplicitThis": true, - "noUnusedLocals": true, - "strictNullChecks": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "downlevelIteration": true, - "resolveJsonModule": true, - "module": "commonjs", - "outDir": "bin", - "moduleResolution": "node", - "target": "ES2017", - "jsx": "react", - "lib": ["ES2017", "dom"], - "sourceMap": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "**/*.spec.ts"] -} diff --git a/demo/dockerfiles/demo-theia-monitor-vscode/settings.json b/demo/dockerfiles/demo-theia-monitor-vscode/settings.json deleted file mode 100644 index 24cd36cc8..000000000 --- a/demo/dockerfiles/demo-theia-monitor-vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "java.project.importOnFirstTimeStartup": "automatic" -} diff --git a/demo/dockerfiles/demo-theia-monitor-vscode/theia-cloud-monitor-1.1.0-next.vsix b/demo/dockerfiles/demo-theia-monitor-vscode/theia-cloud-monitor-1.1.0-next.vsix new file mode 100644 index 000000000..80f934a77 Binary files /dev/null and b/demo/dockerfiles/demo-theia-monitor-vscode/theia-cloud-monitor-1.1.0-next.vsix differ diff --git a/demo/dockerfiles/demo-theia-monitor-vscode/theia-cloud-monitor-1.1.2.vsix b/demo/dockerfiles/demo-theia-monitor-vscode/theia-cloud-monitor-1.1.2.vsix new file mode 100644 index 000000000..8dea4d542 Binary files /dev/null and b/demo/dockerfiles/demo-theia-monitor-vscode/theia-cloud-monitor-1.1.2.vsix differ diff --git a/demo/dockerfiles/demo-theia-monitor-vscode/theia-cloud-monitor-1.2.0-next.vsix b/demo/dockerfiles/demo-theia-monitor-vscode/theia-cloud-monitor-1.2.0-next.vsix new file mode 100644 index 000000000..a9be16cb2 Binary files /dev/null and b/demo/dockerfiles/demo-theia-monitor-vscode/theia-cloud-monitor-1.2.0-next.vsix differ diff --git a/demo/dockerfiles/demo-theia-monitor-vscode/theiacloud-monitor-0.12.0-next.vsix b/demo/dockerfiles/demo-theia-monitor-vscode/theiacloud-monitor-0.12.0-next.vsix deleted file mode 100644 index 579df495f..000000000 Binary files a/demo/dockerfiles/demo-theia-monitor-vscode/theiacloud-monitor-0.12.0-next.vsix and /dev/null differ diff --git a/dockerfiles/conversion-webhook/Dockerfile b/dockerfiles/conversion-webhook/Dockerfile index afe835406..6a2fcebee 100644 --- a/dockerfiles/conversion-webhook/Dockerfile +++ b/dockerfiles/conversion-webhook/Dockerfile @@ -1,18 +1,33 @@ -FROM eclipse-temurin:17-jdk AS builder +FROM eclipse-temurin:21-jdk AS builder +# Accept APT_HTTP_PROXY build-arg for caching +ARG APT_HTTP_PROXY +# Configure apt proxy if provided (hybrid: works on ARC and GitHub runners) +RUN if [ -n "$APT_HTTP_PROXY" ]; then \ + echo "Acquire::http::Proxy \"$APT_HTTP_PROXY\";" > /etc/apt/apt.conf.d/01proxy; \ + # Force HTTP repositories when using proxy (Noble defaults to HTTPS which apt-cacher-ng cannot cache) + sed -i 's|https://|http://|g' /etc/apt/sources.list.d/*.sources 2>/dev/null || true; \ + sed -i 's|https://|http://|g' /etc/apt/sources.list 2>/dev/null || true; \ + echo "Using APT Proxy: $APT_HTTP_PROXY with HTTP repositories"; \ + else \ + echo "No APT Proxy set, using direct connection"; \ + fi RUN apt-get update && apt-get install -y maven WORKDIR /conversion COPY java/common ./common COPY java/conversion ./conversion -RUN cd /conversion/common/maven-conf && \ +RUN --mount=type=cache,target=/root/.m2 \ + cd /conversion/common/maven-conf && \ mvn clean install --no-transfer-progress && \ cd /conversion/common/org.eclipse.theia.cloud.common && \ - mvn clean install --no-transfer-progress&& \ + mvn clean install --no-transfer-progress && \ cd /conversion/conversion/org.eclipse.theia.cloud.conversion && \ mvn clean package -Dmaven.test.skip=true -Dquarkus.package.type=uber-jar --no-transfer-progress -FROM eclipse-temurin:17-jre-alpine +FROM eclipse-temurin:21-jre-alpine WORKDIR /conversion -COPY --from=builder /conversion/conversion/org.eclipse.theia.cloud.conversion/target/conversion-webhook-0.12.0-SNAPSHOT-runner.jar . +COPY --from=builder /conversion/conversion/org.eclipse.theia.cloud.conversion/target/conversion-webhook-1.2.0-SNAPSHOT-runner.jar . + +ENV CERT_RELOAD_PERIOD=604800 -ENTRYPOINT java -jar ./conversion-webhook-0.12.0-SNAPSHOT-runner.jar -CMD [ "" ] \ No newline at end of file +ENTRYPOINT ["java", "-Dquarkus.http.ssl.certificate.reload-period=${CERT_RELOAD_PERIOD}", "-jar", "./conversion-webhook-1.2.0-SNAPSHOT-runner.jar"] +CMD [ "" ] diff --git a/dockerfiles/landing-page/.project b/dockerfiles/landing-page/.project deleted file mode 100644 index 163ddcb4f..000000000 --- a/dockerfiles/landing-page/.project +++ /dev/null @@ -1,11 +0,0 @@ - - - landing-page-docker - - - - - - - - diff --git a/dockerfiles/landing-page/Dockerfile b/dockerfiles/landing-page/Dockerfile deleted file mode 100644 index 004ba1dcb..000000000 --- a/dockerfiles/landing-page/Dockerfile +++ /dev/null @@ -1,21 +0,0 @@ -# This docker file build uses the local common package - -# build stage -FROM node:20-alpine as build-stage -WORKDIR /app -COPY node/common/src ./common/src -COPY node/common/package.json node/common/tsconfig*.json ./common/ -COPY node/configs ./configs/ -COPY node/tsconfig.json node/package*.json ./ -COPY node/landing-page/. ./landing-page/ - -RUN npm ci && \ - npm run build -w common && \ - npm run build -w landing-page && \ - chmod 644 /app/landing-page/dist/favicon.ico - -# production stage -FROM nginx:stable-alpine as production-stage -COPY --from=build-stage /app/landing-page/dist /usr/share/nginx/html -EXPOSE 80 -CMD ["nginx", "-g", "daemon off;"] diff --git a/dockerfiles/operator/Dockerfile b/dockerfiles/operator/Dockerfile index 56f6fdb9b..5190e4a3f 100644 --- a/dockerfiles/operator/Dockerfile +++ b/dockerfiles/operator/Dockerfile @@ -1,23 +1,75 @@ -FROM eclipse-temurin:17-jdk AS builder +FROM eclipse-temurin:21-jdk AS builder +# Accept APT_HTTP_PROXY build-arg for caching +ARG APT_HTTP_PROXY +# Configure apt proxy if provided (hybrid: works on ARC and GitHub runners) +RUN if [ -n "$APT_HTTP_PROXY" ]; then \ + echo "Acquire::http::Proxy \"$APT_HTTP_PROXY\";" > /etc/apt/apt.conf.d/01proxy; \ + # Force HTTP repositories when using proxy (Noble defaults to HTTPS which apt-cacher-ng cannot cache) + sed -i 's|https://|http://|g' /etc/apt/sources.list.d/*.sources 2>/dev/null || true; \ + sed -i 's|https://|http://|g' /etc/apt/sources.list 2>/dev/null || true; \ + echo "Using APT Proxy: $APT_HTTP_PROXY with HTTP repositories"; \ + else \ + echo "No APT Proxy set, using direct connection"; \ + fi RUN apt-get update && apt-get install -y maven WORKDIR /operator COPY java/common ./common COPY java/operator ./operator -RUN cd /operator/common/maven-conf && \ - mvn clean install --no-transfer-progress && \ +RUN --mount=type=secret,id=SENTRY_AUTH_TOKEN \ + --mount=type=cache,target=/root/.m2 \ + SENTRY_AUTH_TOKEN_VALUE="$(cat /run/secrets/SENTRY_AUTH_TOKEN 2>/dev/null || true)" && \ + MAVEN_SENTRY_ARGS="" && \ + if [ -n "$SENTRY_AUTH_TOKEN_VALUE" ]; then \ + export SENTRY_AUTH_TOKEN="$SENTRY_AUTH_TOKEN_VALUE" && \ + MAVEN_SENTRY_ARGS="-Dsentry=true" && \ + echo "Sentry build integration enabled"; \ + else \ + echo "No SENTRY_AUTH_TOKEN provided, skipping Sentry build integration"; \ + fi && \ + cd /operator/common/maven-conf && \ + mvn clean install --no-transfer-progress $MAVEN_SENTRY_ARGS && \ cd /operator/common/org.eclipse.theia.cloud.common && \ - mvn clean install --no-transfer-progress && \ + mvn clean install --no-transfer-progress $MAVEN_SENTRY_ARGS && \ cd /operator/operator/org.eclipse.theia.cloud.operator && \ - mvn clean install --no-transfer-progress && \ + mvn clean install --no-transfer-progress $MAVEN_SENTRY_ARGS && \ cd /operator/operator/org.eclipse.theia.cloud.defaultoperator && \ - mvn clean verify --no-transfer-progress + mvn clean verify --no-transfer-progress $MAVEN_SENTRY_ARGS -FROM eclipse-temurin:17-jre-alpine +FROM eclipse-temurin:21-jre-alpine +RUN apk add --no-cache curl unzip RUN mkdir /templates WORKDIR /log-config COPY java/operator/org.eclipse.theia.cloud.defaultoperator/log4j2.xml . WORKDIR /operator -COPY --from=builder /operator/operator/org.eclipse.theia.cloud.defaultoperator/target/defaultoperator-0.12.0-SNAPSHOT-jar-with-dependencies.jar . +RUN curl -fsSL -o sentry-agent.zip https://github.com/getsentry/sentry-java/releases/download/8.29.0/sentry-opentelemetry-agent-8.29.0.zip && \ + unzip -j sentry-agent.zip -d . && \ + rm sentry-agent.zip +COPY --from=builder /operator/operator/org.eclipse.theia.cloud.defaultoperator/target/defaultoperator-1.2.0-SNAPSHOT-jar-with-dependencies.jar . +COPY --from=builder /operator/operator/org.eclipse.theia.cloud.defaultoperator/sentry.properties ./sentry.properties + +# If set to true, enables Sentry Java agent flags. +ENV SENTRY_ENABLE=false +# Default path for the baked-in Sentry properties file. +# This can still be overridden with a mounted file or env-based config if needed. +ENV SENTRY_PROPERTIES_FILE=/operator/sentry.properties + +RUN cat > /entrypoint.sh <<'EOF_ENTRYPOINT' && chmod +x /entrypoint.sh +#!/bin/sh +set -eu + +JAVA_OPTS="" +if [ "${SENTRY_ENABLE:-false}" = "true" ]; then + JAVA_OPTS="$JAVA_OPTS -javaagent:/operator/sentry-opentelemetry-agent-8.29.0.jar" + if [ -n "${SENTRY_PROPERTIES_FILE:-}" ] && [ -f "${SENTRY_PROPERTIES_FILE}" ]; then + JAVA_OPTS="$JAVA_OPTS -Dsentry.properties.file=${SENTRY_PROPERTIES_FILE}" + fi +fi + +exec java $JAVA_OPTS \ + -Dlog4j2.configurationFile=/log-config/log4j2.xml \ + -jar ./defaultoperator-1.2.0-SNAPSHOT-jar-with-dependencies.jar \ + "$@" +EOF_ENTRYPOINT + # to get more debug information from the kubernetes client itself, add -Dorg.slf4j.simpleLogger.defaultLogLevel=DEBUG below -ENTRYPOINT [ "java", "-Dlog4j2.configurationFile=/log-config/log4j2.xml", "-jar", "./defaultoperator-0.12.0-SNAPSHOT-jar-with-dependencies.jar" ] -CMD [ "" ] +ENTRYPOINT ["/entrypoint.sh"] diff --git a/dockerfiles/operator/Dockerfile.withcache b/dockerfiles/operator/Dockerfile.withcache index df9323799..0c8249bb4 100644 --- a/dockerfiles/operator/Dockerfile.withcache +++ b/dockerfiles/operator/Dockerfile.withcache @@ -1,4 +1,4 @@ -FROM eclipse-temurin:17-jdk AS builder +FROM eclipse-temurin:21-jdk AS builder RUN apt-get update && apt-get install -y maven WORKDIR /operator COPY java/common ./common @@ -13,12 +13,12 @@ RUN --mount=type=cache,target=/root/.m2 \ cd /operator/operator/org.eclipse.theia.cloud.defaultoperator && \ mvn clean verify -FROM eclipse-temurin:17-jre-alpine +FROM eclipse-temurin:21-jre-alpine RUN mkdir /templates WORKDIR /log-config COPY java/operator/org.eclipse.theia.cloud.defaultoperator/log4j2.xml . WORKDIR /operator -COPY --from=builder /operator/operator/org.eclipse.theia.cloud.defaultoperator/target/defaultoperator-0.12.0-SNAPSHOT-jar-with-dependencies.jar . +COPY --from=builder /operator/operator/org.eclipse.theia.cloud.defaultoperator/target/defaultoperator-1.2.0-SNAPSHOT-jar-with-dependencies.jar . # to get more debug information from the kubernetes client itself, add -Dorg.slf4j.simpleLogger.defaultLogLevel=DEBUG below -ENTRYPOINT [ "java", "-Dlog4j2.configurationFile=/log-config/log4j2.xml", "-jar", "./defaultoperator-0.12.0-SNAPSHOT-jar-with-dependencies.jar" ] +ENTRYPOINT [ "java", "-Dlog4j2.configurationFile=/log-config/log4j2.xml", "-jar", "./defaultoperator-1.2.0-SNAPSHOT-jar-with-dependencies.jar" ] CMD [ "" ] diff --git a/dockerfiles/service/Dockerfile b/dockerfiles/service/Dockerfile index b9f6c549a..b4530792d 100644 --- a/dockerfiles/service/Dockerfile +++ b/dockerfiles/service/Dockerfile @@ -1,33 +1,91 @@ -FROM eclipse-temurin:17-jdk AS builder +FROM eclipse-temurin:21-jdk AS builder +# Accept APT_HTTP_PROXY build-arg for caching +ARG APT_HTTP_PROXY +# Configure apt proxy if provided (hybrid: works on ARC and GitHub runners) +RUN if [ -n "$APT_HTTP_PROXY" ]; then \ + echo "Acquire::http::Proxy \"$APT_HTTP_PROXY\";" > /etc/apt/apt.conf.d/01proxy; \ + # Force HTTP repositories when using proxy (Noble defaults to HTTPS which apt-cacher-ng cannot cache) + sed -i 's|https://|http://|g' /etc/apt/sources.list.d/*.sources 2>/dev/null || true; \ + sed -i 's|https://|http://|g' /etc/apt/sources.list 2>/dev/null || true; \ + echo "Using APT Proxy: $APT_HTTP_PROXY with HTTP repositories"; \ + else \ + echo "No APT Proxy set, using direct connection"; \ + fi RUN apt-get update && apt-get install -y maven WORKDIR /service COPY java/common ./common COPY java/service ./service -RUN cd /service/common/maven-conf && \ - mvn clean install --no-transfer-progress && \ +RUN --mount=type=secret,id=SENTRY_AUTH_TOKEN \ + --mount=type=cache,target=/root/.m2 \ + SENTRY_AUTH_TOKEN_VALUE="$(cat /run/secrets/SENTRY_AUTH_TOKEN 2>/dev/null || true)" && \ + MAVEN_SENTRY_ARGS="" && \ + if [ -n "$SENTRY_AUTH_TOKEN_VALUE" ]; then \ + export SENTRY_AUTH_TOKEN="$SENTRY_AUTH_TOKEN_VALUE" && \ + MAVEN_SENTRY_ARGS="-Dsentry=true" && \ + echo "Sentry build integration enabled"; \ + else \ + echo "No SENTRY_AUTH_TOKEN provided, skipping Sentry build integration"; \ + fi && \ + cd /service/common/maven-conf && \ + mvn clean install --no-transfer-progress $MAVEN_SENTRY_ARGS && \ cd /service/common/org.eclipse.theia.cloud.common && \ - mvn clean install --no-transfer-progress&& \ + mvn clean install --no-transfer-progress $MAVEN_SENTRY_ARGS && \ cd /service/service/org.eclipse.theia.cloud.service && \ - mvn clean package -Dmaven.test.skip=true -Dquarkus.package.type=uber-jar --no-transfer-progress + mvn clean package -Dmaven.test.skip=true -Dquarkus.package.type=uber-jar --no-transfer-progress $MAVEN_SENTRY_ARGS -FROM eclipse-temurin:17-jre-alpine +FROM eclipse-temurin:21-jre-alpine +RUN apk add --no-cache curl unzip WORKDIR /service -COPY --from=builder /service/service/org.eclipse.theia.cloud.service/target/service-0.12.0-SNAPSHOT-runner.jar . -ENV APPID default-app-id -ENV SERVICE_PORT 8081 +RUN curl -fsSL -o sentry-agent.zip https://github.com/getsentry/sentry-java/releases/download/8.29.0/sentry-opentelemetry-agent-8.29.0.zip && \ + unzip -j sentry-agent.zip -d . && \ + rm sentry-agent.zip +COPY --from=builder /service/service/org.eclipse.theia.cloud.service/target/service-1.2.0-SNAPSHOT-runner.jar . +COPY --from=builder /service/service/org.eclipse.theia.cloud.service/sentry.properties ./sentry.properties +ENV SERVICE_PORT=8081 -ENV KEYCLOAK_ENABLE true +ENV APPID=default-app-id + +ENV KEYCLOAK_ENABLE=true # Keycloak config -ENV KEYCLOAK_SERVERURL https://keycloak.url/auth/realms/TheiaCloud -ENV KEYCLOAK_CLIENTID theia-cloud -ENV KEYCLOAK_CLIENTSECRET publicbutoauth2proxywantsasecret - -ENTRYPOINT java -Dtheia.cloud.app.id=${APPID} \ - -Dquarkus.http.port=${SERVICE_PORT} \ - -Dtheia.cloud.use.keycloak=${KEYCLOAK_ENABLE} \ - -Dquarkus.oidc.auth-server-url=${KEYCLOAK_SERVERURL} \ - -Dquarkus.oidc.client-id=${KEYCLOAK_CLIENTID} \ - -Dquarkus.oidc.credentials.secret=${KEYCLOAK_CLIENTSECRET} \ - -jar ./service-0.12.0-SNAPSHOT-runner.jar -CMD [ "" ] \ No newline at end of file +ENV KEYCLOAK_SERVERURL=https://keycloak.url/auth/realms/TheiaCloud +ENV KEYCLOAK_CLIENTID=theia-cloud +ENV KEYCLOAK_CLIENTSECRET=publicbutoauth2proxywantsasecret +ENV KEYCLOAK_ADMIN_GROUP=theia-cloud/admin +ENV ADMIN_API_TOKEN= + +# If set to true, enables Sentry Java agent flags. +ENV SENTRY_ENABLE=false +# Default path for the baked-in Sentry properties file. +# This can still be overridden with a mounted file or env-based config if needed. +ENV SENTRY_PROPERTIES_FILE=/service/sentry.properties + +# Use SERVICE_AUTH_TOKEN if set, otherwise fall back to APPID for backwards compatibility +RUN cat > /entrypoint.sh <<'EOF_ENTRYPOINT' && chmod +x /entrypoint.sh +#!/bin/sh +set -eu + +JAVA_OPTS="" +if [ "${SENTRY_ENABLE:-false}" = "true" ]; then + JAVA_OPTS="$JAVA_OPTS -javaagent:/service/sentry-opentelemetry-agent-8.29.0.jar" + if [ -n "${SENTRY_PROPERTIES_FILE:-}" ] && [ -f "${SENTRY_PROPERTIES_FILE}" ]; then + JAVA_OPTS="$JAVA_OPTS -Dsentry.properties.file=${SENTRY_PROPERTIES_FILE}" + fi +fi + +SERVICE_AUTH_TOKEN_EFFECTIVE="${SERVICE_AUTH_TOKEN:-${APPID}}" + +exec java $JAVA_OPTS \ + -Dtheia.cloud.service.auth.token="${SERVICE_AUTH_TOKEN_EFFECTIVE}" \ + -Dquarkus.http.port="${SERVICE_PORT}" \ + -Dtheia.cloud.auth.admin.group="${KEYCLOAK_ADMIN_GROUP}" \ + -Dtheia.cloud.admin.api.token="${ADMIN_API_TOKEN}" \ + -Dtheia.cloud.use.keycloak="${KEYCLOAK_ENABLE}" \ + -Dquarkus.oidc.auth-server-url="${KEYCLOAK_SERVERURL}" \ + -Dquarkus.oidc.client-id="${KEYCLOAK_CLIENTID}" \ + -Dquarkus.oidc.credentials.secret="${KEYCLOAK_CLIENTSECRET}" \ + -jar ./service-1.2.0-SNAPSHOT-runner.jar \ + "$@" +EOF_ENTRYPOINT + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/dockerfiles/service/Dockerfile.withcache b/dockerfiles/service/Dockerfile.withcache index e468153d8..ba30b17b7 100644 --- a/dockerfiles/service/Dockerfile.withcache +++ b/dockerfiles/service/Dockerfile.withcache @@ -1,4 +1,4 @@ -FROM eclipse-temurin:17-jdk AS builder +FROM eclipse-temurin:21-jdk AS builder RUN apt-get update && apt-get install -y maven WORKDIR /service COPY java/common ./common @@ -11,24 +11,28 @@ RUN --mount=type=cache,target=/root/.m2 \ cd /service/service/org.eclipse.theia.cloud.service && \ mvn clean package -Dmaven.test.skip=true -Dquarkus.package.type=uber-jar -FROM eclipse-temurin:17-jre-alpine +FROM eclipse-temurin:21-jre-alpine WORKDIR /service -COPY --from=builder /service/service/org.eclipse.theia.cloud.service/target/service-0.12.0-SNAPSHOT-runner.jar . -ENV APPID default-app-id +COPY --from=builder /service/service/org.eclipse.theia.cloud.service/target/service-1.2.0-SNAPSHOT-runner.jar . + ENV SERVICE_PORT 8081 +ENV APPID default-app-id ENV KEYCLOAK_ENABLE true # Keycloak config ENV KEYCLOAK_SERVERURL https://keycloak.url/auth/realms/TheiaCloud ENV KEYCLOAK_CLIENTID theia-cloud ENV KEYCLOAK_CLIENTSECRET publicbutoauth2proxywantsasecret +ENV ADMIN_API_TOKEN "" -ENTRYPOINT java -Dtheia.cloud.app.id=${APPID} \ +# Use SERVICE_AUTH_TOKEN if set, otherwise fall back to APPID for backwards compatibility +ENTRYPOINT java -Dtheia.cloud.service.auth.token=${SERVICE_AUTH_TOKEN:-${APPID}} \ -Dquarkus.http.port=${SERVICE_PORT} \ + -Dtheia.cloud.admin.api.token=${ADMIN_API_TOKEN} \ -Dtheia.cloud.use.keycloak=${KEYCLOAK_ENABLE} \ -Dquarkus.oidc.auth-server-url=${KEYCLOAK_SERVERURL} \ -Dquarkus.oidc.client-id=${KEYCLOAK_CLIENTID} \ -Dquarkus.oidc.credentials.secret=${KEYCLOAK_CLIENTSECRET} \ - -jar ./service-0.12.0-SNAPSHOT-runner.jar -CMD [ "" ] \ No newline at end of file + -jar ./service-1.2.0-SNAPSHOT-runner.jar +CMD [ "" ] diff --git a/dockerfiles/wondershaper/Dockerfile b/dockerfiles/wondershaper/Dockerfile index e34e18b7c..edf2fe5ce 100644 --- a/dockerfiles/wondershaper/Dockerfile +++ b/dockerfiles/wondershaper/Dockerfile @@ -1,4 +1,16 @@ FROM debian:bookworm-slim +# Accept APT_HTTP_PROXY build-arg for caching +ARG APT_HTTP_PROXY +# Configure apt proxy if provided (hybrid: works on ARC and GitHub runners) +RUN if [ -n "$APT_HTTP_PROXY" ]; then \ + echo "Acquire::http::Proxy \"$APT_HTTP_PROXY\";" > /etc/apt/apt.conf.d/01proxy; \ + # Force HTTP repositories when using proxy (Bookworm uses HTTPS which apt-cacher-ng cannot cache) + sed -i 's|https://|http://|g' /etc/apt/sources.list.d/*.sources 2>/dev/null || true; \ + sed -i 's|https://|http://|g' /etc/apt/sources.list 2>/dev/null || true; \ + echo "Using APT Proxy: $APT_HTTP_PROXY with HTTP repositories"; \ + else \ + echo "No APT Proxy set, using direct connection"; \ + fi RUN apt-get update && apt-get install -y iproute2 wondershaper && \ apt-get clean # kilobits per second diff --git a/documentation/Architecture.md b/documentation/Architecture.md index 12eb59266..8ee2dc1a8 100644 --- a/documentation/Architecture.md +++ b/documentation/Architecture.md @@ -2,7 +2,9 @@ The Theia Cloud Operator listens for changes to custom resources inside the cluster. With those custom resources clients may trigger the creation/deletion/handling of workspaces and sessions. The Operator is responsible for handling all things that are related to the Kubernetes-Resources for workspaces and sessions. All access is authenticated via an Authenticator. -![Theia Cloud Diagram](theiacloud.png "Theia Cloud") +External systems (product UI or integration) launch sessions by calling the REST service. See [External Session Launch Integration](./ExternalIntegration.md) for the launch contract and how launch data reaches the session container. + +![Theia Cloud Diagram](theia-cloud.png "Theia Cloud") ## Operator Java Overview diff --git a/documentation/Building.md b/documentation/Building.md index 83cea270a..e91635a82 100644 --- a/documentation/Building.md +++ b/documentation/Building.md @@ -1,6 +1,6 @@ # Building -Besides the Operator, REST-Service, and the landing page, Theia Cloud also provides a few simple Theia-based demo applications and an init-container running wondershaper which may enable limiting the network bandwidth of a running application. +Besides the Operator and REST-Service, Theia Cloud also provides a few simple Theia-based demo applications and an init-container running wondershaper which may enable limiting the network bandwidth of a running application. **Note:** all commands are from the root of the repository. @@ -16,12 +16,6 @@ docker build --no-cache -t theia-cloud-service -f dockerfiles/service/Dockerfile docker build --no-cache -t theia-cloud-operator -f dockerfiles/operator/Dockerfile . ``` -## Landing page - -```bash -docker build -t theia-cloud-landing-page -f dockerfiles/landing-page/Dockerfile . -``` - ## Wondershaper ```bash diff --git a/documentation/ExternalIntegration.md b/documentation/ExternalIntegration.md new file mode 100644 index 000000000..c6401dac0 --- /dev/null +++ b/documentation/ExternalIntegration.md @@ -0,0 +1,112 @@ +# External Session Launch Integration + +External systems (primarily the EduIDE landing page / Artemis) launch IDE sessions by calling the Quarkus service. This page documents the direct REST contract and how the launch data flows through to the session container. For the auto-generated endpoint and model reference, see [`./api/README.md`](./api/README.md); those files are generated from [`openapi.json`](./openapi.json) (see [`./OpenAPI.md`](./OpenAPI.md)) and must not be edited by hand. + +## The launch endpoint + +`POST /service` accepts a `LaunchRequest` and returns a plain-string session URL. It creates a workspace if required. + +`GET /service/{appId}` is an OPTIONAL health/readiness probe that returns a boolean. It is not a launch prerequisite. + +The following example first pings the service (optional) and then launches a session: + +```bash +# Optional readiness probe. Returns "true" when the service is available. +curl "https:///service/" + +# Launch a session. Blocks until the session URL is ready. +curl -X POST "https:///service" \ + -H "Content-Type: application/json" \ + -H "Accept: text/plain" \ + -d '{ + "appId": "", + "user": "student@example.org", + "appDefinition": "eduide", + "workspaceName": "student-exercise-1", + "env": { + "fromMap": { + "THEIA": "true", + "GIT_URI": "https://artemis.example.org/git/course/exercise.git", + "GIT_USER": "student", + "GIT_MAIL": "student@example.org", + "ARTEMIS_TOKEN": "", + "ARTEMIS_URL": "https://artemis.example.org", + "TEMPLATE": "java-maven" + } + } + }' +``` + +The response body is the session URL as plain text. + +## LaunchRequest and EnvironmentVars payload + +`LaunchRequest` carries the following fields: + +| Field | Type | Notes | +| --------------- | ----------------- | --------------------------------------------------------------------------- | +| `appId` | string | The service auth token. Required. | +| `user` | string | The user identification, usually the email address. | +| `appDefinition` | string | The app to launch. | +| `workspaceName` | string | Optional. Name of the workspace to mount or create. | +| `label` | string | Optional. Human-readable workspace label. A default is generated if absent. | +| `ephemeral` | boolean | Optional. If true, no workspace is created for the session. | +| `timeout` | integer | Minutes to wait for session launch. Default is 3 minutes. | +| `env` | `EnvironmentVars` | Environment variables to deliver to the session. | + +`EnvironmentVars` bundles three sources of environment variables: + +| Field | Type | Notes | +| ---------------- | ------------------- | ------------------------------------------------------------------------- | +| `fromMap` | map of key/value | Environment variables passed directly to the deployment. | +| `fromConfigMaps` | list of names | Names of PRE-EXISTING in-cluster ConfigMaps to source variables from. | +| `fromSecrets` | list of names | Names of PRE-EXISTING in-cluster Secrets to source variables from. | + +External callers outside the cluster pass values via `fromMap`. Prefer `fromSecrets` for sensitive values that are already held as Kubernetes Secrets in the cluster. + +The EduIDE env-var keys the landing page sends via `fromMap` are `THEIA`, `GIT_URI`, `GIT_USER`, `GIT_MAIL`, `ARTEMIS_TOKEN`, `ARTEMIS_URL`, and `TEMPLATE`. Example body: + +```json +{ + "appId": "", + "user": "student@example.org", + "appDefinition": "eduide", + "workspaceName": "student-exercise-1", + "env": { + "fromMap": { + "THEIA": "true", + "GIT_URI": "https://artemis.example.org/git/course/exercise.git", + "GIT_USER": "student", + "GIT_MAIL": "student@example.org", + "ARTEMIS_TOKEN": "", + "ARTEMIS_URL": "https://artemis.example.org", + "TEMPLATE": "java-maven" + } + } +} +``` + +## End-to-end flow + +The service (`RootResource.launch`) validates the request, then `K8sUtil` writes a `Session` custom resource that carries the env map (`spec.envVars`, `spec.envVarsFromConfigMaps`, and `spec.envVarsFromSecrets`). The operator then delivers the variables to the running IDE in one of two ways: + +- Lazy path: the operator injects the variables directly into the IDE container's environment in the pod spec (`AddedHandlerUtil.addCustomEnvVarsToDeploymentFromSession`). +- Eager / prewarmed path: the deployment already exists, so the operator collects the variables (`SessionEnvCollector`) and pushes them asynchronously over HTTP `POST /data` (`AsyncDataInjector`) to the data-bridge extension inside the container. + +The environment variables are opaque to EduIDE-Cloud itself. The session container image (Scorpio) consumes them to configure git, authenticate with Artemis, and clone `GIT_URI`. + +For the broader system context, see [`./Architecture.md`](./Architecture.md). + +## Authentication + +`appId` must equal the server's configured service auth token. It is validated in `BaseResource`, and a mismatch returns HTTP 470. + +OIDC (Keycloak) authentication runs via Quarkus. Send `Authorization: Bearer `. The `user` field defaults to the JWT `email` claim if omitted, and must match the token email if supplied (otherwise HTTP 403). + +In anonymous mode (Keycloak disabled), `user` is mandatory. A blank `user` returns HTTP 400. + +## Behaviors integrators must know + +- `POST /service` blocks until the session URL is ready (up to `timeout`, default 3 minutes). Set the client HTTP timeout above 3 minutes. +- Omitting `workspaceName` (with `ephemeral: false`) creates a new persistent volume and a fresh clone on every call. Pass a stable `workspaceName` to reuse a persistent workspace. +- `ephemeral: true` is rejected with HTTP 400 for app definitions that have workspace-mounting sidecars. Use a workspace-backed launch there. diff --git a/documentation/Install.md b/documentation/Install.md index 5c098c11f..eec746e6d 100644 --- a/documentation/Install.md +++ b/documentation/Install.md @@ -20,37 +20,42 @@ Please check fo As of writing this guide the installation command looks like this:\ `kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml` -### NginX Ingress Controller +### Gateway API + Envoy Gateway (required) -Follow the installation guide for your platform:\ - +Theia Cloud routes traffic through Gateway API (`HTTPRoute`) and requires Envoy Gateway as controller. -### Global certificate (Only when using paths) +The operator configures `X-Forwarded-Uri` using Envoy runtime syntax (`%REQ(:PATH)%`) in HTTPRoute rules. +With non-Envoy Gateway controllers this may be interpreted as a literal value and break path forwarding. -If Theia Cloud is used with paths instead of subdomains, the global HTTPS certificate should exist and be configured as the NginX Ingress Controller's default certificate. +Install Gateway API CRDs and Envoy Gateway first by following: -You can either provide the default certificate yourself or use a dummy service to initially generate the certificate. -In [global-certificate.yaml](./platforms/global-certificate.yaml), replace `example.com` with your own host and `letsencrypt-prod` with your certificate issuer of choice. -Apply the configuration with `kubectl apply -f ./documentation/platforms/global-certificate.yaml`. +- Gateway API: +- Envoy Gateway: -Configure the nginx controllers deployment to use the certificate `default-tls` in the default namespace as its default certificate. -For this, add argument `"--default-ssl-certificate=default/default-tls"` to the container by patching the controller's deployment. +After installation, verify the controller is available (default class name usually `envoy`): ```bash -kubectl patch deploy ingress-nginx-controller --type='json' -n ingress-nginx \ --p '[{ "op": "add", "path": "/spec/template/spec/containers/0/args/-", "value": "--default-ssl-certificate=default/default-tls" }]' +kubectl get gatewayclass +kubectl get gateway -A ``` +### Global certificate (only when using paths) + +If Theia Cloud is configured to use paths instead of subdomains, ensure the configured Gateway listeners cover your base host and use TLS certificates that match that host. + +When using the Helm chart-managed Gateway (`gateway.create=true`), listener + certificate references are rendered automatically from chart values. +When using a shared external Gateway (`gateway.create=false`), configure matching listeners and TLS certificateRefs on that shared Gateway before deploying Theia Cloud. + ## Install For this demo we will use nip.io hostnames. -Obtain the external IP address of the `ingress-nginx-controller`: +Obtain the external IP address of your Envoy Gateway service: ```bash $ kubectl get svc --all-namespaces NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE -ingress-nginx ingress-nginx-controller LoadBalancer 10.52.4.129 34.141.62.32 80:32507/TCP,443:32114/TCP 11m +envoy-gateway-system envoy-gateway LoadBalancer 10.52.4.129 34.141.62.32 80:32507/TCP,443:32114/TCP 11m ``` Open `./terraform/values/valuesDemo.yaml` and update the host section to use `subdomain.34.141.62.32.nip.io`: @@ -67,7 +72,7 @@ hosts: ### Add & update Theia Cloud helm repo ```bash -helm repo add theia-cloud-remote https://github.eclipsesource.com/theia-cloud-helm +helm repo add theia-cloud-remote https://eclipse-theia.github.io/theia-cloud-helm helm repo update ``` @@ -76,9 +81,9 @@ helm repo update ```bash helm install theia-cloud-base theia-cloud-remote/theia-cloud-base --set issuer.email=your-mail@example.com -helm install theia-cloud-crds theia-cloud-remote/theia-cloud-crds --namespace theiacloud --create-namespace +helm install theia-cloud-crds theia-cloud-remote/theia-cloud-crds --namespace theia-cloud --create-namespace -helm install theia-cloud theia-cloud-remote/theia-cloud --namespace theiacloud +helm install theia-cloud theia-cloud-remote/theia-cloud --namespace theia-cloud ``` ### Install the current next version @@ -88,31 +93,31 @@ Simply add the `--devel` flag: ```bash helm install theia-cloud-base theia-cloud-remote/theia-cloud-base --set issuer.email=your-mail@example.com --devel -helm install theia-cloud-crds theia-cloud-remote/theia-cloud-crds --namespace theiacloud --create-namespace --devel +helm install theia-cloud-crds theia-cloud-remote/theia-cloud-crds --namespace theia-cloud --create-namespace --devel -helm install theia-cloud theia-cloud-remote/theia-cloud --namespace theiacloud --devel +helm install theia-cloud theia-cloud-remote/theia-cloud --namespace theia-cloud --devel ``` ### Optional: switch to the newly created namespace ```bash -kubectl config set-context --current --namespace=theiacloud +kubectl config set-context --current --namespace=theia-cloud ``` ### Trouble shooting Recreate tls secret for sessions: -`kubectl delete secret ws-cert-secret -n theiacloud` +`kubectl delete secret ws-cert-secret -n theia-cloud` ## Check ```bash -helm get manifest theia-cloud -n theiacloud +helm get manifest theia-cloud -n theia-cloud ``` ## Uninstall ```bash -helm uninstall theia-cloud -n theiacloud +helm uninstall theia-cloud -n theia-cloud ``` diff --git a/documentation/Naming.md b/documentation/Naming.md new file mode 100644 index 000000000..f4418202a --- /dev/null +++ b/documentation/Naming.md @@ -0,0 +1,48 @@ +# Naming Conventions + +The project name is **Theia Cloud** (with a space), which is a subproject of Eclipse Theia. + +Since various languages and use cases follow different naming conventions, we aim to align with these conventions as closely as possible. + +Below are guidelines for commonly used naming conventions specific to this project. + +## `theia.cloud` + +Use `theia.cloud` in hierarchical structures and identifiers. Examples include: + +- **Java package/jar names**: `org.eclipse.theia.cloud` +- **Java system properties**: `-Dtheia.cloud.service.auth.token=yourServiceAuthToken` +- **Kubernetes group names/metadata**: `theia.cloud`, `theia.cloud/v1beta9`, `sessions.theia.cloud` +- **Theia command identifiers**: `theia.cloud.monitor.activity.report` + +## `TheiaCloud` / `theiaCloud` + +Use **PascalCase** or **camelCase** for Java/TypeScript class, method, and property names, such as `DefaultTheiaCloudOperatorLauncher` or `window.theiaCloudConfig`. + +This format is also appropriate in contexts where spaces are avoided for technical reasons, such as in **Keycloak Realm** names (e.g., `TheiaCloud`). + +## `theia-cloud` + +This format serves as a preferred technical abbreviation. Use it when no other conventions apply, including: + +- **NPM Package names** +- **File names** +- **k8s namespaces** + +## `THEIACLOUD` + +Use **all uppercase** with underscores for **environment variables**, such as `THEIACLOUD_SESSION_NAME`. + +We can't use `THEIA_CLOUD` because `THEIA_` env variables may get special treatment by Theia itself. + +## Theia Cloud + +In general documentation and free text, use the standard project name **Theia Cloud** (with a space). Avoid spaces in technical contexts. + +## `theia-cloud.io` + +For **Kubernetes labels**, use the project domain as a prefix to custom labels, such as `theia-cloud.io/workspace-name`. + +## Avoid `theiacloud` in Code + +Avoid using the project name without separators or spaces, especially without PascalCase or camelCase. This format is reserved for usernames, such as those on Docker or npm. diff --git a/documentation/OpenAPI.md b/documentation/OpenAPI.md index 35194223b..7af37e305 100644 --- a/documentation/OpenAPI.md +++ b/documentation/OpenAPI.md @@ -30,7 +30,9 @@ docker pull swaggerapi/swagger-ui docker run -p 80:8080 swaggerapi/swagger-ui ``` -Then browse to and explore the spec from here: +Then browse to and explore the spec from here: + +Or explore it from the locally started service: . ## Generate Typescript API @@ -48,3 +50,24 @@ Use the `openapi-generator-cli` from the root of this repository: ```bash openapi-generator-cli generate -g typescript-axios -i ./documentation/openapi.json -o node/common/src/client/ --additional-properties=supportsES6=true,typescriptThreePlus=true --skip-validate-spec ``` + +## Generate API Documentation + +Documentation for the service API can be generated as markdown files. + +If not already done for generating the typescript API, install the `openapi-generator-cli`: + +```bash +npm install @openapitools/openapi-generator-cli -g +openapi-generator-cli version-manager set 7.8.0 +``` + +Use the `openapi-generator-cli` from the root of this repository: + +```bash +openapi-generator-cli generate -g markdown -i ./documentation/openapi.json -o ./documentation/api --skip-validate-spec +``` + +> [!TIP] +> The Open API generator supports generating code and documentation in various languages. +> Execute `openapi-generator-cli list` to see all of them. diff --git a/documentation/api/Apis/AppDefinitionAdminResourceApi.md b/documentation/api/Apis/AppDefinitionAdminResourceApi.md new file mode 100644 index 000000000..51637912b --- /dev/null +++ b/documentation/api/Apis/AppDefinitionAdminResourceApi.md @@ -0,0 +1,37 @@ +# AppDefinitionAdminResourceApi + +All URIs are relative to *http://localhost* + +| Method | HTTP request | Description | +|------------- | ------------- | -------------| +| [**serviceAdminAppdefinitionAppDefinitionNamePatch**](AppDefinitionAdminResourceApi.md#serviceAdminAppdefinitionAppDefinitionNamePatch) | **PATCH** /service/admin/appdefinition/{appDefinitionName} | Updates an app definition | + + + +# **serviceAdminAppdefinitionAppDefinitionNamePatch** +> AppDefinition serviceAdminAppdefinitionAppDefinitionNamePatch(appDefinitionName, AppDefinitionUpdateRequest) + +Updates an app definition + + Updates an app definition's properties. Allowed properties to update are defined by AppDefinitionUpdateRequest. + +### Parameters + +|Name | Type | Description | Notes | +|------------- | ------------- | ------------- | -------------| +| **appDefinitionName** | **String**| The K8S resource name of the app definition to update. | [default to null] | +| **AppDefinitionUpdateRequest** | [**AppDefinitionUpdateRequest**](../Models/AppDefinitionUpdateRequest.md)| | | + +### Return type + +[**AppDefinition**](../Models/AppDefinition.md) + +### Authorization + +No authorization required + +### HTTP request headers + +- **Content-Type**: application/json +- **Accept**: application/json + diff --git a/documentation/api/Apis/AppDefinitionResourceApi.md b/documentation/api/Apis/AppDefinitionResourceApi.md new file mode 100644 index 000000000..0b1a888d8 --- /dev/null +++ b/documentation/api/Apis/AppDefinitionResourceApi.md @@ -0,0 +1,36 @@ +# AppDefinitionResourceApi + +All URIs are relative to *http://localhost* + +| Method | HTTP request | Description | +|------------- | ------------- | -------------| +| [**serviceAppdefinitionAppIdGet**](AppDefinitionResourceApi.md#serviceAppdefinitionAppIdGet) | **GET** /service/appdefinition/{appId} | List app definitions | + + + +# **serviceAppdefinitionAppIdGet** +> List serviceAppdefinitionAppIdGet(appId) + +List app definitions + + List available app definitions. + +### Parameters + +|Name | Type | Description | Notes | +|------------- | ------------- | ------------- | -------------| +| **appId** | **String**| | [default to null] | + +### Return type + +[**List**](../Models/AppDefinitionSpec.md) + +### Authorization + +[SecurityScheme](../README.md#SecurityScheme) + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: application/json + diff --git a/documentation/api/Apis/RootAdminResourceApi.md b/documentation/api/Apis/RootAdminResourceApi.md new file mode 100644 index 000000000..867ce134f --- /dev/null +++ b/documentation/api/Apis/RootAdminResourceApi.md @@ -0,0 +1,36 @@ +# RootAdminResourceApi + +All URIs are relative to *http://localhost* + +| Method | HTTP request | Description | +|------------- | ------------- | -------------| +| [**serviceAdminAppIdGet**](RootAdminResourceApi.md#serviceAdminAppIdGet) | **GET** /service/admin/{appId} | Admin Ping | + + + +# **serviceAdminAppIdGet** +> Boolean serviceAdminAppIdGet(appId) + +Admin Ping + + Replies with success if the service is available and the user an admin. + +### Parameters + +|Name | Type | Description | Notes | +|------------- | ------------- | ------------- | -------------| +| **appId** | **String**| | [default to null] | + +### Return type + +**Boolean** + +### Authorization + +No authorization required + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: text/plain + diff --git a/documentation/api/Apis/RootResourceApi.md b/documentation/api/Apis/RootResourceApi.md new file mode 100644 index 000000000..e7736523d --- /dev/null +++ b/documentation/api/Apis/RootResourceApi.md @@ -0,0 +1,64 @@ +# RootResourceApi + +All URIs are relative to *http://localhost* + +| Method | HTTP request | Description | +|------------- | ------------- | -------------| +| [**serviceAppIdGet**](RootResourceApi.md#serviceAppIdGet) | **GET** /service/{appId} | Ping | +| [**servicePost**](RootResourceApi.md#servicePost) | **POST** /service | Launch Session | + + + +# **serviceAppIdGet** +> Boolean serviceAppIdGet(appId) + +Ping + + Replies if the service is available. + +### Parameters + +|Name | Type | Description | Notes | +|------------- | ------------- | ------------- | -------------| +| **appId** | **String**| | [default to null] | + +### Return type + +**Boolean** + +### Authorization + +[SecurityScheme](../README.md#SecurityScheme) + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: text/plain + + +# **servicePost** +> String servicePost(LaunchRequest) + +Launch Session + + Launches a session and creates a workspace if required. Responds with the URL of the launched session. + +### Parameters + +|Name | Type | Description | Notes | +|------------- | ------------- | ------------- | -------------| +| **LaunchRequest** | [**LaunchRequest**](../Models/LaunchRequest.md)| | | + +### Return type + +**String** + +### Authorization + +[SecurityScheme](../README.md#SecurityScheme) + +### HTTP request headers + +- **Content-Type**: application/json +- **Accept**: text/plain + diff --git a/documentation/api/Apis/SessionResourceApi.md b/documentation/api/Apis/SessionResourceApi.md new file mode 100644 index 000000000..95f34d642 --- /dev/null +++ b/documentation/api/Apis/SessionResourceApi.md @@ -0,0 +1,150 @@ +# SessionResourceApi + +All URIs are relative to *http://localhost* + +| Method | HTTP request | Description | +|------------- | ------------- | -------------| +| [**serviceSessionAppIdUserGet**](SessionResourceApi.md#serviceSessionAppIdUserGet) | **GET** /service/session/{appId}/{user} | List sessions | +| [**serviceSessionDelete**](SessionResourceApi.md#serviceSessionDelete) | **DELETE** /service/session | Stop session | +| [**serviceSessionPatch**](SessionResourceApi.md#serviceSessionPatch) | **PATCH** /service/session | Report session activity | +| [**serviceSessionPerformanceAppIdSessionNameGet**](SessionResourceApi.md#serviceSessionPerformanceAppIdSessionNameGet) | **GET** /service/session/performance/{appId}/{sessionName} | Get performance metrics | +| [**serviceSessionPost**](SessionResourceApi.md#serviceSessionPost) | **POST** /service/session | Start a new session | + + + +# **serviceSessionAppIdUserGet** +> List serviceSessionAppIdUserGet(appId, user) + +List sessions + + List sessions of a user. + +### Parameters + +|Name | Type | Description | Notes | +|------------- | ------------- | ------------- | -------------| +| **appId** | **String**| | [default to null] | +| **user** | **String**| | [default to null] | + +### Return type + +[**List**](../Models/SessionSpec.md) + +### Authorization + +[SecurityScheme](../README.md#SecurityScheme) + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: application/json + + +# **serviceSessionDelete** +> Boolean serviceSessionDelete(SessionStopRequest) + +Stop session + + Stops a session. + +### Parameters + +|Name | Type | Description | Notes | +|------------- | ------------- | ------------- | -------------| +| **SessionStopRequest** | [**SessionStopRequest**](../Models/SessionStopRequest.md)| | | + +### Return type + +**Boolean** + +### Authorization + +[SecurityScheme](../README.md#SecurityScheme) + +### HTTP request headers + +- **Content-Type**: application/json +- **Accept**: text/plain + + +# **serviceSessionPatch** +> Boolean serviceSessionPatch(SessionActivityRequest) + +Report session activity + + Updates the last activity timestamp for a session to monitor activity. + +### Parameters + +|Name | Type | Description | Notes | +|------------- | ------------- | ------------- | -------------| +| **SessionActivityRequest** | [**SessionActivityRequest**](../Models/SessionActivityRequest.md)| | | + +### Return type + +**Boolean** + +### Authorization + +[SecurityScheme](../README.md#SecurityScheme) + +### HTTP request headers + +- **Content-Type**: application/json +- **Accept**: text/plain + + +# **serviceSessionPerformanceAppIdSessionNameGet** +> SessionPerformance serviceSessionPerformanceAppIdSessionNameGet(appId, sessionName) + +Get performance metrics + + Returns the current CPU and memory usage of the session's pod. + +### Parameters + +|Name | Type | Description | Notes | +|------------- | ------------- | ------------- | -------------| +| **appId** | **String**| | [default to null] | +| **sessionName** | **String**| | [default to null] | + +### Return type + +[**SessionPerformance**](../Models/SessionPerformance.md) + +### Authorization + +[SecurityScheme](../README.md#SecurityScheme) + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: application/json + + +# **serviceSessionPost** +> String serviceSessionPost(SessionStartRequest) + +Start a new session + + Starts a new session for an existing workspace and responds with the URL of the started session. + +### Parameters + +|Name | Type | Description | Notes | +|------------- | ------------- | ------------- | -------------| +| **SessionStartRequest** | [**SessionStartRequest**](../Models/SessionStartRequest.md)| | | + +### Return type + +**String** + +### Authorization + +[SecurityScheme](../README.md#SecurityScheme) + +### HTTP request headers + +- **Content-Type**: application/json +- **Accept**: text/plain + diff --git a/documentation/api/Apis/WorkspaceResourceApi.md b/documentation/api/Apis/WorkspaceResourceApi.md new file mode 100644 index 000000000..e30a57aac --- /dev/null +++ b/documentation/api/Apis/WorkspaceResourceApi.md @@ -0,0 +1,93 @@ +# WorkspaceResourceApi + +All URIs are relative to *http://localhost* + +| Method | HTTP request | Description | +|------------- | ------------- | -------------| +| [**serviceWorkspaceAppIdUserGet**](WorkspaceResourceApi.md#serviceWorkspaceAppIdUserGet) | **GET** /service/workspace/{appId}/{user} | List workspaces | +| [**serviceWorkspaceDelete**](WorkspaceResourceApi.md#serviceWorkspaceDelete) | **DELETE** /service/workspace | Delete workspace | +| [**serviceWorkspacePost**](WorkspaceResourceApi.md#serviceWorkspacePost) | **POST** /service/workspace | Create workspace | + + + +# **serviceWorkspaceAppIdUserGet** +> List serviceWorkspaceAppIdUserGet(appId, user) + +List workspaces + + Lists the workspaces of a user. + +### Parameters + +|Name | Type | Description | Notes | +|------------- | ------------- | ------------- | -------------| +| **appId** | **String**| | [default to null] | +| **user** | **String**| | [default to null] | + +### Return type + +[**List**](../Models/UserWorkspace.md) + +### Authorization + +[SecurityScheme](../README.md#SecurityScheme) + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: application/json + + +# **serviceWorkspaceDelete** +> Boolean serviceWorkspaceDelete(WorkspaceDeletionRequest) + +Delete workspace + + Deletes a workspace. + +### Parameters + +|Name | Type | Description | Notes | +|------------- | ------------- | ------------- | -------------| +| **WorkspaceDeletionRequest** | [**WorkspaceDeletionRequest**](../Models/WorkspaceDeletionRequest.md)| | | + +### Return type + +**Boolean** + +### Authorization + +[SecurityScheme](../README.md#SecurityScheme) + +### HTTP request headers + +- **Content-Type**: application/json +- **Accept**: text/plain + + +# **serviceWorkspacePost** +> UserWorkspace serviceWorkspacePost(WorkspaceCreationRequest) + +Create workspace + + Creates a new workspace for a user. + +### Parameters + +|Name | Type | Description | Notes | +|------------- | ------------- | ------------- | -------------| +| **WorkspaceCreationRequest** | [**WorkspaceCreationRequest**](../Models/WorkspaceCreationRequest.md)| | | + +### Return type + +[**UserWorkspace**](../Models/UserWorkspace.md) + +### Authorization + +[SecurityScheme](../README.md#SecurityScheme) + +### HTTP request headers + +- **Content-Type**: application/json +- **Accept**: application/json + diff --git a/documentation/api/Models/ActivityTracker.md b/documentation/api/Models/ActivityTracker.md new file mode 100644 index 000000000..1bb91339e --- /dev/null +++ b/documentation/api/Models/ActivityTracker.md @@ -0,0 +1,10 @@ +# ActivityTracker +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **timeoutAfter** | **Integer** | | [optional] [default to null] | +| **notifyAfter** | **Integer** | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/documentation/api/Models/AppDefinition.md b/documentation/api/Models/AppDefinition.md new file mode 100644 index 000000000..65349c560 --- /dev/null +++ b/documentation/api/Models/AppDefinition.md @@ -0,0 +1,21 @@ +# AppDefinition +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **apiVersion** | **String** | | [optional] [default to null] | +| **kind** | **String** | | [optional] [default to null] | +| **metadata** | [**ObjectMeta**](ObjectMeta.md) | | [optional] [default to null] | +| **spec** | [**AppDefinitionSpec**](AppDefinitionSpec.md) | | [optional] [default to null] | +| **status** | [**AppDefinitionStatus**](AppDefinitionStatus.md) | | [optional] [default to null] | +| **singular** | **String** | | [optional] [default to null] | +| **crdName** | **String** | | [optional] [default to null] | +| **scope** | **String** | | [optional] [default to null] | +| **plural** | **String** | | [optional] [default to null] | +| **served** | **Boolean** | | [optional] [default to null] | +| **storage** | **Boolean** | | [optional] [default to null] | +| **deprecated** | **Boolean** | | [optional] [default to null] | +| **deprecationWarning** | **String** | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/documentation/api/Models/AppDefinitionListRequest.md b/documentation/api/Models/AppDefinitionListRequest.md new file mode 100644 index 000000000..ecb02b0c1 --- /dev/null +++ b/documentation/api/Models/AppDefinitionListRequest.md @@ -0,0 +1,9 @@ +# AppDefinitionListRequest +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **appId** | **String** | The App Id of this Theia Cloud instance. Request without a matching Id will be denied. | [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/documentation/api/Models/AppDefinitionSpec.md b/documentation/api/Models/AppDefinitionSpec.md new file mode 100644 index 000000000..96f551d78 --- /dev/null +++ b/documentation/api/Models/AppDefinitionSpec.md @@ -0,0 +1,28 @@ +# AppDefinitionSpec +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **name** | **String** | | [optional] [default to null] | +| **image** | **String** | | [optional] [default to null] | +| **imagePullPolicy** | **String** | | [optional] [default to null] | +| **pullSecret** | **String** | | [optional] [default to null] | +| **uid** | **Integer** | | [optional] [default to null] | +| **port** | **Integer** | | [optional] [default to null] | +| **ingressname** | **String** | | [optional] [default to null] | +| **minInstances** | **Integer** | | [optional] [default to null] | +| **maxInstances** | **Integer** | | [optional] [default to null] | +| **timeout** | **Integer** | | [optional] [default to null] | +| **requestsMemory** | **String** | | [optional] [default to null] | +| **requestsCpu** | **String** | | [optional] [default to null] | +| **limitsMemory** | **String** | | [optional] [default to null] | +| **limitsCpu** | **String** | | [optional] [default to null] | +| **downlinkLimit** | **Integer** | | [optional] [default to null] | +| **uplinkLimit** | **Integer** | | [optional] [default to null] | +| **mountPath** | **String** | | [optional] [default to null] | +| **monitor** | [**Monitor**](Monitor.md) | | [optional] [default to null] | +| **options** | **Map** | | [optional] [default to null] | +| **ingressHostnamePrefixes** | **List** | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/documentation/api/Models/AppDefinitionStatus.md b/documentation/api/Models/AppDefinitionStatus.md new file mode 100644 index 000000000..ee1873466 --- /dev/null +++ b/documentation/api/Models/AppDefinitionStatus.md @@ -0,0 +1,10 @@ +# AppDefinitionStatus +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **operatorStatus** | **String** | | [optional] [default to null] | +| **operatorMessage** | **String** | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/documentation/api/Models/AppDefinitionUpdateRequest.md b/documentation/api/Models/AppDefinitionUpdateRequest.md new file mode 100644 index 000000000..960e157bd --- /dev/null +++ b/documentation/api/Models/AppDefinitionUpdateRequest.md @@ -0,0 +1,11 @@ +# AppDefinitionUpdateRequest +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **appId** | **String** | The App Id of this Theia Cloud instance. Request without a matching Id will be denied. | [default to null] | +| **minInstances** | **Integer** | The minimum number of instances to run. | [optional] [default to null] | +| **maxInstances** | **Integer** | The maximum number of instances to run. | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/documentation/api/Models/EnvironmentVars.md b/documentation/api/Models/EnvironmentVars.md new file mode 100644 index 000000000..10f2a6810 --- /dev/null +++ b/documentation/api/Models/EnvironmentVars.md @@ -0,0 +1,11 @@ +# EnvironmentVars +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **fromMap** | **Map** | Map of environment variables to be passed to Deployment. Ignored if Theia applications are started eagerly. Empty by default. | [optional] [default to null] | +| **fromConfigMaps** | **List** | List of ConfigMaps (by name) containing environment variables to be passed to Deployment as envFrom.configMapRef. Ignored if Theia applications are started eagerly. Empty by default. | [optional] [default to null] | +| **fromSecrets** | **List** | List of Secrets (by name) containing environment variables to be passed to Deployment as envFrom.secretRef. Ignored if Theia applications are started eagerly. Empty by default. | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/documentation/api/Models/LaunchRequest.md b/documentation/api/Models/LaunchRequest.md new file mode 100644 index 000000000..650226e43 --- /dev/null +++ b/documentation/api/Models/LaunchRequest.md @@ -0,0 +1,16 @@ +# LaunchRequest +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **appId** | **String** | The App Id of this Theia Cloud instance. Request without a matching Id will be denied. | [default to null] | +| **user** | **String** | The user identification, usually the email address. | [default to null] | +| **appDefinition** | **String** | The app to launch. Needs to be set if a new or ephemeral session should be launched. For an existing workspace the last app definition will be used if none is given. | [optional] [default to null] | +| **workspaceName** | **String** | The name of the workspace to mount/create. Needs to be set if an existing workspace should be launched. | [optional] [default to null] | +| **label** | **String** | The label of the workspace to mount/create. If no label is given, a default label will be generated. | [optional] [default to null] | +| **ephemeral** | **Boolean** | If true no workspace will be created for the session. | [optional] [default to null] | +| **timeout** | **Integer** | Number of minutes to wait for session launch. Default is 3 Minutes. | [optional] [default to null] | +| **env** | [**EnvironmentVars**](EnvironmentVars.md) | Environment variables | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/documentation/api/Models/ManagedFieldsEntry.md b/documentation/api/Models/ManagedFieldsEntry.md new file mode 100644 index 000000000..b82a31d14 --- /dev/null +++ b/documentation/api/Models/ManagedFieldsEntry.md @@ -0,0 +1,15 @@ +# ManagedFieldsEntry +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **apiVersion** | **String** | | [optional] [default to null] | +| **fieldsType** | **String** | | [optional] [default to null] | +| **fieldsV1** | [**Object**](.md) | | [optional] [default to null] | +| **manager** | **String** | | [optional] [default to null] | +| **operation** | **String** | | [optional] [default to null] | +| **subresource** | **String** | | [optional] [default to null] | +| **time** | **String** | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/documentation/api/Models/Monitor.md b/documentation/api/Models/Monitor.md new file mode 100644 index 000000000..5aec98fab --- /dev/null +++ b/documentation/api/Models/Monitor.md @@ -0,0 +1,10 @@ +# Monitor +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **port** | **Integer** | | [optional] [default to null] | +| **activityTracker** | [**ActivityTracker**](ActivityTracker.md) | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/documentation/api/Models/ObjectMeta.md b/documentation/api/Models/ObjectMeta.md new file mode 100644 index 000000000..7a744a143 --- /dev/null +++ b/documentation/api/Models/ObjectMeta.md @@ -0,0 +1,23 @@ +# ObjectMeta +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **annotations** | **Map** | | [optional] [default to null] | +| **creationTimestamp** | **String** | | [optional] [default to null] | +| **deletionGracePeriodSeconds** | **Long** | | [optional] [default to null] | +| **deletionTimestamp** | **String** | | [optional] [default to null] | +| **finalizers** | **List** | | [optional] [default to null] | +| **generateName** | **String** | | [optional] [default to null] | +| **generation** | **Long** | | [optional] [default to null] | +| **labels** | **Map** | | [optional] [default to null] | +| **managedFields** | [**List**](ManagedFieldsEntry.md) | | [optional] [default to null] | +| **name** | **String** | | [optional] [default to null] | +| **namespace** | **String** | | [optional] [default to null] | +| **ownerReferences** | [**List**](OwnerReference.md) | | [optional] [default to null] | +| **resourceVersion** | **String** | | [optional] [default to null] | +| **selfLink** | **String** | | [optional] [default to null] | +| **uid** | **String** | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/documentation/api/Models/OwnerReference.md b/documentation/api/Models/OwnerReference.md new file mode 100644 index 000000000..3837f2cd4 --- /dev/null +++ b/documentation/api/Models/OwnerReference.md @@ -0,0 +1,14 @@ +# OwnerReference +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **apiVersion** | **String** | | [optional] [default to null] | +| **kind** | **String** | | [optional] [default to null] | +| **blockOwnerDeletion** | **Boolean** | | [optional] [default to null] | +| **controller** | **Boolean** | | [optional] [default to null] | +| **name** | **String** | | [optional] [default to null] | +| **uid** | **String** | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/documentation/api/Models/PingRequest.md b/documentation/api/Models/PingRequest.md new file mode 100644 index 000000000..747a68927 --- /dev/null +++ b/documentation/api/Models/PingRequest.md @@ -0,0 +1,9 @@ +# PingRequest +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **appId** | **String** | The App Id of this Theia Cloud instance. Request without a matching Id will be denied. | [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/documentation/api/Models/SessionActivityRequest.md b/documentation/api/Models/SessionActivityRequest.md new file mode 100644 index 000000000..ff357d548 --- /dev/null +++ b/documentation/api/Models/SessionActivityRequest.md @@ -0,0 +1,10 @@ +# SessionActivityRequest +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **appId** | **String** | The App Id of this Theia Cloud instance. Request without a matching Id will be denied. | [default to null] | +| **sessionName** | **String** | The name of the session for which activity is reported. | [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/documentation/api/Models/SessionListRequest.md b/documentation/api/Models/SessionListRequest.md new file mode 100644 index 000000000..570cf84b1 --- /dev/null +++ b/documentation/api/Models/SessionListRequest.md @@ -0,0 +1,10 @@ +# SessionListRequest +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **appId** | **String** | The App Id of this Theia Cloud instance. Request without a matching Id will be denied. | [default to null] | +| **user** | **String** | The user identification, usually the email address. | [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/documentation/api/Models/SessionPerformance.md b/documentation/api/Models/SessionPerformance.md new file mode 100644 index 000000000..f552aa781 --- /dev/null +++ b/documentation/api/Models/SessionPerformance.md @@ -0,0 +1,12 @@ +# SessionPerformance +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **cpuAmount** | **String** | Used CPU amount of the workspace | [default to null] | +| **cpuFormat** | **String** | Used CPU format of the workspace | [default to null] | +| **memoryAmount** | **String** | Used memory amount of the workspace | [default to null] | +| **memoryFormat** | **String** | Used memory format of the workspace | [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/documentation/api/Models/SessionPerformanceRequest.md b/documentation/api/Models/SessionPerformanceRequest.md new file mode 100644 index 000000000..5b94e6473 --- /dev/null +++ b/documentation/api/Models/SessionPerformanceRequest.md @@ -0,0 +1,10 @@ +# SessionPerformanceRequest +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **appId** | **String** | The App Id of this Theia Cloud instance. Request without a matching Id will be denied. | [default to null] | +| **sessionName** | **String** | The name of the session | [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/documentation/api/Models/SessionSpec.md b/documentation/api/Models/SessionSpec.md new file mode 100644 index 000000000..cee3e0416 --- /dev/null +++ b/documentation/api/Models/SessionSpec.md @@ -0,0 +1,17 @@ +# SessionSpec +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **name** | **String** | | [optional] [default to null] | +| **appDefinition** | **String** | | [optional] [default to null] | +| **user** | **String** | | [optional] [default to null] | +| **workspace** | **String** | | [optional] [default to null] | +| **sessionSecret** | **String** | | [optional] [default to null] | +| **options** | **Map** | | [optional] [default to null] | +| **envVars** | **Map** | | [optional] [default to null] | +| **envVarsFromConfigMaps** | **List** | | [optional] [default to null] | +| **envVarsFromSecrets** | **List** | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/documentation/api/Models/SessionStartRequest.md b/documentation/api/Models/SessionStartRequest.md new file mode 100644 index 000000000..4e9d4dce8 --- /dev/null +++ b/documentation/api/Models/SessionStartRequest.md @@ -0,0 +1,14 @@ +# SessionStartRequest +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **appId** | **String** | The App Id of this Theia Cloud instance. Request without a matching Id will be denied. | [default to null] | +| **user** | **String** | The user identification, usually the email address. | [default to null] | +| **appDefinition** | **String** | The app to launch. | [default to null] | +| **workspaceName** | **String** | The name of the workspace to mount/create. | [optional] [default to null] | +| **timeout** | **Integer** | Number of minutes to wait for session launch. Default is 3 Minutes. | [optional] [default to null] | +| **env** | [**EnvironmentVars**](EnvironmentVars.md) | Environment variables | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/documentation/api/Models/SessionStopRequest.md b/documentation/api/Models/SessionStopRequest.md new file mode 100644 index 000000000..c2b78d25f --- /dev/null +++ b/documentation/api/Models/SessionStopRequest.md @@ -0,0 +1,11 @@ +# SessionStopRequest +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **appId** | **String** | The App Id of this Theia Cloud instance. Request without a matching Id will be denied. | [default to null] | +| **user** | **String** | The user identification, usually the email address. | [default to null] | +| **sessionName** | **String** | The name of the session to stop. | [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/documentation/api/Models/UserWorkspace.md b/documentation/api/Models/UserWorkspace.md new file mode 100644 index 000000000..cacae143d --- /dev/null +++ b/documentation/api/Models/UserWorkspace.md @@ -0,0 +1,13 @@ +# UserWorkspace +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **name** | **String** | The name of the workspace | [default to null] | +| **label** | **String** | The label of the workspace | [default to null] | +| **appDefinition** | **String** | The app this workspace was used with. | [optional] [default to null] | +| **user** | **String** | The user identification, usually the email address. | [default to null] | +| **active** | **Boolean** | Whether the workspace is in use at the moment. | [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/documentation/api/Models/WorkspaceCreationRequest.md b/documentation/api/Models/WorkspaceCreationRequest.md new file mode 100644 index 000000000..91e6e4140 --- /dev/null +++ b/documentation/api/Models/WorkspaceCreationRequest.md @@ -0,0 +1,12 @@ +# WorkspaceCreationRequest +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **appId** | **String** | The App Id of this Theia Cloud instance. Request without a matching Id will be denied. | [default to null] | +| **user** | **String** | The user identification, usually the email address. | [default to null] | +| **appDefinition** | **String** | The app this workspace will be used with. | [optional] [default to null] | +| **label** | **String** | The label of the workspace | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/documentation/api/Models/WorkspaceDeletionRequest.md b/documentation/api/Models/WorkspaceDeletionRequest.md new file mode 100644 index 000000000..74f32c236 --- /dev/null +++ b/documentation/api/Models/WorkspaceDeletionRequest.md @@ -0,0 +1,11 @@ +# WorkspaceDeletionRequest +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **appId** | **String** | The App Id of this Theia Cloud instance. Request without a matching Id will be denied. | [default to null] | +| **user** | **String** | The user identification, usually the email address. | [default to null] | +| **workspaceName** | **String** | The name of the workspace to delete. | [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/documentation/api/Models/WorkspaceListRequest.md b/documentation/api/Models/WorkspaceListRequest.md new file mode 100644 index 000000000..a90d69f0d --- /dev/null +++ b/documentation/api/Models/WorkspaceListRequest.md @@ -0,0 +1,10 @@ +# WorkspaceListRequest +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **appId** | **String** | The App Id of this Theia Cloud instance. Request without a matching Id will be denied. | [default to null] | +| **user** | **String** | The user identification, usually the email address. | [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/documentation/api/README.md b/documentation/api/README.md new file mode 100644 index 000000000..74bc08977 --- /dev/null +++ b/documentation/api/README.md @@ -0,0 +1,64 @@ +# Documentation for Theia Cloud API + + +## Documentation for API Endpoints + +All URIs are relative to *http://localhost* + +| Class | Method | HTTP request | Description | +|------------ | ------------- | ------------- | -------------| +| *AppDefinitionAdminResourceApi* | [**serviceAdminAppdefinitionAppDefinitionNamePatch**](Apis/AppDefinitionAdminResourceApi.md#serviceadminappdefinitionappdefinitionnamepatch) | **PATCH** /service/admin/appdefinition/{appDefinitionName} | Updates an app definition | +| *AppDefinitionResourceApi* | [**serviceAppdefinitionAppIdGet**](Apis/AppDefinitionResourceApi.md#serviceappdefinitionappidget) | **GET** /service/appdefinition/{appId} | List app definitions | +| *RootAdminResourceApi* | [**serviceAdminAppIdGet**](Apis/RootAdminResourceApi.md#serviceadminappidget) | **GET** /service/admin/{appId} | Admin Ping | +| *RootResourceApi* | [**serviceAppIdGet**](Apis/RootResourceApi.md#serviceappidget) | **GET** /service/{appId} | Ping | +*RootResourceApi* | [**servicePost**](Apis/RootResourceApi.md#servicepost) | **POST** /service | Launch Session | +| *SessionResourceApi* | [**serviceSessionAppIdUserGet**](Apis/SessionResourceApi.md#servicesessionappiduserget) | **GET** /service/session/{appId}/{user} | List sessions | +*SessionResourceApi* | [**serviceSessionDelete**](Apis/SessionResourceApi.md#servicesessiondelete) | **DELETE** /service/session | Stop session | +*SessionResourceApi* | [**serviceSessionPatch**](Apis/SessionResourceApi.md#servicesessionpatch) | **PATCH** /service/session | Report session activity | +*SessionResourceApi* | [**serviceSessionPerformanceAppIdSessionNameGet**](Apis/SessionResourceApi.md#servicesessionperformanceappidsessionnameget) | **GET** /service/session/performance/{appId}/{sessionName} | Get performance metrics | +*SessionResourceApi* | [**serviceSessionPost**](Apis/SessionResourceApi.md#servicesessionpost) | **POST** /service/session | Start a new session | +| *WorkspaceResourceApi* | [**serviceWorkspaceAppIdUserGet**](Apis/WorkspaceResourceApi.md#serviceworkspaceappiduserget) | **GET** /service/workspace/{appId}/{user} | List workspaces | +*WorkspaceResourceApi* | [**serviceWorkspaceDelete**](Apis/WorkspaceResourceApi.md#serviceworkspacedelete) | **DELETE** /service/workspace | Delete workspace | +*WorkspaceResourceApi* | [**serviceWorkspacePost**](Apis/WorkspaceResourceApi.md#serviceworkspacepost) | **POST** /service/workspace | Create workspace | + + + +## Documentation for Models + + - [ActivityTracker](./Models/ActivityTracker.md) + - [AppDefinition](./Models/AppDefinition.md) + - [AppDefinitionListRequest](./Models/AppDefinitionListRequest.md) + - [AppDefinitionSpec](./Models/AppDefinitionSpec.md) + - [AppDefinitionStatus](./Models/AppDefinitionStatus.md) + - [AppDefinitionUpdateRequest](./Models/AppDefinitionUpdateRequest.md) + - [EnvironmentVars](./Models/EnvironmentVars.md) + - [LaunchRequest](./Models/LaunchRequest.md) + - [ManagedFieldsEntry](./Models/ManagedFieldsEntry.md) + - [Monitor](./Models/Monitor.md) + - [ObjectMeta](./Models/ObjectMeta.md) + - [OwnerReference](./Models/OwnerReference.md) + - [PingRequest](./Models/PingRequest.md) + - [SessionActivityRequest](./Models/SessionActivityRequest.md) + - [SessionListRequest](./Models/SessionListRequest.md) + - [SessionPerformance](./Models/SessionPerformance.md) + - [SessionPerformanceRequest](./Models/SessionPerformanceRequest.md) + - [SessionSpec](./Models/SessionSpec.md) + - [SessionStartRequest](./Models/SessionStartRequest.md) + - [SessionStopRequest](./Models/SessionStopRequest.md) + - [UserWorkspace](./Models/UserWorkspace.md) + - [WorkspaceCreationRequest](./Models/WorkspaceCreationRequest.md) + - [WorkspaceDeletionRequest](./Models/WorkspaceDeletionRequest.md) + - [WorkspaceListRequest](./Models/WorkspaceListRequest.md) + + + +## Documentation for Authorization + + +### SecurityScheme + +- **Type**: OAuth +- **Flow**: implicit +- **Authorization URL**: +- **Scopes**: N/A + diff --git a/documentation/openapi.json b/documentation/openapi.json index f64d016ab..6f92f03fe 100644 --- a/documentation/openapi.json +++ b/documentation/openapi.json @@ -1,13 +1,687 @@ { - "openapi": "3.0.3", + "openapi": "3.1.0", + "components": { + "schemas": { + "ActivityTracker": { + "type": "object", + "properties": { + "timeoutAfter": { + "type": "integer", + "format": "int32" + }, + "notifyAfter": { + "type": "integer", + "format": "int32" + } + } + }, + "AppDefinition": { + "type": "object", + "properties": { + "apiVersion": { + "type": "string", + "readOnly": true + }, + "kind": { + "type": "string", + "readOnly": true + }, + "metadata": { + "$ref": "#/components/schemas/ObjectMeta" + }, + "spec": { + "$ref": "#/components/schemas/AppDefinitionSpec" + }, + "status": { + "$ref": "#/components/schemas/AppDefinitionStatus" + }, + "singular": { + "type": "string", + "writeOnly": true + }, + "crdName": { + "type": "string" + }, + "scope": { + "type": "string", + "writeOnly": true + }, + "plural": { + "type": "string", + "writeOnly": true + }, + "served": { + "type": "boolean", + "writeOnly": true + }, + "storage": { + "type": "boolean", + "writeOnly": true + }, + "deprecated": { + "type": "boolean", + "writeOnly": true + }, + "deprecationWarning": { + "type": "string", + "writeOnly": true + } + } + }, + "AppDefinitionListRequest": { + "type": "object", + "required": ["appId"], + "description": "A request to list available app definitions.", + "properties": { + "appId": { + "type": "string", + "description": "The App Id of this Theia Cloud instance. Request without a matching Id will be denied." + } + } + }, + "AppDefinitionSpec": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "image": { + "type": "string" + }, + "imagePullPolicy": { + "type": "string" + }, + "pullSecret": { + "type": "string" + }, + "uid": { + "type": "integer", + "format": "int32" + }, + "port": { + "type": "integer", + "format": "int32" + }, + "ingressname": { + "type": "string" + }, + "minInstances": { + "type": "integer", + "format": "int32" + }, + "maxInstances": { + "type": "integer", + "format": "int32" + }, + "timeout": { + "type": "integer", + "format": "int32" + }, + "requestsMemory": { + "type": "string" + }, + "requestsCpu": { + "type": "string" + }, + "limitsMemory": { + "type": "string" + }, + "limitsCpu": { + "type": "string" + }, + "downlinkLimit": { + "type": "integer", + "format": "int32" + }, + "uplinkLimit": { + "type": "integer", + "format": "int32" + }, + "mountPath": { + "type": "string" + }, + "monitor": { + "$ref": "#/components/schemas/Monitor" + }, + "options": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "ingressHostnamePrefixes": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "AppDefinitionStatus": { + "type": "object", + "properties": { + "operatorStatus": { + "type": "string" + }, + "operatorMessage": { + "type": "string" + } + } + }, + "AppDefinitionUpdateRequest": { + "type": "object", + "required": ["appId"], + "properties": { + "appId": { + "type": "string", + "description": "The App Id of this Theia Cloud instance. Request without a matching Id will be denied." + }, + "minInstances": { + "type": "integer", + "format": "int32", + "description": "The minimum number of instances to run." + }, + "maxInstances": { + "type": "integer", + "format": "int32", + "description": "The maximum number of instances to run." + } + } + }, + "EnvironmentVars": { + "type": "object", + "description": "An object to hold all the ways environment variables can be passed. Not to be used by itself.", + "properties": { + "fromMap": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Map of environment variables to be passed to Deployment. Ignored if Theia applications are started eagerly. Empty by default." + }, + "fromConfigMaps": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of ConfigMaps (by name) containing environment variables to be passed to Deployment as envFrom.configMapRef. Ignored if Theia applications are started eagerly. Empty by default." + }, + "fromSecrets": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of Secrets (by name) containing environment variables to be passed to Deployment as envFrom.secretRef. Ignored if Theia applications are started eagerly. Empty by default." + } + } + }, + "FieldsV1": { + "type": "object" + }, + "LaunchRequest": { + "type": "object", + "required": ["appId", "user"], + "description": "A request to launch a new session.", + "properties": { + "appId": { + "type": "string", + "description": "The App Id of this Theia Cloud instance. Request without a matching Id will be denied." + }, + "user": { + "type": "string", + "description": "The user identification, usually the email address." + }, + "appDefinition": { + "type": "string", + "description": "The app to launch. Needs to be set if a new or ephemeral session should be launched. For an existing workspace the last app definition will be used if none is given." + }, + "workspaceName": { + "type": "string", + "description": "The name of the workspace to mount/create. Needs to be set if an existing workspace should be launched." + }, + "label": { + "type": "string", + "description": "The label of the workspace to mount/create. If no label is given, a default label will be generated." + }, + "ephemeral": { + "type": "boolean", + "description": "If true no workspace will be created for the session." + }, + "timeout": { + "type": "integer", + "format": "int32", + "description": "Number of minutes to wait for session launch. Default is 3 Minutes." + }, + "env": { + "$ref": "#/components/schemas/EnvironmentVars", + "type": "object", + "description": "Environment variables" + } + } + }, + "ManagedFieldsEntry": { + "type": "object", + "properties": { + "apiVersion": { + "type": "string" + }, + "fieldsType": { + "type": "string" + }, + "fieldsV1": { + "$ref": "#/components/schemas/FieldsV1" + }, + "manager": { + "type": "string" + }, + "operation": { + "type": "string" + }, + "subresource": { + "type": "string" + }, + "time": { + "type": "string" + } + } + }, + "Monitor": { + "type": "object", + "properties": { + "port": { + "type": "integer", + "format": "int32" + }, + "activityTracker": { + "$ref": "#/components/schemas/ActivityTracker" + } + } + }, + "ObjectMeta": { + "type": "object", + "properties": { + "annotations": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "creationTimestamp": { + "type": "string" + }, + "deletionGracePeriodSeconds": { + "type": "integer", + "format": "int64" + }, + "deletionTimestamp": { + "type": "string" + }, + "finalizers": { + "type": "array", + "items": { + "type": "string" + } + }, + "generateName": { + "type": "string" + }, + "generation": { + "type": "integer", + "format": "int64" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "managedFields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ManagedFieldsEntry" + } + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "ownerReferences": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OwnerReference" + } + }, + "resourceVersion": { + "type": "string" + }, + "selfLink": { + "type": "string" + }, + "uid": { + "type": "string" + } + } + }, + "OwnerReference": { + "type": "object", + "properties": { + "apiVersion": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "blockOwnerDeletion": { + "type": "boolean" + }, + "controller": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "uid": { + "type": "string" + } + } + }, + "PingRequest": { + "type": "object", + "required": ["appId"], + "description": "Request to ping the availability of the service.", + "properties": { + "appId": { + "type": "string", + "description": "The App Id of this Theia Cloud instance. Request without a matching Id will be denied." + } + } + }, + "SessionActivityRequest": { + "type": "object", + "required": ["appId", "sessionName"], + "description": "A request to report activity for a running session.", + "properties": { + "appId": { + "type": "string", + "description": "The App Id of this Theia Cloud instance. Request without a matching Id will be denied." + }, + "sessionName": { + "type": "string", + "description": "The name of the session for which activity is reported." + } + } + }, + "SessionListRequest": { + "type": "object", + "required": ["appId", "user"], + "description": "A request to list the sessions of a user.", + "properties": { + "appId": { + "type": "string", + "description": "The App Id of this Theia Cloud instance. Request without a matching Id will be denied." + }, + "user": { + "type": "string", + "description": "The user identification, usually the email address." + } + } + }, + "SessionPerformance": { + "type": "object", + "required": ["cpuAmount", "cpuFormat", "memoryAmount", "memoryFormat"], + "description": "Description of the performance of a session", + "properties": { + "cpuAmount": { + "type": "string", + "description": "Used CPU amount of the workspace" + }, + "cpuFormat": { + "type": "string", + "description": "Used CPU format of the workspace" + }, + "memoryAmount": { + "type": "string", + "description": "Used memory amount of the workspace" + }, + "memoryFormat": { + "type": "string", + "description": "Used memory format of the workspace" + } + } + }, + "SessionPerformanceRequest": { + "type": "object", + "required": ["appId", "sessionName"], + "description": "A request to list the sessions of a user.", + "properties": { + "appId": { + "type": "string", + "description": "The App Id of this Theia Cloud instance. Request without a matching Id will be denied." + }, + "sessionName": { + "type": "string", + "description": "The name of the session" + } + } + }, + "SessionSetConfigValueRequest": { + "type": "object", + "required": ["appId", "key", "value"], + "properties": { + "appId": { + "type": "string", + "description": "The App Id of this Theia Cloud instance. Request without a matching Id will be denied." + }, + "key": { + "type": "string", + "description": "The configuration key" + }, + "value": { + "type": ["string", "null"], + "description": "The configuration value" + } + } + }, + "SessionSpec": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "appDefinition": { + "type": "string" + }, + "user": { + "type": "string" + }, + "workspace": { + "type": "string" + }, + "sessionSecret": { + "type": "string" + }, + "options": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "envVars": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "envVarsFromConfigMaps": { + "type": "array", + "items": { + "type": "string" + } + }, + "envVarsFromSecrets": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "SessionStartRequest": { + "type": "object", + "required": ["appId", "user", "appDefinition"], + "description": "A request to start a session", + "properties": { + "appId": { + "type": "string", + "description": "The App Id of this Theia Cloud instance. Request without a matching Id will be denied." + }, + "user": { + "type": "string", + "description": "The user identification, usually the email address." + }, + "appDefinition": { + "type": "string", + "description": "The app to launch." + }, + "workspaceName": { + "type": "string", + "description": "The name of the workspace to mount/create." + }, + "timeout": { + "type": "integer", + "format": "int32", + "description": "Number of minutes to wait for session launch. Default is 3 Minutes." + }, + "env": { + "$ref": "#/components/schemas/EnvironmentVars", + "type": "object", + "description": "Environment variables" + } + } + }, + "SessionStopRequest": { + "type": "object", + "required": ["appId", "user", "sessionName"], + "description": "A request to stop a session", + "properties": { + "appId": { + "type": "string", + "description": "The App Id of this Theia Cloud instance. Request without a matching Id will be denied." + }, + "user": { + "type": "string", + "description": "The user identification, usually the email address." + }, + "sessionName": { + "type": "string", + "description": "The name of the session to stop." + } + } + }, + "UserWorkspace": { + "type": "object", + "required": ["name", "label", "user", "active"], + "description": "Description of a user workspace", + "properties": { + "name": { + "type": "string", + "description": "The name of the workspace" + }, + "label": { + "type": "string", + "description": "The label of the workspace" + }, + "appDefinition": { + "type": "string", + "description": "The app this workspace was used with." + }, + "user": { + "type": "string", + "description": "The user identification, usually the email address." + }, + "active": { + "type": "boolean", + "description": "Whether the workspace is in use at the moment." + } + } + }, + "WorkspaceCreationRequest": { + "type": "object", + "required": ["appId", "user"], + "description": "Request to create a new workspace.", + "properties": { + "appId": { + "type": "string", + "description": "The App Id of this Theia Cloud instance. Request without a matching Id will be denied." + }, + "user": { + "type": "string", + "description": "The user identification, usually the email address." + }, + "appDefinition": { + "type": "string", + "description": "The app this workspace will be used with." + }, + "label": { + "type": "string", + "description": "The label of the workspace" + } + } + }, + "WorkspaceDeletionRequest": { + "type": "object", + "required": ["appId", "user", "workspaceName"], + "description": "Request to delete a workspace", + "properties": { + "appId": { + "type": "string", + "description": "The App Id of this Theia Cloud instance. Request without a matching Id will be denied." + }, + "user": { + "type": "string", + "description": "The user identification, usually the email address." + }, + "workspaceName": { + "type": "string", + "description": "The name of the workspace to delete." + } + } + }, + "WorkspaceListRequest": { + "type": "object", + "required": ["appId", "user"], + "description": "Request to list workspaces of a user.", + "properties": { + "appId": { + "type": "string", + "description": "The App Id of this Theia Cloud instance. Request without a matching Id will be denied." + }, + "user": { + "type": "string", + "description": "The user identification, usually the email address." + } + } + } + }, + "securitySchemes": { + "SecurityScheme": { + "type": "oauth2", + "description": "Authentication", + "flows": { + "implicit": {} + } + } + } + }, "info": { "title": "Theia Cloud API", - "version": "0.12.0" + "version": "1.2.0" }, "paths": { "/service": { "post": { - "tags": ["Root Resource"], "summary": "Launch Session", "description": "Launches a session and creates a workspace if required. Responds with the URL of the launched session.", "requestBody": { @@ -17,7 +691,8 @@ "$ref": "#/components/schemas/LaunchRequest" } } - } + }, + "required": true }, "responses": { "200": { @@ -30,13 +705,14 @@ } } }, - "403": { - "description": "Not Allowed" - }, "401": { "description": "Not Authorized" + }, + "403": { + "description": "Not Allowed" } }, + "tags": ["Root Resource"], "security": [ { "SecurityScheme": [] @@ -44,94 +720,60 @@ ] } }, - "/service/session": { - "post": { - "tags": ["Session Resource"], - "summary": "Start a new session", - "description": "Starts a new session for an existing workspace and responds with the URL of the started session.", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SessionStartRequest" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "text/plain": { - "schema": { - "type": "string" - } - } - } - }, - "403": { - "description": "Not Allowed" - }, - "401": { - "description": "Not Authorized" - } - }, - "security": [ + "/service/admin/appdefinition/{appDefinitionName}": { + "patch": { + "summary": "Updates an app definition", + "description": "Updates an app definition's properties. Allowed properties to update are defined by AppDefinitionUpdateRequest.", + "parameters": [ { - "SecurityScheme": [] + "description": "The K8S resource name of the app definition to update.", + "name": "appDefinitionName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } } - ] - }, - "delete": { - "tags": ["Session Resource"], - "summary": "Stop session", - "description": "Stops a session.", + ], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionStopRequest" + "$ref": "#/components/schemas/AppDefinitionUpdateRequest" } } - } + }, + "required": true }, "responses": { "200": { "description": "OK", "content": { - "text/plain": { + "application/json": { "schema": { - "type": "boolean" + "$ref": "#/components/schemas/AppDefinition" } } } - }, - "403": { - "description": "Not Allowed" - }, - "401": { - "description": "Not Authorized" } }, - "security": [ + "tags": ["App Definition Admin Resource"] + } + }, + "/service/admin/{appId}": { + "get": { + "summary": "Admin Ping", + "description": "Replies with success if the service is available and the user an admin.", + "parameters": [ { - "SecurityScheme": [] - } - ] - }, - "patch": { - "tags": ["Session Resource"], - "summary": "Report session activity", - "description": "Updates the last activity timestamp for a session to monitor activity.", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SessionActivityRequest" - } + "name": "appId", + "in": "path", + "required": true, + "schema": { + "type": "string" } } - }, + ], "responses": { "200": { "description": "OK", @@ -142,26 +784,15 @@ } } } - }, - "403": { - "description": "Not Allowed" - }, - "401": { - "description": "Not Authorized" } }, - "security": [ - { - "SecurityScheme": [] - } - ] + "tags": ["Root Admin Resource"] } }, - "/service/session/performance/{appId}/{sessionName}": { + "/service/appdefinition/{appId}": { "get": { - "tags": ["Session Resource"], - "summary": "Get performance metrics", - "description": "Returns the current CPU and memory usage of the session's pod.", + "summary": "List app definitions", + "description": "List available app definitions.", "parameters": [ { "name": "appId", @@ -170,14 +801,6 @@ "schema": { "type": "string" } - }, - { - "name": "sessionName", - "in": "path", - "required": true, - "schema": { - "type": "string" - } } ], "responses": { @@ -186,18 +809,22 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionPerformance" + "type": "array", + "items": { + "$ref": "#/components/schemas/AppDefinitionSpec" + } } } } }, - "403": { - "description": "Not Allowed" - }, "401": { "description": "Not Authorized" + }, + "403": { + "description": "Not Allowed" } }, + "tags": ["App Definition Resource"], "security": [ { "SecurityScheme": [] @@ -205,107 +832,95 @@ ] } }, - "/service/session/{appId}/{user}": { - "get": { - "tags": ["Session Resource"], - "summary": "List sessions", - "description": "List sessions of a user.", - "parameters": [ - { - "name": "appId", - "in": "path", - "required": true, - "schema": { - "type": "string" + "/service/session": { + "delete": { + "summary": "Stop session", + "description": "Stops a session.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionStopRequest" + } } }, - { - "name": "user", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], + "required": true + }, "responses": { "200": { "description": "OK", "content": { - "application/json": { + "text/plain": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SessionSpec" - } + "type": "boolean" } } } }, - "403": { - "description": "Not Allowed" - }, "401": { "description": "Not Authorized" + }, + "403": { + "description": "Not Allowed" } }, + "tags": ["Session Resource"], "security": [ { "SecurityScheme": [] } ] - } - }, - "/service/workspace": { + }, "post": { - "tags": ["Workspace Resource"], - "summary": "Create workspace", - "description": "Creates a new workspace for a user.", + "summary": "Start a new session", + "description": "Starts a new session for an existing workspace and responds with the URL of the started session.", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WorkspaceCreationRequest" + "$ref": "#/components/schemas/SessionStartRequest" } } - } + }, + "required": true }, "responses": { "200": { "description": "OK", "content": { - "application/json": { + "text/plain": { "schema": { - "$ref": "#/components/schemas/UserWorkspace" + "type": "string" } } } }, - "403": { - "description": "Not Allowed" - }, "401": { "description": "Not Authorized" + }, + "403": { + "description": "Not Allowed" } }, + "tags": ["Session Resource"], "security": [ { "SecurityScheme": [] } ] }, - "delete": { - "tags": ["Workspace Resource"], - "summary": "Delete workspace", - "description": "Deletes a workspace.", + "patch": { + "summary": "Report session activity", + "description": "Updates the last activity timestamp for a session to monitor activity.", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WorkspaceDeletionRequest" + "$ref": "#/components/schemas/SessionActivityRequest" } } - } + }, + "required": true }, "responses": { "200": { @@ -318,13 +933,14 @@ } } }, - "403": { - "description": "Not Allowed" - }, "401": { "description": "Not Authorized" + }, + "403": { + "description": "Not Allowed" } }, + "tags": ["Session Resource"], "security": [ { "SecurityScheme": [] @@ -332,11 +948,10 @@ ] } }, - "/service/workspace/{appId}/{user}": { + "/service/session/performance/{appId}/{sessionName}": { "get": { - "tags": ["Workspace Resource"], - "summary": "List workspaces", - "description": "Lists the workspaces of a user.", + "summary": "Get performance metrics", + "description": "Returns the current CPU and memory usage of the session's pod.", "parameters": [ { "name": "appId", @@ -347,7 +962,7 @@ } }, { - "name": "user", + "name": "sessionName", "in": "path", "required": true, "schema": { @@ -361,21 +976,19 @@ "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/UserWorkspace" - } + "$ref": "#/components/schemas/SessionPerformance" } } } }, - "403": { - "description": "Not Allowed" - }, "401": { "description": "Not Authorized" + }, + "403": { + "description": "Not Allowed" } }, + "tags": ["Session Resource"], "security": [ { "SecurityScheme": [] @@ -383,11 +996,10 @@ ] } }, - "/service/{appId}": { + "/service/session/{appId}/{user}": { "get": { - "tags": ["Root Resource"], - "summary": "Ping", - "description": "Replies if the service is available.", + "summary": "List sessions", + "description": "List sessions of a user.", "parameters": [ { "name": "appId", @@ -396,379 +1008,268 @@ "schema": { "type": "string" } + }, + { + "name": "user", + "in": "path", + "required": true, + "schema": { + "type": "string" + } } ], "responses": { "200": { "description": "OK", "content": { - "text/plain": { + "application/json": { "schema": { - "type": "boolean" + "type": "array", + "items": { + "$ref": "#/components/schemas/SessionSpec" + } } } } }, - "403": { - "description": "Not Allowed" - }, "401": { "description": "Not Authorized" + }, + "403": { + "description": "Not Allowed" } }, + "tags": ["Session Resource"], "security": [ { "SecurityScheme": [] } ] } - } - }, - "components": { - "schemas": { - "EnvironmentVars": { - "description": "An object to hold all the ways environment variables can be passed. Not to be used by itself.", - "type": "object", - "properties": { - "fromMap": { - "description": "Map of environment variables to be passed to Deployment. Ignored if Theia applications are started eagerly. Empty by default.", - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "fromConfigMaps": { - "description": "List of ConfigMaps (by name) containing environment variables to be passed to Deployment as envFrom.configMapRef. Ignored if Theia applications are started eagerly. Empty by default.", - "type": "array", - "items": { - "type": "string" - } - }, - "fromSecrets": { - "description": "List of Secrets (by name) containing environment variables to be passed to Deployment as envFrom.secretRef. Ignored if Theia applications are started eagerly. Empty by default.", - "type": "array", - "items": { + }, + "/service/session/{session}/config": { + "post": { + "summary": "Set config value", + "description": "Sets a config value in the config store if it is available. This requires the @eclipse-theiacloud/config-store Theia extension to be present in the application.", + "parameters": [ + { + "description": "The name of the session", + "name": "session", + "in": "path", + "required": true, + "schema": { "type": "string" } } - } - }, - "LaunchRequest": { - "description": "A request to launch a new session.", - "required": ["appId", "user"], - "type": "object", - "properties": { - "appId": { - "description": "The App Id of this Theia Cloud instance. Request without a matching Id will be denied.", - "type": "string" - }, - "user": { - "description": "The user identification, usually the email address.", - "type": "string" - }, - "appDefinition": { - "description": "The app to launch. Needs to be set if a new or ephemeral session should be launched. For an existing workspace the last app definition will be used if none is given.", - "type": "string" - }, - "workspaceName": { - "description": "The name of the workspace to mount/create. Needs to be set if an existing workspace should be launched.", - "type": "string" - }, - "label": { - "description": "The label of the workspace to mount/create. If no label is given, a default label will be generated.", - "type": "string" - }, - "ephemeral": { - "description": "If true no workspace will be created for the session.", - "type": "boolean" - }, - "timeout": { - "format": "int32", - "description": "Number of minutes to wait for session launch. Default is 3 Minutes.", - "type": "integer" - }, - "env": { - "description": "Environment variables", - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/EnvironmentVars" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionSetConfigValueRequest" } - ] - } - } - }, - "PingRequest": { - "description": "Request to ping the availability of the service.", - "required": ["appId"], - "type": "object", - "properties": { - "appId": { - "description": "The App Id of this Theia Cloud instance. Request without a matching Id will be denied.", - "type": "string" - } - } - }, - "SessionActivityRequest": { - "description": "A request to report activity for a running session.", - "required": ["appId", "sessionName"], - "type": "object", - "properties": { - "appId": { - "description": "The App Id of this Theia Cloud instance. Request without a matching Id will be denied.", - "type": "string" - }, - "sessionName": { - "description": "The name of the session for which activity is reported.", - "type": "string" - } - } - }, - "SessionListRequest": { - "description": "A request to list the sessions of a user.", - "required": ["appId", "user"], - "type": "object", - "properties": { - "appId": { - "description": "The App Id of this Theia Cloud instance. Request without a matching Id will be denied.", - "type": "string" - }, - "user": { - "description": "The user identification, usually the email address.", - "type": "string" - } - } - }, - "SessionPerformance": { - "description": "Description of the performance of a session", - "required": ["cpuAmount", "cpuFormat", "memoryAmount", "memoryFormat"], - "type": "object", - "properties": { - "cpuAmount": { - "description": "Used CPU amount of the workspace", - "type": "string" - }, - "cpuFormat": { - "description": "Used CPU format of the workspace", - "type": "string" - }, - "memoryAmount": { - "description": "Used memory amount of the workspace", - "type": "string" - }, - "memoryFormat": { - "description": "Used memory format of the workspace", - "type": "string" - } - } - }, - "SessionPerformanceRequest": { - "description": "A request to list the sessions of a user.", - "required": ["appId", "sessionName"], - "type": "object", - "properties": { - "appId": { - "description": "The App Id of this Theia Cloud instance. Request without a matching Id will be denied.", - "type": "string" - }, - "sessionName": { - "description": "The name of the session", - "type": "string" - } - } - }, - "SessionSpec": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "appDefinition": { - "type": "string" - }, - "user": { - "type": "string" - }, - "workspace": { - "type": "string" - }, - "sessionSecret": { - "type": "string" - }, - "options": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "envVars": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "envVarsFromConfigMaps": { - "type": "array", - "items": { - "type": "string" } }, - "envVarsFromSecrets": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "SessionStartRequest": { - "description": "A request to start a session", - "required": ["appId", "user", "appDefinition"], - "type": "object", - "properties": { - "appId": { - "description": "The App Id of this Theia Cloud instance. Request without a matching Id will be denied.", - "type": "string" - }, - "user": { - "description": "The user identification, usually the email address.", - "type": "string" + "required": true + }, + "responses": { + "204": { + "description": "Config value set successfully." }, - "appDefinition": { - "description": "The app to launch.", - "type": "string" + "403": { + "description": "User does not own the session." }, - "workspaceName": { - "description": "The name of the workspace to mount/create.", - "type": "string" + "474": { + "description": "Session not found." }, - "timeout": { - "format": "int32", - "description": "Number of minutes to wait for session launch. Default is 3 Minutes.", - "type": "integer" + "500": { + "description": "Internal server error while setting config value or other unexpected errors." }, - "env": { - "description": "Environment variables", - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/EnvironmentVars" - } - ] - } - } - }, - "SessionStopRequest": { - "description": "A request to stop a session", - "required": ["appId", "user", "sessionName"], - "type": "object", - "properties": { - "appId": { - "description": "The App Id of this Theia Cloud instance. Request without a matching Id will be denied.", - "type": "string" + "580": { + "description": "Config store not available." }, - "user": { - "description": "The user identification, usually the email address.", - "type": "string" + "401": { + "description": "Not Authorized" }, - "sessionName": { - "description": "The name of the session to stop.", - "type": "string" + "403": { + "description": "Not Allowed" } - } - }, - "UserWorkspace": { - "description": "Description of a user workspace", - "required": ["name", "label", "user", "active"], - "type": "object", - "properties": { - "name": { - "description": "The name of the workspace", - "type": "string" - }, - "label": { - "description": "The label of the workspace", - "type": "string" + }, + "tags": ["Session Resource"], + "security": [ + { + "SecurityScheme": [] + } + ] + } + }, + "/service/workspace": { + "delete": { + "summary": "Delete workspace", + "description": "Deletes a workspace.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkspaceDeletionRequest" + } + } }, - "appDefinition": { - "description": "The app this workspace was used with.", - "type": "string" + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserWorkspace" + } + } + } }, - "user": { - "description": "The user identification, usually the email address.", - "type": "string" + "401": { + "description": "Not Authorized" }, - "active": { - "description": "Whether the workspace is in use at the moment.", - "type": "boolean" + "403": { + "description": "Not Allowed" } - } + }, + "tags": ["Workspace Resource"], + "security": [ + { + "SecurityScheme": [] + } + ] }, - "WorkspaceCreationRequest": { - "description": "Request to create a new workspace.", - "required": ["appId", "user"], - "type": "object", - "properties": { - "appId": { - "description": "The App Id of this Theia Cloud instance. Request without a matching Id will be denied.", - "type": "string" + "post": { + "summary": "Create workspace", + "description": "Creates a new workspace for a user.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkspaceCreationRequest" + } + } }, - "user": { - "description": "The user identification, usually the email address.", - "type": "string" + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserWorkspace" + } + } + } }, - "appDefinition": { - "description": "The app this workspace will be used with.", - "type": "string" + "401": { + "description": "Not Authorized" }, - "label": { - "description": "The label of the workspace", - "type": "string" + "403": { + "description": "Not Allowed" } - } - }, - "WorkspaceDeletionRequest": { - "description": "Request to delete a workspace", - "required": ["appId", "user", "workspaceName"], - "type": "object", - "properties": { - "appId": { - "description": "The App Id of this Theia Cloud instance. Request without a matching Id will be denied.", - "type": "string" - }, - "user": { - "description": "The user identification, usually the email address.", - "type": "string" + }, + "tags": ["Workspace Resource"], + "security": [ + { + "SecurityScheme": [] + } + ] + } + }, + "/service/workspace/{appId}/{user}": { + "get": { + "summary": "List workspaces", + "description": "Lists the workspaces of a user.", + "parameters": [ + { + "name": "appId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } }, - "workspaceName": { - "description": "The name of the workspace to delete.", - "type": "string" + { + "name": "user", + "in": "path", + "required": true, + "schema": { + "type": "string" + } } - } - }, - "WorkspaceListRequest": { - "description": "Request to list workspaces of a user.", - "required": ["appId", "user"], - "type": "object", - "properties": { - "appId": { - "description": "The App Id of this Theia Cloud instance. Request without a matching Id will be denied.", - "type": "string" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserWorkspace" + } + } + } + } }, - "user": { - "description": "The user identification, usually the email address.", - "type": "string" + "401": { + "description": "Not Authorized" + }, + "403": { + "description": "Not Allowed" } - } + }, + "tags": ["Workspace Resource"], + "security": [ + { + "SecurityScheme": [] + } + ] } }, - "securitySchemes": { - "SecurityScheme": { - "type": "oauth2", - "description": "Authentication", - "flows": { - "implicit": {} - } + "/service/{appId}": { + "get": { + "summary": "Ping", + "description": "Replies if the service is available.", + "parameters": [ + { + "name": "appId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "boolean" + } + } + } + }, + "401": { + "description": "Not Authorized" + }, + "403": { + "description": "Not Allowed" + } + }, + "tags": ["Root Resource"], + "security": [ + { + "SecurityScheme": [] + } + ] } } } diff --git a/documentation/operator.graphml b/documentation/operator.graphml index 51eb645d4..c3c656e57 100644 --- a/documentation/operator.graphml +++ b/documentation/operator.graphml @@ -21,7 +21,7 @@ - TheiaCloud + Theia Cloud @@ -67,7 +67,7 @@ AppDefinitionAddedHandler - <<interface>> + <<interface>> SessionAddedHandler @@ -98,7 +98,7 @@ SessionAddedHandler - LazyStartSessionAddedHandler + LazyStartSessionAddedHandler @@ -113,7 +113,7 @@ SessionAddedHandler - EagerStartSessionAddedHandler + EagerStartSessionAddedHandler @@ -143,7 +143,7 @@ SessionAddedHandler - We may have different kinds of implementations of the handlers. + We may have different kinds of implementations of the handlers. E.g. we might have handlers that start up unassigned pods already and assign the running pods to a user on session added (EagerStart) Or we could do it Che-like and spin up the pod when a new session is added @@ -174,7 +174,7 @@ IngressCreationService - Listens for app definition/sessions changes + Listens for app definition/sessions changes and delegates to handlers diff --git a/documentation/platforms/Minikube.md b/documentation/platforms/Minikube.md index e72675159..602ff8521 100644 --- a/documentation/platforms/Minikube.md +++ b/documentation/platforms/Minikube.md @@ -1,7 +1,7 @@ # Getting started with Minikube Minikube is a local Kubernetes that allows you to test and develop for Kubernetes.\ -In this guide we will show you how to install minikube and helm 3 as well as cert-manager, the cloud native certificate management, the NginX Ingress Controller and Keycloak. We will use existing installation methods for all of the Theia Cloud preqrequisites.\ +In this guide we will show you how to install minikube and helm 3 as well as cert-manager, Gateway API + Envoy Gateway, and Keycloak. We will use existing installation methods for all of the Theia Cloud prerequisites.\ Finally we will install and try Theia Cloud using helm. If you have no experience with Kubernetes/Minikube yet, we encourage you to check out some basic tutorials first, e.g. and @@ -38,7 +38,7 @@ Build the docker image as usual. ### Operator cannot set the workspace URL to the Session This usually happens when using custom host names that are mapped in a hosts definition file (e.g. `/etc/hosts` on Linux) to the local IP of Minikube. -One [known case](https://github.com/eclipsesource/theia-cloud/issues/150) occurred on MacOS with Docker Desktop: +One [known case](https://github.com/eclipse-theia/theia-cloud/issues/150) occurred on MacOS with Docker Desktop: The VM running Kubernetes does not know the hosts settings of the host operating system. Thus, the custom hostname could not be resolved in the cluster and, thus, the operator could not resolve the session pods' addresses. diff --git a/documentation/theiacloud.png b/documentation/theia-cloud.png similarity index 100% rename from documentation/theiacloud.png rename to documentation/theia-cloud.png diff --git a/java/common/maven-conf/pom.xml b/java/common/maven-conf/pom.xml index f7beecee8..8944bcae2 100644 --- a/java/common/maven-conf/pom.xml +++ b/java/common/maven-conf/pom.xml @@ -5,7 +5,7 @@ 4.0.0 org.eclipse.theia.cloud conf - 0.12.0-SNAPSHOT + 1.2.0-SNAPSHOT pom Theia Cloud Maven Configuration Common properties and configuration @@ -17,20 +17,31 @@ quarkus-bom io.quarkus.platform 7.0.0 - 2.23.1 + 2.25.1 3.7.1 - 3.13.0 - 3.4.0 - 2.2.2 - 5.11.0 - 20240303 - 4.7.6 - 1.1.4 - 1.7.36 - 6.12.1 - 3.11.3 + 3.14.0 + 3.5.3 + 2.2.5 + 5.13.4 + 5.18.0 + 20250517 + 4.7.7 + 3.0.1 + 2.0.17 + 7.3.1 + 3.25.1 + + + dash-licenses-snapshots + https://repo.eclipse.org/content/repositories/dash-licenses-snapshots/ + + true + + + + github @@ -38,7 +49,7 @@ github GitHub Packages - https://maven.pkg.github.com/eclipsesource/theia-cloud + https://maven.pkg.github.com/eclipse-theia/theia-cloud @@ -84,4 +95,4 @@ -
\ No newline at end of file + diff --git a/java/common/org.eclipse.theia.cloud.common/pom.xml b/java/common/org.eclipse.theia.cloud.common/pom.xml index 22adab06f..3af633dc6 100644 --- a/java/common/org.eclipse.theia.cloud.common/pom.xml +++ b/java/common/org.eclipse.theia.cloud.common/pom.xml @@ -11,7 +11,7 @@ org.eclipse.theia.cloud conf - 0.12.0-SNAPSHOT + 1.2.0-SNAPSHOT ../../common/maven-conf/ @@ -21,6 +21,16 @@ kubernetes-client ${kubernetes-client.version} + + io.fabric8 + kubernetes-httpclient-okhttp + ${kubernetes-client.version} + + + org.json + json + ${org.json.version} + org.apache.logging.log4j log4j-api @@ -37,6 +47,23 @@ ${junit-jupiter.version} test + + + org.mockito + mockito-core + ${mockito.version} + test + + + io.sentry + sentry + 8.0.0 + + + io.sentry + sentry-okhttp + 8.0.0 + @@ -45,8 +72,8 @@ maven-compiler-plugin ${maven-compiler-plugin.version} - 17 - 17 + 21 + 21 @@ -64,4 +91,4 @@ - \ No newline at end of file + diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/DataBridgeClient.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/DataBridgeClient.java new file mode 100644 index 000000000..7b050561c --- /dev/null +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/DataBridgeClient.java @@ -0,0 +1,48 @@ +package org.eclipse.theia.cloud.common.k8s.client; + +import org.eclipse.theia.cloud.common.k8s.resource.session.DataInjectionResponse; +import org.eclipse.theia.cloud.common.k8s.resource.session.Session; + +import java.util.Map; +import java.util.Optional; + +/** + * Client for interacting with the data bridge service running in session + * pods. + */ +public interface DataBridgeClient { + + /** + * Injects data into a session's data bridge. + * + * @param sessionName The name of the session + * @param data Map of environment variable data to inject + * @param correlationId For logging/tracing + * @return Response indicating success or failure, or empty if the session or + * bridge is unreachable + */ + Optional injectData(String sessionName, Map data, + String correlationId); + + /** + * Injects data into a session using the Session object. + * + * @param session The session object + * @param data Map of environment variable data to inject + * @param correlationId For logging/tracing + * @return Response indicating success or failure, or empty if the bridge is + * unreachable + */ + Optional injectData(Session session, Map data, + String correlationId); + + /** + * Checks if the data bridge is healthy/reachable. + * + * @param sessionName The name of the session + * @param correlationId For logging/tracing + * @return true if health check succeeds + */ + boolean healthCheck(String sessionName, String correlationId); +} + diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/DefaultDataBridgeClient.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/DefaultDataBridgeClient.java new file mode 100644 index 000000000..73be6479a --- /dev/null +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/DefaultDataBridgeClient.java @@ -0,0 +1,203 @@ +package org.eclipse.theia.cloud.common.k8s.client; + +import static org.eclipse.theia.cloud.common.util.LogMessageUtil.formatLogMessage; + +import java.io.IOException; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinition; +import org.eclipse.theia.cloud.common.k8s.resource.session.DataInjectionResponse; +import org.eclipse.theia.cloud.common.k8s.resource.session.Session; +import org.eclipse.theia.cloud.common.util.DataBridgeUtil; +import org.json.JSONObject; + +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +import io.sentry.okhttp.SentryOkHttpEventListener; +import io.sentry.okhttp.SentryOkHttpInterceptor; + +/** + * Default implementation of {@link DataBridgeClient} using OkHttp. + */ +public class DefaultDataBridgeClient implements DataBridgeClient { + + private static final Logger LOGGER = LogManager.getLogger(DefaultDataBridgeClient.class); + + private static final MediaType JSON_MEDIA_TYPE = MediaType.parse("application/json; charset=utf-8"); + + private final OkHttpClient httpClient; + private final TheiaCloudClient theiaCloudClient; + + /** + * Creates a client with default timeout settings. + * + * @param theiaCloudClient The Theia Cloud client for accessing K8s resources + */ + public DefaultDataBridgeClient(TheiaCloudClient theiaCloudClient) { + this(theiaCloudClient, createDefaultHttpClient()); + } + + /** + * Creates a client with custom HTTP client. + * + * @param theiaCloudClient The Theia Cloud client for accessing K8s resources + * @param httpClient Custom HTTP client + */ + public DefaultDataBridgeClient(TheiaCloudClient theiaCloudClient, OkHttpClient httpClient) { + this.theiaCloudClient = theiaCloudClient; + this.httpClient = httpClient; + } + + private static OkHttpClient createDefaultHttpClient() { + return new OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .writeTimeout(10, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .addInterceptor(new SentryOkHttpInterceptor()) + .eventListener(new SentryOkHttpEventListener()) + .build(); + } + + @Override + public Optional injectData(String sessionName, Map data, + String correlationId) { + Optional session = theiaCloudClient.sessions().get(sessionName); + if (session.isEmpty()) { + LOGGER.warn(formatLogMessage(correlationId, "Session not found: " + sessionName)); + return Optional.empty(); + } + return injectData(session.get(), data, correlationId); + } + + @Override + public Optional injectData(Session session, Map data, + String correlationId) { + String sessionName = session.getSpec().getName(); + + // Get the session's service IP + Optional serviceIP = theiaCloudClient.getClusterIPFromSessionName(sessionName); + + if (serviceIP.isEmpty()) { + LOGGER.warn( + formatLogMessage(correlationId, "Could not resolve service IP for session: " + sessionName)); + return Optional.empty(); + } + + // Get the app definition to find the data bridge port + String appDefName = session.getSpec().getAppDefinition(); + Optional appDef = theiaCloudClient.appDefinitions().get(appDefName); + + if (appDef.isEmpty()) { + LOGGER.warn(formatLogMessage(correlationId, "App definition not found: " + appDefName)); + return Optional.empty(); + } + + if (!DataBridgeUtil.isDataBridgeEnabled(appDef.get().getSpec())) { + LOGGER.debug(formatLogMessage(correlationId, + "Data bridge disabled for app definition: " + appDefName + ", skipping injection")); + return Optional.empty(); + } + + int port = DataBridgeUtil.getDataBridgePort(appDef.get().getSpec()); + + // Build the URL + Optional url = DataBridgeUtil.getDataInjectionURL(serviceIP.get(), port); + + if (url.isEmpty()) { + LOGGER.warn(formatLogMessage(correlationId, "Could not construct data injection URL")); + return Optional.empty(); + } + + return performInjection(url.get(), data, correlationId, sessionName); + } + + private Optional performInjection(String url, Map data, + String correlationId, String sessionName) { + LOGGER.info(formatLogMessage(correlationId, "Injecting data to session: " + sessionName + " at " + url)); + + try { + // Build request body matching the TypeScript schema + JSONObject requestBody = new JSONObject(); + JSONObject environment = new JSONObject(data); + requestBody.put("environment", environment); + + RequestBody body = RequestBody.create(requestBody.toString(), JSON_MEDIA_TYPE); + + // Build and execute request + Request request = new Request.Builder().url(url).post(body).build(); + + try (Response response = httpClient.newCall(request).execute()) { + if (response.isSuccessful()) { + LOGGER.info(formatLogMessage(correlationId, + "Successfully injected data to: " + sessionName)); + return Optional + .of(DataInjectionResponse.success("Data injected successfully")); + } else { + String errorBody = response.body() != null ? response.body().string() : "Unknown error"; + LOGGER.warn(formatLogMessage(correlationId, "Failed to inject data. Status: " + + response.code() + ", Body: " + errorBody)); + return Optional.of(DataInjectionResponse + .failure("HTTP " + response.code() + ": " + errorBody)); + } + } + } catch (IOException e) { + LOGGER.error( + formatLogMessage(correlationId, "IOException while injecting data to: " + sessionName), e); + return Optional.of(DataInjectionResponse.failure("Network error: " + e.getMessage())); + } + } + + @Override + public boolean healthCheck(String sessionName, String correlationId) { + Optional session = theiaCloudClient.sessions().get(sessionName); + if (session.isEmpty()) { + return false; + } + + Optional serviceIP = theiaCloudClient.getClusterIPFromSessionName(sessionName); + + if (serviceIP.isEmpty()) { + return false; + } + + // Get port from app definition + String appDefName = session.get().getSpec().getAppDefinition(); + Optional appDef = theiaCloudClient.appDefinitions().get(appDefName); + + if (appDef.isEmpty() || !DataBridgeUtil.isDataBridgeEnabled(appDef.get().getSpec())) { + return false; + } + + int port = DataBridgeUtil.getDataBridgePort(appDef.get().getSpec()); + + Optional url = DataBridgeUtil.getHealthCheckURL(serviceIP.get(), port); + + if (url.isEmpty()) { + return false; + } + + try { + Request request = new Request.Builder().url(url.get()).get().build(); + + try (Response response = httpClient.newCall(request).execute()) { + boolean healthy = response.isSuccessful(); + if (healthy) { + LOGGER.debug(formatLogMessage(correlationId, "Health check passed for session: " + sessionName)); + } + return healthy; + } + } catch (IOException e) { + LOGGER.debug(formatLogMessage(correlationId, "Health check failed for session: " + sessionName), e); + return false; + } + } +} + diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/DefaultSessionResourceClient.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/DefaultSessionResourceClient.java index 517fdef4b..d75779c49 100644 --- a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/DefaultSessionResourceClient.java +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/DefaultSessionResourceClient.java @@ -16,6 +16,8 @@ package org.eclipse.theia.cloud.common.k8s.client; import java.time.Instant; +import java.util.HashMap; +import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -23,6 +25,7 @@ import org.eclipse.theia.cloud.common.k8s.resource.session.SessionSpec; import org.eclipse.theia.cloud.common.k8s.resource.session.SessionSpecResourceList; import org.eclipse.theia.cloud.common.k8s.resource.session.SessionStatus; +import org.eclipse.theia.cloud.common.tracing.TraceContext; import org.eclipse.theia.cloud.common.util.TheiaCloudError; import io.fabric8.kubernetes.api.model.ObjectMeta; @@ -46,10 +49,16 @@ public Session create(String correlationId, SessionSpec spec) { ObjectMeta metadata = new ObjectMeta(); metadata.setName(spec.getName()); + + // Inject trace context into annotations for operator to continue trace + Map annotations = new HashMap<>(); + TraceContext.fromCurrent().ifPresent(ctx -> ctx.mergeInto(annotations)); + if (!annotations.isEmpty()) { + metadata.setAnnotations(annotations); + } + session.setMetadata(metadata); - updateStatus(correlationId, session, status -> status.setLastActivity(Instant.now().toEpochMilli())); - info(correlationId, "Create Session " + session.getSpec()); return operation().resource(session).create(); } diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/ResourceClient.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/ResourceClient.java index d470ef5b9..dc1fa7e12 100644 --- a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/ResourceClient.java +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/ResourceClient.java @@ -58,6 +58,8 @@ default T edit(String correlationId, String name, Consumer consumer) { info(correlationId, "Edit " + name); Resource resource = resource(name); if (resource.get() == null) { + warn(correlationId, "Resource not found! Could not edit " + name + + ". Was this called before the resource has been created?"); return null; } return resource.edit(JavaUtil.toUnary(consumer)); @@ -67,6 +69,8 @@ default T editStatus(String correlationId, String name, Consumer consumer) { trace(correlationId, "Edit status of " + name); Resource resource = resource(name); if (resource.get() == null) { + warn(correlationId, "Resource " + name + + " not found. Could not update the status. Note that the status of a resource cannot be changed before it is created on the cluster."); return null; } return resource.editStatus(JavaUtil.toUnary(consumer)); diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/TheiaCloudClient.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/TheiaCloudClient.java index 6bc70eb60..aeeedca12 100644 --- a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/TheiaCloudClient.java +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/TheiaCloudClient.java @@ -15,6 +15,8 @@ ********************************************************************************/ package org.eclipse.theia.cloud.common.k8s.client; +import org.eclipse.theia.cloud.common.util.CustomResourceUtil; + import java.util.Optional; import io.fabric8.kubernetes.api.model.HasMetadata; @@ -26,10 +28,11 @@ import io.fabric8.kubernetes.api.model.PersistentVolumeList; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.ServiceList; +import io.fabric8.kubernetes.api.model.gatewayapi.v1.HTTPRoute; +import io.fabric8.kubernetes.api.model.gatewayapi.v1.HTTPRouteList; import io.fabric8.kubernetes.api.model.networking.v1.Ingress; import io.fabric8.kubernetes.api.model.networking.v1.IngressList; import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.KubernetesClientBuilder; import io.fabric8.kubernetes.client.NamespacedKubernetesClient; import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; import io.fabric8.kubernetes.client.dsl.Resource; @@ -61,6 +64,10 @@ default ResourceClient ingresses() { return client(kubernetes().network().v1().ingresses(), Ingress.class); } + default ResourceClient httpRoutes() { + return client(HTTPRoute.class, HTTPRouteList.class); + } + @SuppressWarnings("unchecked") default ResourceClient> client(Class typeClass) { return client(typeClass, @@ -78,9 +85,29 @@ default > ResourceCli } default Optional getClusterIPFromSessionName(String sessionName) { - try (final KubernetesClient client = new KubernetesClientBuilder().build()) { + try (final KubernetesClient client = CustomResourceUtil.createClient()) { ServiceList svcList = client.services().inNamespace(namespace()).list(); for (Service svc : svcList.getItems()) { + if (svc.getMetadata().getName().endsWith("-int")) { + continue; + } + for (OwnerReference ownerReference : svc.getMetadata().getOwnerReferences()) { + if (sessionName.equals(ownerReference.getName()) && "Session".equals(ownerReference.getKind())) { + return Optional.of(svc.getSpec().getClusterIP()); + } + } + } + } + return Optional.empty(); + } + + default Optional getInternalClusterIPFromSessionName(String sessionName) { + try (final KubernetesClient client = CustomResourceUtil.createClient()) { + ServiceList svcList = client.services().inNamespace(namespace()).list(); + for (Service svc : svcList.getItems()) { + if (!svc.getMetadata().getName().endsWith("-int")) { + continue; + } for (OwnerReference ownerReference : svc.getMetadata().getOwnerReferences()) { if (sessionName.equals(ownerReference.getName()) && "Session".equals(ownerReference.getKind())) { return Optional.of(svc.getSpec().getClusterIP()); diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/appdefinition/AppDefinition.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/appdefinition/AppDefinition.java index 65ef5bc88..c1c8769d0 100644 --- a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/appdefinition/AppDefinition.java +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/appdefinition/AppDefinition.java @@ -29,7 +29,7 @@ import io.fabric8.kubernetes.model.annotation.Singular; import io.fabric8.kubernetes.model.annotation.Version; -@Version("v1beta10") +@Version("v1beta11") @Group("theia.cloud") @Kind("AppDefinition") @Singular("appdefinition") @@ -37,7 +37,7 @@ public class AppDefinition extends CustomResource implements Namespaced { private static final long serialVersionUID = 8749670583218521755L; - public static final String API = "theia.cloud/v1beta10"; + public static final String API = "theia.cloud/v1beta11"; public static final String KIND = "AppDefinition"; public static final String CRD_NAME = "appdefinitions.theia.cloud"; diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/appdefinition/AppDefinitionSpec.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/appdefinition/AppDefinitionSpec.java index 7478d9cbf..bae2269ba 100644 --- a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/appdefinition/AppDefinitionSpec.java +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/appdefinition/AppDefinitionSpec.java @@ -20,6 +20,7 @@ import java.util.Map; import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.hub.AppDefinitionHub; +import org.eclipse.theia.cloud.common.serialization.SensitiveData; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; @@ -37,6 +38,7 @@ public class AppDefinitionSpec { private String imagePullPolicy; @JsonProperty("pullSecret") + @SensitiveData private String pullSecret; @JsonProperty("uid") @@ -87,6 +89,9 @@ public class AppDefinitionSpec { @JsonProperty("ingressHostnamePrefixes") private List ingressHostnamePrefixes; + @JsonProperty("sidecars") + private List sidecars; + /** * Default constructor. */ @@ -115,6 +120,7 @@ public AppDefinitionSpec(AppDefinitionHub fromHub) { this.options = fromHub.getOptions().orElse(null); this.ingressHostnamePrefixes = fromHub.getIngressHostnamePrefixes().orElse(null); + this.sidecars = fromHub.getSidecars().orElse(null); int monitorPort = fromHub.getMonitorPort().orElse(0); if (monitorPort > 0) { @@ -162,10 +168,18 @@ public int getMinInstances() { return minInstances; } + public void setMinInstances(int minInstances) { + this.minInstances = minInstances; + } + public Integer getMaxInstances() { return maxInstances; } + public void setMaxInstances(Integer maxInstances) { + this.maxInstances = maxInstances; + } + public Integer getTimeout() { return timeout; } @@ -210,12 +224,35 @@ public List getIngressHostnamePrefixes() { return ingressHostnamePrefixes; } + public List getSidecars() { + return sidecars; + } + + public boolean hasSidecars() { + return sidecars != null && !sidecars.isEmpty(); + } + + public boolean requiresSharedWorkspace() { + if (sidecars == null) { + return false; + } + + for (SidecarSpec sidecar : sidecars) { + if (sidecar != null && sidecar.isMountWorkspace()) { + return true; + } + } + + return false; + } + @Override public String toString() { + final String redactedPullSecret = "***"; return "AppDefinitionSpec [name=" + name + ", image=" + image + ", imagePullPolicy=" + imagePullPolicy - + ", pullSecret=" + pullSecret + ", uid=" + uid + ", port=" + port + ", ingressname=" + ingressname - + ", minInstances=" + minInstances + ", maxInstances=" + maxInstances + ", timeout=" + timeout - + ", requestsMemory=" + requestsMemory + ", requestsCpu=" + requestsCpu + ", limitsMemory=" + + ", pullSecret=" + redactedPullSecret + ", uid=" + uid + ", port=" + port + ", ingressname=" + + ingressname + ", minInstances=" + minInstances + ", maxInstances=" + maxInstances + ", timeout=" + + timeout + ", requestsMemory=" + requestsMemory + ", requestsCpu=" + requestsCpu + ", limitsMemory=" + limitsMemory + ", limitsCpu=" + limitsCpu + ", downlinkLimit=" + downlinkLimit + ", uplinkLimit=" + uplinkLimit + ", mountPath=" + mountPath + "]"; } diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/appdefinition/SidecarSpec.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/appdefinition/SidecarSpec.java new file mode 100644 index 000000000..f04c7a746 --- /dev/null +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/appdefinition/SidecarSpec.java @@ -0,0 +1,119 @@ +/******************************************************************************** + * Copyright (C) 2026 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.common.k8s.resource.appdefinition; + +import java.util.List; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +@JsonDeserialize() +public class SidecarSpec { + + @JsonProperty("name") + private String name; + + @JsonProperty("image") + private String image; + + @JsonProperty("port") + private int port; + + @JsonProperty("languages") + private List languages; + + @JsonProperty("cpuLimit") + private String cpuLimit; + + @JsonProperty("memoryLimit") + private String memoryLimit; + + @JsonProperty("cpuRequest") + private String cpuRequest; + + @JsonProperty("memoryRequest") + private String memoryRequest; + + @JsonProperty("mountWorkspace") + private Boolean mountWorkspace; + + /** + * Default constructor. + */ + public SidecarSpec() { + } + + public String getName() { + return name; + } + + public String getImage() { + return image; + } + + public int getPort() { + return port; + } + + public List getLanguages() { + return languages; + } + + public String getCpuLimit() { + return cpuLimit; + } + + public String getMemoryLimit() { + return memoryLimit; + } + + public String getCpuRequest() { + return cpuRequest; + } + + public String getMemoryRequest() { + return memoryRequest; + } + + public boolean isMountWorkspace() { + return mountWorkspace == null ? false : mountWorkspace; + } + + @Override + public String toString() { + return "SidecarSpec [name=" + name + ", image=" + image + ", port=" + port + ", languages=" + languages + + ", cpuLimit=" + cpuLimit + ", memoryLimit=" + memoryLimit + ", cpuRequest=" + cpuRequest + + ", memoryRequest=" + memoryRequest + ", mountWorkspace=" + mountWorkspace + "]"; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SidecarSpec that = (SidecarSpec) o; + return port == that.port + && Objects.equals(mountWorkspace, that.mountWorkspace) + && Objects.equals(name, that.name) + && Objects.equals(image, that.image) + && Objects.equals(languages, that.languages) + && Objects.equals(cpuLimit, that.cpuLimit) + && Objects.equals(memoryLimit, that.memoryLimit) + && Objects.equals(cpuRequest, that.cpuRequest) + && Objects.equals(memoryRequest, that.memoryRequest); + } + + @Override + public int hashCode() { + return Objects.hash(name, image, port, languages, cpuLimit, memoryLimit, cpuRequest, memoryRequest, + mountWorkspace); + } + +} diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/appdefinition/hub/AppDefinitionHub.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/appdefinition/hub/AppDefinitionHub.java index 32731a2bb..6b22a6247 100644 --- a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/appdefinition/hub/AppDefinitionHub.java +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/appdefinition/hub/AppDefinitionHub.java @@ -21,6 +21,7 @@ import java.util.OptionalInt; import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinition; +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.SidecarSpec; import io.fabric8.kubernetes.api.model.ObjectMeta; @@ -58,6 +59,8 @@ public class AppDefinitionHub { final Optional> options; + final Optional> sidecars; + public AppDefinitionHub(AppDefinition toHub) { this.metadata = Optional.ofNullable(toHub.getMetadata()); this.name = Optional.ofNullable(toHub.getSpec().getName()); @@ -68,7 +71,9 @@ public AppDefinitionHub(AppDefinition toHub) { this.port = OptionalInt.of(toHub.getSpec().getPort()); this.ingressname = Optional.ofNullable(toHub.getSpec().getIngressname()); this.minInstances = OptionalInt.of(toHub.getSpec().getMinInstances()); - this.maxInstances = OptionalInt.of(toHub.getSpec().getMaxInstances()); + this.maxInstances = toHub.getSpec().getMaxInstances() != null + ? OptionalInt.of(toHub.getSpec().getMaxInstances()) + : OptionalInt.empty(); this.requestsMemory = Optional.ofNullable(toHub.getSpec().getRequestsMemory()); this.requestsCpu = Optional.ofNullable(toHub.getSpec().getRequestsCpu()); this.limitsMemory = Optional.ofNullable(toHub.getSpec().getLimitsMemory()); @@ -78,8 +83,11 @@ public AppDefinitionHub(AppDefinition toHub) { this.mountPath = Optional.ofNullable(toHub.getSpec().getMountPath()); this.options = Optional.ofNullable(toHub.getSpec().getOptions()); this.ingressHostnamePrefixes = Optional.ofNullable(toHub.getSpec().getIngressHostnamePrefixes()); + this.sidecars = Optional.ofNullable(toHub.getSpec().getSidecars()); - this.timeoutLimit = OptionalInt.of(toHub.getSpec().getTimeout()); + this.timeoutLimit = toHub.getSpec().getTimeout() != null + ? OptionalInt.of(toHub.getSpec().getTimeout()) + : OptionalInt.empty(); if (toHub.getSpec().getMonitor() != null) { this.monitorPort = OptionalInt.of(toHub.getSpec().getMonitor().getPort()); @@ -130,8 +138,11 @@ public AppDefinitionHub( this.mountPath = Optional.ofNullable(toHub.getSpec().getMountPath()); this.options = Optional.empty(); this.ingressHostnamePrefixes = Optional.empty(); + this.sidecars = Optional.empty(); - this.timeoutLimit = OptionalInt.of(toHub.getSpec().getTimeout()); + this.timeoutLimit = toHub.getSpec().getTimeout() != null + ? OptionalInt.of(toHub.getSpec().getTimeout()) + : OptionalInt.empty(); if (toHub.getSpec().getMonitor() != null) { this.monitorPort = OptionalInt.of(toHub.getSpec().getMonitor().getPort()); @@ -182,6 +193,7 @@ public AppDefinitionHub( this.mountPath = Optional.ofNullable(toHub.getSpec().getMountPath()); this.options = Optional.empty(); this.ingressHostnamePrefixes = Optional.empty(); + this.sidecars = Optional.empty(); if (toHub.getSpec().getTimeout() != null) { this.timeoutLimit = OptionalInt.of(toHub.getSpec().getTimeout().getLimit()); @@ -322,4 +334,62 @@ public Optional> getIngressHostnamePrefixes() { return ingressHostnamePrefixes; } + public Optional> getSidecars() { + return sidecars; + } + + @SuppressWarnings("deprecation") + public AppDefinitionHub( + org.eclipse.theia.cloud.common.k8s.resource.appdefinition.v1beta10.AppDefinitionV1beta10 toHub) { + this.metadata = Optional.ofNullable(toHub.getMetadata()); + this.name = Optional.ofNullable(toHub.getSpec().getName()); + this.image = Optional.ofNullable(toHub.getSpec().getImage()); + this.imagePullPolicy = Optional.ofNullable(toHub.getSpec().getImagePullPolicy()); + this.pullSecret = Optional.ofNullable(toHub.getSpec().getPullSecret()); + this.uid = OptionalInt.of(toHub.getSpec().getUid()); + this.port = OptionalInt.of(toHub.getSpec().getPort()); + this.ingressname = Optional.ofNullable(toHub.getSpec().getIngressname()); + this.minInstances = OptionalInt.of(toHub.getSpec().getMinInstances()); + this.maxInstances = OptionalInt.of(toHub.getSpec().getMaxInstances()); + this.requestsMemory = Optional.ofNullable(toHub.getSpec().getRequestsMemory()); + this.requestsCpu = Optional.ofNullable(toHub.getSpec().getRequestsCpu()); + this.limitsMemory = Optional.ofNullable(toHub.getSpec().getLimitsMemory()); + this.limitsCpu = Optional.ofNullable(toHub.getSpec().getLimitsCpu()); + this.downlinkLimit = OptionalInt.of(toHub.getSpec().getDownlinkLimit()); + this.uplinkLimit = OptionalInt.of(toHub.getSpec().getUplinkLimit()); + this.mountPath = Optional.ofNullable(toHub.getSpec().getMountPath()); + this.options = Optional.ofNullable(toHub.getSpec().getOptions()); + this.ingressHostnamePrefixes = Optional.ofNullable(toHub.getSpec().getIngressHostnamePrefixes()); + this.sidecars = Optional.empty(); + + this.timeoutLimit = toHub.getSpec().getTimeout() != null + ? OptionalInt.of(toHub.getSpec().getTimeout()) + : OptionalInt.empty(); + + if (toHub.getSpec().getMonitor() != null) { + this.monitorPort = OptionalInt.of(toHub.getSpec().getMonitor().getPort()); + if (toHub.getSpec().getMonitor().getActivityTracker() != null) { + this.monitorActivityTrackerNotifyAfter = OptionalInt + .of(toHub.getSpec().getMonitor().getActivityTracker().getNotifyAfter()); + this.monitorActivityTrackerTimeoutAfter = OptionalInt + .of(toHub.getSpec().getMonitor().getActivityTracker().getTimeoutAfter()); + } else { + this.monitorActivityTrackerNotifyAfter = OptionalInt.empty(); + this.monitorActivityTrackerTimeoutAfter = OptionalInt.empty(); + } + } else { + this.monitorPort = OptionalInt.empty(); + this.monitorActivityTrackerNotifyAfter = OptionalInt.empty(); + this.monitorActivityTrackerTimeoutAfter = OptionalInt.empty(); + } + + if (toHub.getStatus() != null) { + this.operatorStatus = Optional.ofNullable(toHub.getNonNullStatus().getOperatorStatus()); + this.operatorMessage = Optional.ofNullable(toHub.getNonNullStatus().getOperatorMessage()); + } else { + this.operatorStatus = Optional.empty(); + this.operatorMessage = Optional.empty(); + } + } + } diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/appdefinition/v1beta10/AppDefinitionV1beta10.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/appdefinition/v1beta10/AppDefinitionV1beta10.java new file mode 100644 index 000000000..7dbcadb73 --- /dev/null +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/appdefinition/v1beta10/AppDefinitionV1beta10.java @@ -0,0 +1,67 @@ +/******************************************************************************** + * Copyright (C) 2022-2023 EclipseSource, Lockular, Ericsson, STMicroelectronics and + * others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.common.k8s.resource.appdefinition.v1beta10; + +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.hub.AppDefinitionHub; +import org.eclipse.theia.cloud.common.util.CustomResourceUtil; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.Kind; +import io.fabric8.kubernetes.model.annotation.Plural; +import io.fabric8.kubernetes.model.annotation.Singular; +import io.fabric8.kubernetes.model.annotation.Version; + +@Deprecated +@Version("v1beta10") +@Group("theia.cloud") +@Kind("AppDefinition") +@Singular("appdefinition") +@Plural("appdefinitions") +public class AppDefinitionV1beta10 extends CustomResource + implements Namespaced { + + private static final long serialVersionUID = 8749670583218521755L; + public static final String API = "theia.cloud/v1beta10"; + public static final String KIND = "AppDefinition"; + public static final String CRD_NAME = "appdefinitions.theia.cloud"; + + @Override + public String toString() { + return CustomResourceUtil.toString(this); + } + + public AppDefinitionV1beta10() { + } + + public AppDefinitionV1beta10(AppDefinitionHub fromHub) { + if (fromHub.getMetadata().isPresent()) { + this.setMetadata(fromHub.getMetadata().get()); + } + this.spec = new AppDefinitionV1beta10Spec(fromHub); + this.status = new AppDefinitionV1beta10Status(fromHub); + } + + @JsonIgnore + public AppDefinitionV1beta10Status getNonNullStatus() { + return super.getStatus() != null ? super.getStatus() : new AppDefinitionV1beta10Status(); + } + +} diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/appdefinition/v1beta10/AppDefinitionV1beta10Spec.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/appdefinition/v1beta10/AppDefinitionV1beta10Spec.java new file mode 100644 index 000000000..f740be6d9 --- /dev/null +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/appdefinition/v1beta10/AppDefinitionV1beta10Spec.java @@ -0,0 +1,287 @@ +/******************************************************************************** + * Copyright (C) 2022-2023 EclipseSource, Lockular, Ericsson, STMicroelectronics and + * others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.common.k8s.resource.appdefinition.v1beta10; + +import java.util.List; +import java.util.Map; + +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.hub.AppDefinitionHub; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +@Deprecated +@JsonDeserialize() +public class AppDefinitionV1beta10Spec { + + @JsonProperty("name") + private String name; + + @JsonProperty("image") + private String image; + + @JsonProperty("imagePullPolicy") + private String imagePullPolicy; + + @JsonProperty("pullSecret") + private String pullSecret; + + @JsonProperty("uid") + private int uid; + + @JsonProperty("port") + private int port; + + @JsonProperty("ingressname") + private String ingressname; + + @JsonProperty("minInstances") + private int minInstances; + + @JsonProperty("maxInstances") + private Integer maxInstances; + + @JsonProperty("timeout") + private Integer timeout; + + @JsonProperty("requestsMemory") + private String requestsMemory; + + @JsonProperty("requestsCpu") + private String requestsCpu; + + @JsonProperty("limitsMemory") + private String limitsMemory; + + @JsonProperty("limitsCpu") + private String limitsCpu; + + @JsonProperty("downlinkLimit") + private int downlinkLimit; + + @JsonProperty("uplinkLimit") + private int uplinkLimit; + + @JsonProperty("mountPath") + private String mountPath; + + @JsonProperty("monitor") + private Monitor monitor; + + @JsonProperty("options") + private Map options; + + @JsonProperty("ingressHostnamePrefixes") + private List ingressHostnamePrefixes; + + public AppDefinitionV1beta10Spec() { + } + + public AppDefinitionV1beta10Spec(AppDefinitionHub fromHub) { + this.name = fromHub.getName().orElse(null); + this.image = fromHub.getImage().orElse(null); + this.imagePullPolicy = fromHub.getImagePullPolicy().orElse(null); + this.pullSecret = fromHub.getPullSecret().orElse(null); + this.uid = fromHub.getUid().orElse(0); + this.port = fromHub.getPort().orElse(0); + this.ingressname = fromHub.getIngressname().orElse(null); + this.minInstances = fromHub.getMinInstances().orElse(0); + this.maxInstances = fromHub.getMaxInstances().isPresent() ? fromHub.getMaxInstances().getAsInt() : null; + this.requestsMemory = fromHub.getRequestsMemory().orElse(null); + this.requestsCpu = fromHub.getRequestsCpu().orElse(null); + this.limitsMemory = fromHub.getLimitsMemory().orElse(null); + this.limitsCpu = fromHub.getLimitsCpu().orElse(null); + this.downlinkLimit = fromHub.getDownlinkLimit().orElse(0); + this.uplinkLimit = fromHub.getUplinkLimit().orElse(0); + this.mountPath = fromHub.getMountPath().orElse(null); + + this.timeout = fromHub.getTimeoutLimit().orElse(0); + + this.options = fromHub.getOptions().orElse(null); + this.ingressHostnamePrefixes = fromHub.getIngressHostnamePrefixes().orElse(null); + + int monitorPort = fromHub.getMonitorPort().orElse(0); + if (monitorPort > 0) { + this.monitor = new Monitor(); + this.monitor.port = monitorPort; + + int timeoutAfter = fromHub.getMonitorActivityTrackerTimeoutAfter().orElse(0); + if (timeoutAfter > 0) { + this.monitor.activityTracker = new Monitor.ActivityTracker(); + this.monitor.activityTracker.timeoutAfter = timeoutAfter; + this.monitor.activityTracker.notifyAfter = fromHub.getMonitorActivityTrackerNotifyAfter().orElse(0); + } + } + } + + public String getName() { + return name; + } + + public String getImage() { + return image; + } + + public String getImagePullPolicy() { + return imagePullPolicy; + } + + public String getPullSecret() { + return pullSecret; + } + + public int getUid() { + return uid; + } + + public int getPort() { + return port; + } + + public String getIngressname() { + return ingressname; + } + + public int getMinInstances() { + return minInstances; + } + + public void setMinInstances(int minInstances) { + this.minInstances = minInstances; + } + + public Integer getMaxInstances() { + return maxInstances; + } + + public void setMaxInstances(Integer maxInstances) { + this.maxInstances = maxInstances; + } + + public Integer getTimeout() { + return timeout; + } + + public String getRequestsMemory() { + return requestsMemory; + } + + public String getRequestsCpu() { + return requestsCpu; + } + + public String getLimitsMemory() { + return limitsMemory; + } + + public String getLimitsCpu() { + return limitsCpu; + } + + public int getDownlinkLimit() { + return downlinkLimit; + } + + public int getUplinkLimit() { + return uplinkLimit; + } + + public String getMountPath() { + return mountPath; + } + + public Monitor getMonitor() { + return monitor; + } + + public Map getOptions() { + return options; + } + + public List getIngressHostnamePrefixes() { + return ingressHostnamePrefixes; + } + + @Override + public String toString() { + return "AppDefinitionV1beta10Spec [name=" + name + ", image=" + image + ", imagePullPolicy=" + imagePullPolicy + + ", pullSecret=***, uid=" + uid + ", port=" + port + ", ingressname=" + ingressname + + ", minInstances=" + minInstances + ", maxInstances=" + maxInstances + ", timeout=" + timeout + + ", requestsMemory=" + requestsMemory + ", requestsCpu=" + requestsCpu + ", limitsMemory=" + + limitsMemory + ", limitsCpu=" + limitsCpu + ", downlinkLimit=" + downlinkLimit + ", uplinkLimit=" + + uplinkLimit + ", mountPath=" + mountPath + "]"; + } + + public static class Monitor { + @JsonProperty("port") + private int port; + + @JsonProperty("activityTracker") + private ActivityTracker activityTracker; + + public Monitor() { + } + + public Monitor(ActivityTracker activityTracker, int port) { + this.activityTracker = activityTracker; + this.port = port; + } + + public int getPort() { + return port; + } + + public ActivityTracker getActivityTracker() { + return activityTracker; + } + + @Override + public String toString() { + return "Monitor [activityTracker=" + activityTracker + ", port=" + port + "]"; + } + + public static class ActivityTracker { + @JsonProperty("timeoutAfter") + private int timeoutAfter; + + @JsonProperty("notifyAfter") + private int notifyAfter; + + public ActivityTracker() { + } + + public ActivityTracker(int timeoutAfter, int notifyAfter) { + this.timeoutAfter = timeoutAfter; + this.notifyAfter = notifyAfter; + } + + public int getTimeoutAfter() { + return timeoutAfter; + } + + public int getNotifyAfter() { + return notifyAfter; + } + + @Override + public String toString() { + return "ActivityTracker [timeoutAfter=" + timeoutAfter + ", notifyAfter=" + notifyAfter + "]"; + } + + } + } +} diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/appdefinition/v1beta10/AppDefinitionV1beta10SpecResourceList.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/appdefinition/v1beta10/AppDefinitionV1beta10SpecResourceList.java new file mode 100644 index 000000000..bff29b4aa --- /dev/null +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/appdefinition/v1beta10/AppDefinitionV1beta10SpecResourceList.java @@ -0,0 +1,26 @@ +/******************************************************************************** + * Copyright (C) 2022 EclipseSource, Lockular, Ericsson, STMicroelectronics and + * others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.common.k8s.resource.appdefinition.v1beta10; + +import io.fabric8.kubernetes.api.model.DefaultKubernetesResourceList; + +@Deprecated +public class AppDefinitionV1beta10SpecResourceList extends DefaultKubernetesResourceList { + + private static final long serialVersionUID = 722136158817065564L; + +} diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/appdefinition/v1beta10/AppDefinitionV1beta10Status.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/appdefinition/v1beta10/AppDefinitionV1beta10Status.java new file mode 100644 index 000000000..c6725df80 --- /dev/null +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/appdefinition/v1beta10/AppDefinitionV1beta10Status.java @@ -0,0 +1,45 @@ +/******************************************************************************** + * Copyright (C) 2023 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.common.k8s.resource.appdefinition.v1beta10; + +import org.eclipse.theia.cloud.common.k8s.resource.ResourceStatus; +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.hub.AppDefinitionHub; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +@Deprecated +@JsonDeserialize +public class AppDefinitionV1beta10Status extends ResourceStatus { + + public AppDefinitionV1beta10Status() { + } + + public AppDefinitionV1beta10Status(AppDefinitionHub fromHub) { + if (fromHub.getOperatorMessage().isPresent()) { + this.setOperatorMessage(fromHub.getOperatorMessage().get()); + } + if (fromHub.getOperatorStatus().isPresent()) { + this.setOperatorStatus(fromHub.getOperatorStatus().get()); + } + } + + @Override + public String toString() { + return "AppDefinitionV1beta10Status [getOperatorStatus()=" + getOperatorStatus() + ", getOperatorMessage()=" + + getOperatorMessage() + "]"; + } + +} diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/session/DataInjectionRequest.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/session/DataInjectionRequest.java new file mode 100644 index 000000000..3e86698a9 --- /dev/null +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/session/DataInjectionRequest.java @@ -0,0 +1,38 @@ +package org.eclipse.theia.cloud.common.k8s.resource.session; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Request object for injecting data into a session's data bridge. + */ +public class DataInjectionRequest { + + private final Map environment; + + /** + * Creates a data injection request. + * + * @param environment Map of environment variable data to inject + */ + public DataInjectionRequest(Map environment) { + this.environment = environment != null ? new HashMap<>(environment) : new HashMap<>(); + } + + /** + * Gets the environment variables to inject. + * + * @return Unmodifiable map of environment variables + */ + public Map getEnvironment() { + return Collections.unmodifiableMap(environment); + } + + @Override + public String toString() { + // Don't log actual data values for security + return "DataInjectionRequest{" + "environmentKeys=" + environment.keySet() + '}'; + } +} + diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/session/DataInjectionResponse.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/session/DataInjectionResponse.java new file mode 100644 index 000000000..877b31640 --- /dev/null +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/session/DataInjectionResponse.java @@ -0,0 +1,63 @@ +package org.eclipse.theia.cloud.common.k8s.resource.session; + +/** + * Response object for data injection operations. + */ +public class DataInjectionResponse { + + private final boolean success; + private final String message; + private final String error; + + /** + * Creates a data injection response. + * + * @param success Whether the injection was successful + * @param message Success message (null if failed) + * @param error Error message (null if successful) + */ + public DataInjectionResponse(boolean success, String message, String error) { + this.success = success; + this.message = message; + this.error = error; + } + + /** + * Creates a successful response. + * + * @param message Success message + * @return Response indicating success + */ + public static DataInjectionResponse success(String message) { + return new DataInjectionResponse(true, message, null); + } + + /** + * Creates a failure response. + * + * @param error Error message + * @return Response indicating failure + */ + public static DataInjectionResponse failure(String error) { + return new DataInjectionResponse(false, null, error); + } + + public boolean isSuccess() { + return success; + } + + public String getMessage() { + return message; + } + + public String getError() { + return error; + } + + @Override + public String toString() { + return "DataInjectionResponse{" + "success=" + success + ", message='" + message + '\'' + ", error='" + + error + '\'' + '}'; + } +} + diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/serialization/SensitiveData.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/serialization/SensitiveData.java new file mode 100644 index 000000000..febd3bb0b --- /dev/null +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/serialization/SensitiveData.java @@ -0,0 +1,39 @@ +/******************************************************************************** + * Copyright (C) 2024 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.common.serialization; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + *

+ * Annotates object properties that contain sensitive content and must not be serialized when returned publicly or to + * arbitrary users (e.g. via a REST endpoint of the service). + *

+ *

+ * Note that the serializer {@link SensitiveDataSerializer} must be registered in the Jackson ObjectMapper for this + * annotation to be respected. + *

+ * + * @see SensitiveDataSerializer + * @see SensitiveDataBeanSerializerModifier + */ +@Target({ ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface SensitiveData { +} diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/serialization/SensitiveDataBeanSerializerModifier.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/serialization/SensitiveDataBeanSerializerModifier.java new file mode 100644 index 000000000..8f766e7b0 --- /dev/null +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/serialization/SensitiveDataBeanSerializerModifier.java @@ -0,0 +1,34 @@ +package org.eclipse.theia.cloud.common.serialization; + +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; +import com.fasterxml.jackson.databind.ser.BeanPropertyWriter; + +import java.util.List; + +/** + *

+ * A Jackson {@link BeanSerializerModifier} that modifies the serialization of sensitive data. It assigns the + * {@link SensitiveDataSerializer} to fields annotated with {@link SensitiveData}. The serializer is assigned for + * regular and null value serialization to prevent leaking information. + *

+ *

+ * To use this serializer modifier, it must be registered with Jackson's + * {@link com.fasterxml.jackson.databind.ObjectMapper ObjectMapper}. + *

+ */ +public class SensitiveDataBeanSerializerModifier extends BeanSerializerModifier { + @Override + public List changeProperties(SerializationConfig config, BeanDescription beanDesc, + List beanProperties) { + for (BeanPropertyWriter writer : beanProperties) { + // Check if the field has the @SensitiveData annotation + if (writer.getMember().getAnnotation(SensitiveData.class) != null) { + writer.assignNullSerializer(new SensitiveDataSerializer(writer.getType())); + writer.assignSerializer(new SensitiveDataSerializer(writer.getType())); + } + } + return beanProperties; + } +} diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/serialization/SensitiveDataSerializer.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/serialization/SensitiveDataSerializer.java new file mode 100644 index 000000000..2f41ecb44 --- /dev/null +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/serialization/SensitiveDataSerializer.java @@ -0,0 +1,54 @@ +package org.eclipse.theia.cloud.common.serialization; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; + +/** + * A Jackson {@link JsonSerializer} that redacts sensitive data. It suppresses the serialization of sensitive data by + * writing a predefined value instead. It must be used as the serializer for normal and null values of sensitive data. + */ +public class SensitiveDataSerializer extends JsonSerializer { + + public static String REDACTED_STRING = "***"; + public static int REDACTED_NUMBER = 0; + public static boolean REDACTED_BOOLEAN = false; + + protected JavaType propertyType; + + public SensitiveDataSerializer(JavaType propertyType) { + this.propertyType = propertyType; + } + + @Override + public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (propertyType.isTypeOrSubTypeOf(String.class)) { + gen.writeString(REDACTED_STRING); + } else if (propertyType.isContainerType()) { + if (propertyType.isMapLikeType()) { + gen.writeStartObject(); + gen.writeEndObject(); + } else if (propertyType.isArrayType() || propertyType.isCollectionLikeType()) { + gen.writeStartArray(); + gen.writeEndArray(); + } else { + gen.writeNull(); + } + // Check value.getClass in case of primitive number types (e.g. int, long, double, etc.) + // This is necessary because the propertyType for these is not a subtype of Number but the class of the + // boxed value is. + } else if (propertyType.isTypeOrSubTypeOf(Number.class) + || (value != null && Number.class.isAssignableFrom(value.getClass()))) { + gen.writeNumber(REDACTED_NUMBER); + // Check value.getClass in case of primitive boolean type + } else if (propertyType.isTypeOrSubTypeOf(Boolean.class) + || (value != null && Boolean.class.isAssignableFrom(value.getClass()))) { + gen.writeBoolean(REDACTED_BOOLEAN); + } else { + gen.writeNull(); + } + } +} diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/tracing/TraceContext.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/tracing/TraceContext.java new file mode 100644 index 000000000..e2aae66d6 --- /dev/null +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/tracing/TraceContext.java @@ -0,0 +1,190 @@ +/******************************************************************************** + * Copyright (C) 2025 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.common.tracing; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.sentry.BaggageHeader; +import io.sentry.ISpan; +import io.sentry.Sentry; +import io.sentry.SentryTraceHeader; + +/** + * Trace context for propagating distributed traces across service boundaries via Kubernetes annotations. + * Stores the full sentry-trace header and baggage for proper trace continuation using Sentry.continueTrace(). + * + * The sentry-trace header format is: {trace_id}-{span_id}-{sampled} + * - trace_id: 32 hex chars (128 bits) + * - span_id: 16 hex chars (64 bits) + * - sampled: optional, "1" (sampled), "0" (not sampled), or empty (defer) + */ +public final class TraceContext { + + /** Annotation key for the full sentry-trace header */ + private static final String ANNOTATION_SENTRY_TRACE = "theia-cloud.io/sentry-trace"; + + /** Annotation key for the baggage header (Dynamic Sampling Context) */ + private static final String ANNOTATION_BAGGAGE = "theia-cloud.io/baggage"; + + private final String sentryTrace; + private final String baggage; + + private TraceContext(String sentryTrace, String baggage) { + if (sentryTrace == null || sentryTrace.isEmpty()) { + throw new IllegalArgumentException("sentryTrace cannot be null or empty"); + } + this.sentryTrace = sentryTrace; + this.baggage = baggage; + } + + /** + * Returns the sentry-trace header value. + * Format: {trace_id}-{span_id}-{sampled} + */ + public String getSentryTrace() { + return sentryTrace; + } + + /** + * Returns the baggage header value (may be null). + */ + public String getBaggage() { + return baggage; + } + + /** + * Returns the baggage as a list (for Sentry.continueTrace compatibility). + */ + public List getBaggageList() { + if (baggage == null || baggage.isEmpty()) { + return new ArrayList<>(); + } + List list = new ArrayList<>(1); + list.add(baggage); + return list; + } + + /** + * Extracts trace context from the current Sentry span/transaction. + * Uses Sentry's built-in methods to get properly formatted headers. + */ + public static Optional fromCurrent() { + final ISpan current = Sentry.getSpan(); + if (current == null) { + return Optional.empty(); + } + + // Get the sentry-trace header using Sentry's API + SentryTraceHeader traceHeader = Sentry.getTraceparent(); + if (traceHeader == null) { + return Optional.empty(); + } + + String sentryTrace = traceHeader.getValue(); + + // Get baggage header for Dynamic Sampling Context + BaggageHeader baggageHeader = Sentry.getBaggage(); + String baggage = baggageHeader != null ? baggageHeader.getValue() : null; + + return Optional.of(new TraceContext(sentryTrace, baggage)); + } + + /** + * Extracts trace context from a specific span. + * Use this to capture context before scheduling async tasks, so the async task + * can continue the same trace even after the original span is finished. + */ + public static Optional fromSpan(ISpan span) { + if (span == null) { + return Optional.empty(); + } + + // Get the sentry-trace header from the span + SentryTraceHeader traceHeader = span.toSentryTrace(); + if (traceHeader == null) { + return Optional.empty(); + } + + String sentryTrace = traceHeader.getValue(); + + // Get baggage header for Dynamic Sampling Context + BaggageHeader baggageHeader = span.toBaggageHeader(null); + String baggage = baggageHeader != null ? baggageHeader.getValue() : null; + + return Optional.of(new TraceContext(sentryTrace, baggage)); + } + + /** + * Extracts trace context from Kubernetes resource annotations. + */ + public static Optional fromAnnotations(Map annotations) { + if (annotations == null) { + return Optional.empty(); + } + + String sentryTrace = annotations.get(ANNOTATION_SENTRY_TRACE); + if (sentryTrace == null || sentryTrace.isEmpty()) { + return Optional.empty(); + } + + String baggage = annotations.get(ANNOTATION_BAGGAGE); + return Optional.of(new TraceContext(sentryTrace, baggage)); + } + + /** + * Extracts trace context from Kubernetes ObjectMeta annotations. + */ + public static Optional fromMetadata(ObjectMeta metadata) { + if (metadata == null || metadata.getAnnotations() == null) { + return Optional.empty(); + } + return fromAnnotations(metadata.getAnnotations()); + } + + /** + * Converts trace context to Kubernetes annotations map. + */ + public Map toAnnotations() { + Map annotations = new HashMap<>(); + annotations.put(ANNOTATION_SENTRY_TRACE, sentryTrace); + if (baggage != null && !baggage.isEmpty()) { + annotations.put(ANNOTATION_BAGGAGE, baggage); + } + return annotations; + } + + /** + * Merges trace context annotations into an existing annotations map. + */ + public void mergeInto(Map annotations) { + if (annotations == null) { + return; + } + annotations.put(ANNOTATION_SENTRY_TRACE, sentryTrace); + if (baggage != null && !baggage.isEmpty()) { + annotations.put(ANNOTATION_BAGGAGE, baggage); + } + } + + @Override + public String toString() { + return new StringBuilder("TraceContext{sentryTrace=") + .append(sentryTrace) + .append(", baggage=") + .append(baggage != null ? "present" : "null") + .append("}") + .toString(); + } +} diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/tracing/Tracing.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/tracing/Tracing.java new file mode 100644 index 000000000..30a0dad03 --- /dev/null +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/tracing/Tracing.java @@ -0,0 +1,197 @@ +/******************************************************************************** + * Copyright (C) 2025 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.common.tracing; + +import java.util.function.Supplier; +import java.util.Optional; + +import io.sentry.ISpan; +import io.sentry.ITransaction; +import io.sentry.Sentry; +import io.sentry.SpanStatus; +import io.sentry.TransactionContext; +import io.sentry.TransactionOptions; + +/** + * Minimal, elegant tracing API. Always creates child spans when a parent exists. + * Pass spans explicitly in method signatures for clarity. + */ +public final class Tracing { + + private Tracing() { + } + + /** + * Starts a new transaction (root span of a trace). + */ + public static ITransaction startTransaction(String operation, String description) { + return Sentry.startTransaction(operation, description); + } + + /** + * Creates a child span from a parent (transaction or span). + * If parent is null, starts a new transaction instead. + */ + public static ISpan childSpan(ISpan parent, String operation, String description) { + if (parent != null) { + return parent.startChild(operation, description); + } + return Sentry.startTransaction(operation, description); + } + + /** + * Creates a child span from a transaction. + */ + public static ISpan childSpan(ITransaction parent, String operation, String description) { + if (parent != null) { + return parent.startChild(operation, description); + } + return Sentry.startTransaction(operation, description); + } + + /** + * Creates a child span from the current active span/transaction. + * If none exists, starts a new transaction. + */ + public static ISpan childSpan(String operation, String description) { + ISpan current = Sentry.getSpan(); + return childSpan(current, operation, description); + } + + /** + * Continues a trace from a TraceContext by creating a properly linked transaction. + * Uses Sentry.continueTrace() to properly connect traces across service boundaries. + * + * This enables distributed tracing where: + * - The Service creates a session with trace context in annotations + * - The Operator extracts the context and continues the same trace + * - Both transactions appear connected in Sentry's trace view + */ + public static ISpan continueTrace(TraceContext context, String name, String operation) { + // Use Sentry.continueTrace() to properly parse and store trace context + TransactionContext txContext = Sentry.continueTrace( + context.getSentryTrace(), + context.getBaggageList() + ); + + if (txContext != null) { + // Create transaction options to bind to scope + TransactionOptions options = new TransactionOptions(); + options.setBindToScope(true); + + // Start transaction with the continued context + ITransaction tx = Sentry.startTransaction(txContext, options); + tx.setName(name); + tx.setOperation(operation); + return tx; + } + + // Fallback: if continuation fails, start new transaction with reference tags + ITransaction tx = Sentry.startTransaction(name, operation); + return tx; + } + + /** + * Finishes a span with the given status. + */ + public static void finish(ISpan span, SpanStatus status) { + if (span != null) { + span.setStatus(status); + span.finish(); + } + } + + /** + * Finishes a span with success status. + */ + public static void finishSuccess(ISpan span) { + finish(span, SpanStatus.OK); + } + + /** + * Finishes a span with error status and captures exception. + */ + public static void finishError(ISpan span, Throwable error) { + if (span != null) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + if (error != null) { + span.setThrowable(error); + Sentry.captureException(error); + } + span.finish(); + } + } + + /** + * Executes a supplier within a span, automatically finishing it. + */ + public static T traced(ISpan parent, String operation, String description, Supplier action) { + ISpan span = childSpan(parent, operation, description); + try { + T result = action.get(); + finishSuccess(span); + return result; + } catch (Exception e) { + finishError(span, e); + throw e; + } + } + + /** + * Executes a runnable within a span, automatically finishing it. + */ + public static void traced(ISpan parent, String operation, String description, Runnable action) { + traced(parent, operation, description, () -> { + action.run(); + return null; + }); + } + + /** + * Continues a trace for async operations that run after the parent span is finished. + * Creates a new transaction linked to the same trace as the parent. + * + * IMPORTANT: For async operations scheduled via executors, the parent span will typically + * be finished before the async task runs. You cannot create child spans from finished spans. + * Instead, extract TraceContext from the parent span BEFORE scheduling, then call this + * method in the async task to create a linked transaction. + * + * Usage pattern: + *
+     * // Before scheduling async task (while span is still active)
+     * Optional traceContext = TraceContext.fromSpan(parentSpan);
+     * 
+     * executor.submit(() -> {
+     *     // In async task (parent span may be finished)
+     *     ISpan span = Tracing.continueTraceAsync(traceContext, "async.operation", "Async operation");
+     *     try {
+     *         // do work
+     *         Tracing.finishSuccess(span);
+     *     } catch (Exception e) {
+     *         Tracing.finishError(span, e);
+     *     }
+     * });
+     * 
+ * + * @param traceContext The trace context extracted from the parent span (use TraceContext.fromSpan()) + * @param name The name for the new transaction + * @param operation The operation name + * @return A new transaction linked to the same trace, or a standalone transaction if context is empty + */ + public static ISpan continueTraceAsync(Optional traceContext, String name, String operation) { + if (traceContext.isPresent()) { + return continueTrace(traceContext.get(), name, operation); + } + // Fallback: start a new unlinked transaction + TransactionOptions options = new TransactionOptions(); + options.setBindToScope(true); + return Sentry.startTransaction(name, operation, options); + } +} diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/util/CustomResourceUtil.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/util/CustomResourceUtil.java index 3f43ab9d6..70759da7b 100644 --- a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/util/CustomResourceUtil.java +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/util/CustomResourceUtil.java @@ -24,6 +24,10 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; import io.fabric8.kubernetes.client.NamespacedKubernetesClient; +import io.fabric8.kubernetes.client.okhttp.OkHttpClientFactory; +import io.sentry.okhttp.SentryOkHttpEventListener; +import io.sentry.okhttp.SentryOkHttpInterceptor; +import okhttp3.OkHttpClient; public final class CustomResourceUtil { @@ -35,7 +39,16 @@ public static NamespacedKubernetesClient createClient() { } public static NamespacedKubernetesClient createClient(Config config) { - KubernetesClient client = new KubernetesClientBuilder().withConfig(config).build(); + KubernetesClient client = new KubernetesClientBuilder() + .withConfig(config) + .withHttpClientFactory(new OkHttpClientFactory() { + @Override + protected void additionalConfig(OkHttpClient.Builder builder) { + builder.addInterceptor(new SentryOkHttpInterceptor()); + builder.eventListener(new SentryOkHttpEventListener()); + } + }) + .build(); return client.adapt(NamespacedKubernetesClient.class); } diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/util/DataBridgeUtil.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/util/DataBridgeUtil.java new file mode 100644 index 000000000..9b7c303b3 --- /dev/null +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/util/DataBridgeUtil.java @@ -0,0 +1,92 @@ +package org.eclipse.theia.cloud.common.util; + +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinitionSpec; + +import java.util.Optional; + +/** + * Utility class for data bridge operations. + */ +public final class DataBridgeUtil { + + public static final String DATA_BRIDGE_PORT_OPTION = "dataBridgePort"; + public static final String DATA_BRIDGE_ENABLED_OPTION = "dataBridgeEnabled"; + public static final int DEFAULT_DATA_BRIDGE_PORT = 16281; + private static final String DATA_PATH = "/data"; + private static final String HEALTH_PATH = "/health"; + + private DataBridgeUtil() { + // Utility class + } + + /** + * Checks if data bridge is enabled for this app definition. + * When enabled, env vars are injected at runtime via the data bridge + * instead of being set as container environment variables. + * + * @param appDefSpec The app definition specification + * @return true if data bridge is enabled + */ + public static boolean isDataBridgeEnabled(AppDefinitionSpec appDefSpec) { + if (appDefSpec.getOptions() == null) { + return false; + } + String value = appDefSpec.getOptions().get(DATA_BRIDGE_ENABLED_OPTION); + if (value == null) { + return false; + } + return "true".equalsIgnoreCase(value) || "1".equals(value); + } + + /** + * Gets the data bridge port from app definition options. Defaults to + * 16281 if not specified. + * + * @param appDefSpec The app definition specification + * @return The data bridge port number + */ + public static int getDataBridgePort(AppDefinitionSpec appDefSpec) { + if (appDefSpec.getOptions() == null) { + return DEFAULT_DATA_BRIDGE_PORT; + } + + if (!appDefSpec.getOptions().containsKey(DATA_BRIDGE_PORT_OPTION)) { + return DEFAULT_DATA_BRIDGE_PORT; + } + + try { + return Integer.parseInt(appDefSpec.getOptions().get(DATA_BRIDGE_PORT_OPTION)); + } catch (NumberFormatException e) { + return DEFAULT_DATA_BRIDGE_PORT; + } + } + + /** + * Constructs the data injection URL for a session. Format: + * http://{service-ip}:{port}/credentials + * + * @param serviceIP The service cluster IP + * @param port The data bridge port + * @return Optional containing the URL, or empty if service IP is invalid + */ + public static Optional getDataInjectionURL(String serviceIP, int port) { + if (serviceIP == null || serviceIP.isEmpty()) { + return Optional.empty(); + } + return Optional.of("http://" + serviceIP + ":" + port + DATA_PATH); + } + + /** + * Constructs the health check URL for a session. + * + * @param serviceIP The service cluster IP + * @param port The data bridge port + * @return Optional containing the URL, or empty if service IP is invalid + */ + public static Optional getHealthCheckURL(String serviceIP, int port) { + if (serviceIP == null || serviceIP.isEmpty()) { + return Optional.empty(); + } + return Optional.of("http://" + serviceIP + ":" + port + HEALTH_PATH); + } +} diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/util/LabelsUtil.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/util/LabelsUtil.java new file mode 100644 index 000000000..20fc39662 --- /dev/null +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/util/LabelsUtil.java @@ -0,0 +1,77 @@ +/******************************************************************************** + * Copyright (C) 2024 EclipseSource. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.common.util; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; + +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinition; +import org.eclipse.theia.cloud.common.k8s.resource.session.Session; +import org.eclipse.theia.cloud.common.k8s.resource.session.SessionSpec; + +public class LabelsUtil { + private static final Logger LOGGER = Logger.getLogger(LabelsUtil.class.getName()); + + public static final String LABEL_CUSTOM_PREFIX = "theia-cloud.io"; + + public static final String LABEL_KEY_SESSION = "app.kubernetes.io/component"; + public static final String LABEL_VALUE_SESSION = "session"; + + public static final String LABEL_KEY_THEIACLOUD = "app.kubernetes.io/part-of"; + public static final String LABEL_VALUE_THEIACLOUD = "theia-cloud"; + + public static final String LABEL_KEY_USER = LABEL_CUSTOM_PREFIX + "/user"; + public static final String LABEL_KEY_APPDEF = LABEL_CUSTOM_PREFIX + "/app-definition"; + public static final String LABEL_KEY_SESSION_NAME = LABEL_CUSTOM_PREFIX + "/session"; + public static final String LABEL_KEY_SESSION_UUID = LABEL_CUSTOM_PREFIX + "/session-uuid"; + + private static final int MAX_LABEL_LENGTH = 63; + + private static String truncateLabelValue(String value) { + if (value.length() > MAX_LABEL_LENGTH) { + LOGGER.warning("Label value truncated: " + value); + return value.substring(0, MAX_LABEL_LENGTH); + } + return value; + } + + public static Map createSessionLabels(Session session, AppDefinition appDefinition) { + SessionSpec sessionSpec = session.getSpec(); + Map labels = new HashMap<>(); + String sanitizedUser = sessionSpec.getUser().replaceAll("@", "_at_").replaceAll("[^a-zA-Z0-9]", "_"); + + labels.put(LABEL_KEY_SESSION, LABEL_VALUE_SESSION); + labels.put(LABEL_KEY_THEIACLOUD, LABEL_VALUE_THEIACLOUD); + labels.put(LABEL_KEY_USER, truncateLabelValue(sanitizedUser)); + labels.put(LABEL_KEY_APPDEF, truncateLabelValue(appDefinition.getSpec().getName())); + labels.put(LABEL_KEY_SESSION_NAME, truncateLabelValue(sessionSpec.getName())); + labels.put(LABEL_KEY_SESSION_UUID, truncateLabelValue(session.getMetadata().getUid())); + + return labels; + } + + /** + * Returns the set of label keys that are specific to a specific session, i.e. the user, session name, and session + * UUID keys. + * + * @return The session specific label keys. + */ + public static Set getSessionSpecificLabelKeys() { + return Set.of(LABEL_KEY_SESSION_NAME, LABEL_KEY_SESSION_UUID, LABEL_KEY_USER); + } +} diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/util/NamingUtil.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/util/NamingUtil.java index 242468a47..29887543b 100644 --- a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/util/NamingUtil.java +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/util/NamingUtil.java @@ -31,6 +31,12 @@ public final class NamingUtil { public static final char VALID_NAME_SUFFIX = 'z'; + /** + * Prefix for names generated based on an app definition and an index. These names are typically used for objects + * related to eagerly started instance pods (e.g. services or deployments). + */ + public static final String APP_DEFINITION_INSTANCE_PREFIX = "instance-"; + private static final Locale US_LOCALE = new Locale("en", "US"); private NamingUtil() { @@ -41,7 +47,22 @@ private NamingUtil() { * @see NamingUtil#createName(AppDefinition, int, String) */ public static String createName(AppDefinition appDefinition, int instance) { - return createName(appDefinition, instance); + return createName(appDefinition, instance, null); + } + + /** + * Creates a string that can be used as a name for a Kubernetes object with a suffix. The suffix is always preserved + * in full and other parts are shortened accordingly to stay within length limits. + * + * @param appDefinition the {@link AppDefinition} + * @param instance instance id + * @param suffix the suffix to append to the name. Must not be null or blank. + * @return the name with suffix + */ + public static String createNameWithSuffix(AppDefinition appDefinition, int instance, String suffix) { + String prefix = APP_DEFINITION_INSTANCE_PREFIX + instance; + return createNameWithSuffix(prefix, null, appDefinition.getSpec().getName(), + appDefinition.getMetadata().getUid(), suffix); } /** @@ -56,10 +77,10 @@ public static String createName(AppDefinition appDefinition, int instance) { * same type for an AppDefinition. *

*

- * The created name contains a "session" prefix, the session's user and app definition, the identifier (if given), - * and the last segment of the Session's UID. User, app definition and identifier are shortened to keep the name - * within Kubernetes' character limit (63) minus 6 characters. The latter allows Kubernetes to add 6 characters at - * the end of deployment pod names while the pod names pod names will still contain the whole name of the deployment + * The created name contains a "instance-" prefix, instance number, the identifier (if given), and the last segment + * of the App Definition's UID. User, app definition and identifier are shortened to keep the name within + * Kubernetes' character limit (63) minus 6 characters. The latter allows Kubernetes to add 6 characters at the end + * of deployment pod names while the pod names pod names will still contain the whole name of the deployment *

* * @param appDefinition the {@link AppDefinition} @@ -69,7 +90,7 @@ public static String createName(AppDefinition appDefinition, int instance) { * @return the name */ public static String createName(AppDefinition appDefinition, int instance, String identifier) { - String prefix = "instance-" + instance; + String prefix = APP_DEFINITION_INSTANCE_PREFIX + instance; return createName(prefix, identifier, null, appDefinition.getSpec().getName(), appDefinition.getMetadata().getUid()); } @@ -82,6 +103,19 @@ public static String createName(Session session) { session.getMetadata().getUid()); } + /** + * Creates a string that can be used as a name for a Kubernetes object with a suffix. The suffix is always preserved + * in full and other parts are shortened accordingly to stay within length limits. + * + * @param session the {@link Session} + * @param suffix the suffix to append to the name. Must not be null or blank. + * @return the name with suffix + */ + public static String createNameWithSuffix(Session session, String suffix) { + return createNameWithSuffix("session", session.getSpec().getUser(), session.getSpec().getAppDefinition(), + session.getMetadata().getUid(), suffix); + } + /** * Creates a string that can be used as a name for a Kubernetes object. When the same arguments are passed to this * method again, the resulting name will be the same. For different arguments the resulting name will be unique in @@ -150,6 +184,54 @@ public static String createName(Workspace workspace, String identifier) { workspace.getMetadata().getUid()); } + /** + * Builds a valid Kubernetes object names with a suffix. The suffix is always preserved in full and other parts are + * shortened accordingly to stay within length limits. + * + * @param prefix The prefix to start the object name with. Should start with a letter and be at most 13 + * characters long. Longer prefixes are possible but might lead to other info being cut short. + * @param user The user, may be null + * @param appDefinition The app definition name, may be null + * @param uid a unique Kubernetes object id that the name relates to. I.e. the UID of a session when + * creating the name of a session deployment. + * @param suffix the suffix to append to the name. Must not be null or blank. + * @return the joined and valid Kubernetes name with suffix + */ + private static String createNameWithSuffix(String prefix, String user, String appDefinition, String uid, + String suffix) { + /* + * Kubenertes UIDs are standardized UUIDs/GUIDs. This means the uid string will have a length of 36. We take the + * last segment with length 12 to generate unique names for each Session even if user and app definition are the + * same. + */ + String shortUid = trimUid(uid); + + // If the user is an email address, only take the part before the @ sign because + // this is usually sufficient to identify the user. + String userName = user != null ? user.split("@")[0] : null; + + // Calculate available space considering the suffix + String validSuffix = suffix != null && !suffix.isBlank() ? suffix : null; + if (validSuffix == null) { + throw new IllegalArgumentException("Suffix must not be null or blank"); + } + + int suffixLength = validSuffix.length() + 1; // +1 for the "-" separator + int availableLength = VALID_NAME_LIMIT - suffixLength; + + // Calculate lengths for the variable parts, ensuring we reserve space for prefix, uid, and separators + int fixedPartsLength = prefix.length() + 1 + shortUid.length(); // +1 for separator before uid + int availableForVariableParts = Math.max(0, availableLength - fixedPartsLength); + + // Divide remaining space between user and app definition + int infoSegmentLength = availableForVariableParts > 0 ? Math.max(1, availableForVariableParts / 2) : 0; + + String shortUserName = trimLength(userName, infoSegmentLength); + String shortAppDef = trimLength(appDefinition, infoSegmentLength); + + return asValidName(prefix, shortUserName, shortAppDef, shortUid, validSuffix); + } + /** * Builds a valid Kubernetes object names. Except for the prefix, all segments are limited to a fixed number of * characters to ensure that the resulting name includes information from all parameters. @@ -174,7 +256,7 @@ private static String createName(String prefix, String identifier, String user, // If the user is an email address, only take the part before the @ sign because // this is usually sufficient to identify the user. - String userName = user.split("@")[0]; + String userName = user != null ? user.split("@")[0] : null; int infoSegmentLength; String shortenedIdentifier = null; @@ -200,8 +282,8 @@ private static String trimUid(String uid) { } private static String trimLength(String text, int maxLength) { - if (text == null) { - return text; + if (text == null || maxLength <= 0) { + return null; } return text.substring(0, Math.min(text.length(), maxLength)); } diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/util/SessionUtil.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/util/SessionUtil.java index be64a2271..59c8debfa 100644 --- a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/util/SessionUtil.java +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/util/SessionUtil.java @@ -15,10 +15,99 @@ ********************************************************************************/ package org.eclipse.theia.cloud.common.util; +import java.util.Optional; + +import org.eclipse.theia.cloud.common.k8s.client.TheiaCloudClient; +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinition; +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinitionSpec; +import org.eclipse.theia.cloud.common.k8s.resource.session.Session; + public final class SessionUtil { private SessionUtil() { // util } + /** + * Get the cluster URL of the session pod, if available. This is the cluster internal URL of the pod. + * + * @param client The Theia Cloud K8s client to use for the request. + * @param session The session to get the cluster IP for. + * @return The cluster IP of the session pod, if available. + */ + public static Optional getClusterURL(TheiaCloudClient client, Session session) { + Optional ip = getClusterIP(client, session); + Optional port = getPort(client, session); + if (ip.isPresent() && port.isPresent()) { + return Optional.of("http://" + ip.get() + ":" + port.get()); + } + return Optional.empty(); + } + + /** + * Get the internal cluster URL of the session pod, if available. This is the cluster internal URL of the internal + * service that bypasses OAuth2 proxy for service-to-service communication. + * + * @param client The Theia Cloud K8s client to use for the request. + * @param session The session to get the internal cluster IP for. + * @return The internal cluster URL of the session pod, if available. + */ + public static Optional getInternalClusterURL(TheiaCloudClient client, Session session) { + Optional ip = getInternalClusterIP(client, session); + Optional port = getInternalPort(); + if (ip.isPresent() && port.isPresent()) { + return Optional.of("http://" + ip.get() + ":" + port.get()); + } + return Optional.empty(); + } + + /** + * Get the cluster IP of the session pod, if available. The cluster IP is the internal IP address of the pod. + * + * @param client The Theia Cloud K8s client to use for the request. + * @param session The session to get the cluster IP for. + * @return The cluster IP of the session pod, if available. + */ + public static Optional getClusterIP(TheiaCloudClient client, Session session) { + Optional sessionIP = client.getClusterIPFromSessionName(session.getSpec().getName()); + return sessionIP; + } + + /** + * Get the internal cluster IP of the session pod, if available. The internal cluster IP is the IP address of the + * internal service that bypasses OAuth2 proxy for service-to-service communication. + * + * @param client The Theia Cloud K8s client to use for the request. + * @param session The session to get the internal cluster IP for. + * @return The internal cluster IP of the session pod, if available. + */ + public static Optional getInternalClusterIP(TheiaCloudClient client, Session session) { + Optional sessionIP = client.getInternalClusterIPFromSessionName(session.getSpec().getName()); + return sessionIP; + } + + /** + * Get the port that the session's Theia application is running on. This does not consider the port that the OAuth + * proxy runs on which is target by external connections. + * + * @param client The Theia Cloud K8s client to use for the request. + * @param session The session to get the port for. + * @return The port of the session pod, if available. + */ + public static Optional getPort(TheiaCloudClient client, Session session) { + String appDefinitionId = session.getSpec().getAppDefinition(); + return client.appDefinitions().get(appDefinitionId)// + .map(AppDefinition::getSpec)// + .map(AppDefinitionSpec::getPort); + } + + /** + * Get the internal service port for service-to-service communication. This is a fixed port (3001) that the internal + * services use to bypass OAuth2 proxy. + * + * @return The internal service port (3001). + */ + public static Optional getInternalPort() { + return Optional.of(3001); + } } diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/util/TheiaCloudError.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/util/TheiaCloudError.java index 297fe346f..a25600f4f 100644 --- a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/util/TheiaCloudError.java +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/util/TheiaCloudError.java @@ -47,6 +47,10 @@ public class TheiaCloudError { public static final TheiaCloudError SESSION_USER_NO_SESSIONS = new TheiaCloudError(554, "No sessions allowed for this user."); + // server errors: 58x optional features + public static final TheiaCloudError CONFIG_STORE_NOT_AVAILABLE = new TheiaCloudError(580, + "The Theia Cloud Config Store is not available. It needs to be installed in the application."); + private final int code; private final String reason; diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/resources/META-INF/services/io.fabric8.kubernetes.api.model.KubernetesResource b/java/common/org.eclipse.theia.cloud.common/src/main/resources/META-INF/services/io.fabric8.kubernetes.api.model.KubernetesResource index 1ea803012..8588ba6ab 100644 --- a/java/common/org.eclipse.theia.cloud.common/src/main/resources/META-INF/services/io.fabric8.kubernetes.api.model.KubernetesResource +++ b/java/common/org.eclipse.theia.cloud.common/src/main/resources/META-INF/services/io.fabric8.kubernetes.api.model.KubernetesResource @@ -1,4 +1,5 @@ org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinition +org.eclipse.theia.cloud.common.k8s.resource.appdefinition.v1beta10.AppDefinitionV1beta10 org.eclipse.theia.cloud.common.k8s.resource.appdefinition.v1beta9.AppDefinitionV1beta9 org.eclipse.theia.cloud.common.k8s.resource.appdefinition.v1beta8.AppDefinitionV1beta8 org.eclipse.theia.cloud.common.k8s.resource.session.Session diff --git a/java/common/org.eclipse.theia.cloud.common/src/test/java/org/eclipse/theia/cloud/common/serialization/SensitiveDataSerializerTests.java b/java/common/org.eclipse.theia.cloud.common/src/test/java/org/eclipse/theia/cloud/common/serialization/SensitiveDataSerializerTests.java new file mode 100644 index 000000000..a93a2528d --- /dev/null +++ b/java/common/org.eclipse.theia.cloud.common/src/test/java/org/eclipse/theia/cloud/common/serialization/SensitiveDataSerializerTests.java @@ -0,0 +1,189 @@ +package org.eclipse.theia.cloud.common.serialization; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.type.TypeFactory; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.HashMap; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Unit tests for {@link SensitiveDataSerializer}. + */ +class SensitiveDataSerializerTest { + + private JsonGenerator jsonGenerator; + private SerializerProvider serializerProvider; + private TypeFactory typeFactory; + + @BeforeEach + void setUp() { + jsonGenerator = mock(JsonGenerator.class); + serializerProvider = mock(SerializerProvider.class); + typeFactory = TypeFactory.defaultInstance(); + } + + @Test + void serialize_StringType() throws IOException { + JavaType stringType = typeFactory.constructType(String.class); + SensitiveDataSerializer serializer = new SensitiveDataSerializer(stringType); + + serializer.serialize("test", jsonGenerator, serializerProvider); + verify(jsonGenerator).writeString(SensitiveDataSerializer.REDACTED_STRING); + } + + @Test + void serialize_StringType_Null() throws IOException { + JavaType stringType = typeFactory.constructType(String.class); + SensitiveDataSerializer serializer = new SensitiveDataSerializer(stringType); + + serializer.serialize(null, jsonGenerator, serializerProvider); + verify(jsonGenerator).writeString(SensitiveDataSerializer.REDACTED_STRING); + } + + @Test + void serialize_MapType() throws IOException { + JavaType mapType = typeFactory.constructMapType(HashMap.class, String.class, Object.class); + SensitiveDataSerializer serializer = new SensitiveDataSerializer(mapType); + + serializer.serialize(new HashMap<>(), jsonGenerator, serializerProvider); + verify(jsonGenerator).writeStartObject(); + verify(jsonGenerator).writeEndObject(); + } + + @Test + void serialize_MapType_Null() throws IOException { + JavaType mapType = typeFactory.constructMapType(HashMap.class, String.class, Object.class); + SensitiveDataSerializer serializer = new SensitiveDataSerializer(mapType); + + serializer.serialize(null, jsonGenerator, serializerProvider); + verify(jsonGenerator).writeStartObject(); + verify(jsonGenerator).writeEndObject(); + } + + @Test + void serialize_ArrayType() throws IOException { + JavaType arrayType = typeFactory.constructArrayType(String.class); + SensitiveDataSerializer serializer = new SensitiveDataSerializer(arrayType); + + serializer.serialize(new String[] { "test" }, jsonGenerator, serializerProvider); + verify(jsonGenerator).writeStartArray(); + verify(jsonGenerator).writeEndArray(); + } + + @Test + void serialize_ArrayType_Null() throws IOException { + JavaType arrayType = typeFactory.constructArrayType(String.class); + SensitiveDataSerializer serializer = new SensitiveDataSerializer(arrayType); + + serializer.serialize(null, jsonGenerator, serializerProvider); + verify(jsonGenerator).writeStartArray(); + verify(jsonGenerator).writeEndArray(); + } + + @Test + void serialize_NumberType() throws IOException { + JavaType numberType = typeFactory.constructType(Integer.class); + SensitiveDataSerializer serializer = new SensitiveDataSerializer(numberType); + + serializer.serialize(123, jsonGenerator, serializerProvider); + verify(jsonGenerator).writeNumber(SensitiveDataSerializer.REDACTED_NUMBER); + } + + @Test + void serialize_NumberType_Null() throws IOException { + JavaType numberType = typeFactory.constructType(Integer.class); + SensitiveDataSerializer serializer = new SensitiveDataSerializer(numberType); + + serializer.serialize(null, jsonGenerator, serializerProvider); + verify(jsonGenerator).writeNumber(SensitiveDataSerializer.REDACTED_NUMBER); + } + + @Test + void serialize_PrimitiveIntType() throws IOException { + JavaType intType = typeFactory.constructType(int.class); + SensitiveDataSerializer serializer = new SensitiveDataSerializer(intType); + + serializer.serialize(123, jsonGenerator, serializerProvider); + verify(jsonGenerator).writeNumber(SensitiveDataSerializer.REDACTED_NUMBER); + } + + @Test + void serialize_PrimitiveLongType() throws IOException { + JavaType longType = typeFactory.constructType(long.class); + SensitiveDataSerializer serializer = new SensitiveDataSerializer(longType); + + serializer.serialize(123L, jsonGenerator, serializerProvider); + verify(jsonGenerator).writeNumber(SensitiveDataSerializer.REDACTED_NUMBER); + } + + @Test + void serialize_PrimitiveFloatType() throws IOException { + JavaType floatType = typeFactory.constructType(float.class); + SensitiveDataSerializer serializer = new SensitiveDataSerializer(floatType); + + serializer.serialize(123.45f, jsonGenerator, serializerProvider); + verify(jsonGenerator).writeNumber(SensitiveDataSerializer.REDACTED_NUMBER); + } + + @Test + void serialize_PrimitiveDoubleType() throws IOException { + JavaType doubleType = typeFactory.constructType(double.class); + SensitiveDataSerializer serializer = new SensitiveDataSerializer(doubleType); + + serializer.serialize(123.45, jsonGenerator, serializerProvider); + verify(jsonGenerator).writeNumber(SensitiveDataSerializer.REDACTED_NUMBER); + } + + @Test + void serialize_BooleanType() throws IOException { + JavaType booleanType = typeFactory.constructType(Boolean.class); + SensitiveDataSerializer serializer = new SensitiveDataSerializer(booleanType); + + serializer.serialize(true, jsonGenerator, serializerProvider); + verify(jsonGenerator).writeBoolean(SensitiveDataSerializer.REDACTED_BOOLEAN); + } + + @Test + void serialize_BooleanType_Null() throws IOException { + JavaType booleanType = typeFactory.constructType(Boolean.class); + SensitiveDataSerializer serializer = new SensitiveDataSerializer(booleanType); + + serializer.serialize(null, jsonGenerator, serializerProvider); + verify(jsonGenerator).writeBoolean(SensitiveDataSerializer.REDACTED_BOOLEAN); + } + + @Test + void serialize_PrimitiveBooleanType() throws IOException { + JavaType booleanType = typeFactory.constructType(boolean.class); + SensitiveDataSerializer serializer = new SensitiveDataSerializer(booleanType); + + serializer.serialize(true, jsonGenerator, serializerProvider); + verify(jsonGenerator).writeBoolean(SensitiveDataSerializer.REDACTED_BOOLEAN); + } + + @Test + void serialize_UnsupportedType() throws IOException { + JavaType unsupportedType = typeFactory.constructType(Object.class); + SensitiveDataSerializer serializer = new SensitiveDataSerializer(unsupportedType); + + serializer.serialize(new Object(), jsonGenerator, serializerProvider); + verify(jsonGenerator).writeNull(); + } + + @Test + void serialize_UnsupportedType_Null() throws IOException { + JavaType unsupportedType = typeFactory.constructType(Object.class); + SensitiveDataSerializer serializer = new SensitiveDataSerializer(unsupportedType); + + serializer.serialize(null, jsonGenerator, serializerProvider); + verify(jsonGenerator).writeNull(); + } +} \ No newline at end of file diff --git a/java/common/org.eclipse.theia.cloud.common/src/test/java/org/eclipse/theia/cloud/common/util/NamingUtilTests.java b/java/common/org.eclipse.theia.cloud.common/src/test/java/org/eclipse/theia/cloud/common/util/NamingUtilTests.java index d68f2d0a3..d0284d8f8 100644 --- a/java/common/org.eclipse.theia.cloud.common/src/test/java/org/eclipse/theia/cloud/common/util/NamingUtilTests.java +++ b/java/common/org.eclipse.theia.cloud.common/src/test/java/org/eclipse/theia/cloud/common/util/NamingUtilTests.java @@ -16,7 +16,10 @@ package org.eclipse.theia.cloud.common.util; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinition; +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinitionSpec; import org.eclipse.theia.cloud.common.k8s.resource.session.Session; import org.eclipse.theia.cloud.common.k8s.resource.session.SessionSpec; import org.eclipse.theia.cloud.common.k8s.resource.workspace.Workspace; @@ -30,6 +33,22 @@ */ class NamingUtilTests { + @Test + void createName_AppDefinitionAndInstace() { + AppDefinition appDefinition = createAppDefinition(); + + String result = NamingUtil.createName(appDefinition, 1); + assertEquals("instance-1-some-app-definiti-381261d79c23", result); + } + + @Test + void createName_AppDefinitionAndInstaceAndIdentifier() { + AppDefinition appDefinition = createAppDefinition(); + + String result = NamingUtil.createName(appDefinition, 1, "longidentifier"); + assertEquals("instance-1-longidentif-some-app-de-381261d79c23", result); + } + @Test void createName_SessionAndNullIdentifier() { Session session = createSession(); @@ -94,6 +113,74 @@ void createName_WorkspaceAndIdentifier() { assertEquals("ws-longidentif-some-userna-test-app-de-381261d79c23", result); } + @Test + void createNameWithSuffix_AppDefinitionAndInstance() { + AppDefinition appDefinition = createAppDefinition(); + + String result = NamingUtil.createNameWithSuffix(appDefinition, 1, "internal"); + assertEquals("instance-1-some-app-defini-381261d79c23-internal", result); + } + + @Test + void createNameWithSuffix_Session() { + Session session = createSession(); + + String result = NamingUtil.createNameWithSuffix(session, "internal"); + assertEquals("session-some-username-test-app-definit-426930ea37d7-internal", result); + } + + @Test + void createNameWithSuffix_AppDefinitionWithLongSuffix() { + AppDefinition appDefinition = createAppDefinition(); + + String result = NamingUtil.createNameWithSuffix(appDefinition, 1, "very-long-suffix-for-testing"); + // Ensure the suffix is preserved and the total length doesn't exceed limit + assertTrue(result.endsWith("-very-long-suffix-for-testing")); + assertTrue(result.length() <= NamingUtil.VALID_NAME_LIMIT); + assertTrue(result.startsWith("instance-1")); + } + + @Test + void createNameWithSuffix_VerifyLengthAndSuffixPreservation() { + Session session = createSession(); + AppDefinition appDefinition = createAppDefinition(); + + // Test with regular suffix + String sessionResult = NamingUtil.createNameWithSuffix(session, "internal"); + String appDefResult = NamingUtil.createNameWithSuffix(appDefinition, 1, "internal"); + + // Verify all results end with the suffix + assertTrue(sessionResult.endsWith("-internal")); + assertTrue(appDefResult.endsWith("-internal")); + + // Verify all results are within length limit + assertTrue(sessionResult.length() <= NamingUtil.VALID_NAME_LIMIT); + assertTrue(appDefResult.length() <= NamingUtil.VALID_NAME_LIMIT); + + // Verify all results start with expected prefixes + assertTrue(sessionResult.startsWith("session-")); + assertTrue(appDefResult.startsWith("instance-1-")); + + // Verify the results are still valid Kubernetes names (no invalid characters) + assertTrue(sessionResult.matches("[a-z0-9-]+")); + assertTrue(appDefResult.matches("[a-z0-9-]+")); + } + + private AppDefinition createAppDefinition() { + AppDefinition appDefinition = new AppDefinition(); + ObjectMeta objectMeta = new ObjectMeta(); + objectMeta.setUid("6f1a8966-4d5a-41dc-82ba-381261d79c23"); + appDefinition.setMetadata(objectMeta); + AppDefinitionSpec spec = new AppDefinitionSpec() { + @Override + public String getName() { + return "some-app-definition"; + } + }; + appDefinition.setSpec(spec); + return appDefinition; + } + private Session createSession() { Session session = new Session(); ObjectMeta objectMeta = new ObjectMeta(); diff --git a/java/conversion/org.eclipse.theia.cloud.conversion/README.md b/java/conversion/org.eclipse.theia.cloud.conversion/README.md index 38d3514c8..7e6014674 100644 --- a/java/conversion/org.eclipse.theia.cloud.conversion/README.md +++ b/java/conversion/org.eclipse.theia.cloud.conversion/README.md @@ -49,7 +49,7 @@ Or, if you don't have GraalVM installed, you can run the native executable build ./mvnw package -Pnative -Dquarkus.native.container-build=true ``` -You can then execute your native executable with: `./target/conversion-webhook-1.0.0-SNAPSHOT-runner` +You can then execute your native executable with: `./target/conversion-webhook-1.2.0-SNAPSHOT-runner` If you want to learn more about building native executables, please consult https://quarkus.io/guides/maven-tooling. diff --git a/java/conversion/org.eclipse.theia.cloud.conversion/pom.xml b/java/conversion/org.eclipse.theia.cloud.conversion/pom.xml index 5bfd29a66..07d136338 100644 --- a/java/conversion/org.eclipse.theia.cloud.conversion/pom.xml +++ b/java/conversion/org.eclipse.theia.cloud.conversion/pom.xml @@ -9,7 +9,7 @@ org.eclipse.theia.cloud conf - 0.12.0-SNAPSHOT + 1.2.0-SNAPSHOT ../../common/maven-conf/ @@ -33,7 +33,7 @@ io.quarkus - quarkus-resteasy-reactive-jackson + quarkus-rest-jackson @@ -45,7 +45,7 @@ org.eclipse.theia.cloud common - 0.12.0-SNAPSHOT + 1.2.0-SNAPSHOT @@ -81,8 +81,8 @@ maven-compiler-plugin ${maven-compiler-plugin.version} - 17 - 17 + 21 + 21 -parameters @@ -135,4 +135,4 @@ - \ No newline at end of file + diff --git a/java/conversion/org.eclipse.theia.cloud.conversion/src/main/java/org/eclipse/theia/cloud/conversion/ConversionEndpoint.java b/java/conversion/org.eclipse.theia.cloud.conversion/src/main/java/org/eclipse/theia/cloud/conversion/ConversionEndpoint.java index cb7149ee6..1b46fe7d2 100644 --- a/java/conversion/org.eclipse.theia.cloud.conversion/src/main/java/org/eclipse/theia/cloud/conversion/ConversionEndpoint.java +++ b/java/conversion/org.eclipse.theia.cloud.conversion/src/main/java/org/eclipse/theia/cloud/conversion/ConversionEndpoint.java @@ -16,6 +16,7 @@ package org.eclipse.theia.cloud.conversion; import org.eclipse.theia.cloud.conversion.mappers.appdefinition.AppDefinitionV1beta10Mapper; +import org.eclipse.theia.cloud.conversion.mappers.appdefinition.AppDefinitionV1beta11Mapper; import org.eclipse.theia.cloud.conversion.mappers.appdefinition.AppDefinitionV1beta8Mapper; import org.eclipse.theia.cloud.conversion.mappers.appdefinition.AppDefinitionV1beta9Mapper; import org.eclipse.theia.cloud.conversion.mappers.session.SessionV1beta6Mapper; @@ -48,6 +49,7 @@ public ConversionEndpoint() { appDefinitionController.registerMapper(new AppDefinitionV1beta8Mapper()); appDefinitionController.registerMapper(new AppDefinitionV1beta9Mapper()); appDefinitionController.registerMapper(new AppDefinitionV1beta10Mapper()); + appDefinitionController.registerMapper(new AppDefinitionV1beta11Mapper()); this.workspaceController = new ConversionController(); workspaceController.registerMapper(new WorkspaceV1beta3Mapper()); diff --git a/java/conversion/org.eclipse.theia.cloud.conversion/src/main/java/org/eclipse/theia/cloud/conversion/mappers/appdefinition/AppDefinitionV1beta10Mapper.java b/java/conversion/org.eclipse.theia.cloud.conversion/src/main/java/org/eclipse/theia/cloud/conversion/mappers/appdefinition/AppDefinitionV1beta10Mapper.java index 196517686..4f43669b2 100644 --- a/java/conversion/org.eclipse.theia.cloud.conversion/src/main/java/org/eclipse/theia/cloud/conversion/mappers/appdefinition/AppDefinitionV1beta10Mapper.java +++ b/java/conversion/org.eclipse.theia.cloud.conversion/src/main/java/org/eclipse/theia/cloud/conversion/mappers/appdefinition/AppDefinitionV1beta10Mapper.java @@ -15,23 +15,24 @@ ********************************************************************************/ package org.eclipse.theia.cloud.conversion.mappers.appdefinition; -import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinition; import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.hub.AppDefinitionHub; +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.v1beta10.AppDefinitionV1beta10; import io.javaoperatorsdk.webhook.conversion.Mapper; import io.javaoperatorsdk.webhook.conversion.TargetVersion; +@SuppressWarnings("deprecation") @TargetVersion("v1beta10") -public class AppDefinitionV1beta10Mapper implements Mapper { +public class AppDefinitionV1beta10Mapper implements Mapper { @Override - public AppDefinitionHub toHub(AppDefinition resource) { + public AppDefinitionHub toHub(AppDefinitionV1beta10 resource) { return new AppDefinitionHub(resource); } @Override - public AppDefinition fromHub(AppDefinitionHub hub) { - return new AppDefinition(hub); + public AppDefinitionV1beta10 fromHub(AppDefinitionHub hub) { + return new AppDefinitionV1beta10(hub); } } diff --git a/java/conversion/org.eclipse.theia.cloud.conversion/src/main/java/org/eclipse/theia/cloud/conversion/mappers/appdefinition/AppDefinitionV1beta11Mapper.java b/java/conversion/org.eclipse.theia.cloud.conversion/src/main/java/org/eclipse/theia/cloud/conversion/mappers/appdefinition/AppDefinitionV1beta11Mapper.java new file mode 100644 index 000000000..815c19b15 --- /dev/null +++ b/java/conversion/org.eclipse.theia.cloud.conversion/src/main/java/org/eclipse/theia/cloud/conversion/mappers/appdefinition/AppDefinitionV1beta11Mapper.java @@ -0,0 +1,37 @@ +/******************************************************************************** + * Copyright (C) 2023 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.conversion.mappers.appdefinition; + +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinition; +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.hub.AppDefinitionHub; + +import io.javaoperatorsdk.webhook.conversion.Mapper; +import io.javaoperatorsdk.webhook.conversion.TargetVersion; + +@TargetVersion("v1beta11") +public class AppDefinitionV1beta11Mapper implements Mapper { + + @Override + public AppDefinitionHub toHub(AppDefinition resource) { + return new AppDefinitionHub(resource); + } + + @Override + public AppDefinition fromHub(AppDefinitionHub hub) { + return new AppDefinition(hub); + } + +} diff --git a/java/conversion/org.eclipse.theia.cloud.conversion/src/main/resources/application.properties b/java/conversion/org.eclipse.theia.cloud.conversion/src/main/resources/application.properties index a269f8d04..eed059354 100644 --- a/java/conversion/org.eclipse.theia.cloud.conversion/src/main/resources/application.properties +++ b/java/conversion/org.eclipse.theia.cloud.conversion/src/main/resources/application.properties @@ -1,2 +1,3 @@ quarkus.http.ssl.certificate.files=/etc/webhook/certs/tls.crt -quarkus.http.ssl.certificate.key-files=/etc/webhook/certs/tls.key \ No newline at end of file +quarkus.http.ssl.certificate.key-files=/etc/webhook/certs/tls.key +quarkus.http.ssl.certificate.reload-period=604800 \ No newline at end of file diff --git a/java/operator/README.md b/java/operator/README.md new file mode 100644 index 000000000..40aca29fb --- /dev/null +++ b/java/operator/README.md @@ -0,0 +1,43 @@ +# Theia Cloud Operator + +This folder contains the Java projects for the Theia Cloud operator, including: + +- **Default Theia Cloud Operator**: Provides a default implementation of the operator. +- **Theia Cloud Operator Library**: The key functionality for the Theia Cloud Operator, to be used in your implementation. + +## Projects + +### Default Theia Cloud Operator + +This project implements the default Kubernetes Operator for Theia Cloud. + +#### Build and Run + +```sh +mvn clean install +java -jar target/defaultoperator-1.2.0-SNAPSHOT-jar-with-dependencies.jar +``` + +#### Debugging the Default Theia Cloud Operator + +To debug the Default Theia Cloud Operator, follow these steps: + +- **Set the Replicas**: + + - Set the replicas of the operator in your Theia Cloud installation to 0. + +- **Start the Debug Config**: + + - Launch the debug configuration with the appropriate flags for debugging. + - There are specific configurations for the [terraform test-configurations](../../terraform/test-configurations/) available. + - The configurations will prompt you for your Minikube IP address. To find out the IP address, run the following command in your terminal: + +```sh + minikube ip +``` + +_Note_: Ensure your local `kubectl` is set to the correct namespace (e.g., `theia-cloud`), as the debuggable operator will interact with your local Kubernetes cluster. + +### Theia Cloud Operator Library + +A shared library for building Theia Cloud Kubernetes operators. diff --git a/java/operator/org.eclipse.theia.cloud.defaultoperator/pom.xml b/java/operator/org.eclipse.theia.cloud.defaultoperator/pom.xml index 1a7d3eabc..b1ecdece2 100644 --- a/java/operator/org.eclipse.theia.cloud.defaultoperator/pom.xml +++ b/java/operator/org.eclipse.theia.cloud.defaultoperator/pom.xml @@ -11,7 +11,7 @@ org.eclipse.theia.cloud conf - 0.12.0-SNAPSHOT + 1.2.0-SNAPSHOT ../../common/maven-conf/ @@ -19,12 +19,12 @@ org.eclipse.theia.cloud operator - 0.12.0-SNAPSHOT + 1.2.0-SNAPSHOT org.eclipse.theia.cloud common - 0.12.0-SNAPSHOT + 1.2.0-SNAPSHOT org.json @@ -77,8 +77,8 @@ maven-compiler-plugin ${maven-compiler-plugin.version} - 17 - 17 + 21 + 21 @@ -109,4 +109,41 @@ - \ No newline at end of file + + + sentry + + + sentry + true + + + + + + io.sentry + sentry-maven-plugin + 0.10.0 + true + + aet + theia + ${env.SENTRY_AUTH_TOKEN} + https://sentry.aet.cit.tum.de + true + + + + + uploadSourceBundle + validateSdkDependencyVersions + + + + + + + + + + diff --git a/java/operator/org.eclipse.theia.cloud.defaultoperator/sentry.properties b/java/operator/org.eclipse.theia.cloud.defaultoperator/sentry.properties new file mode 100644 index 000000000..4524a9fc1 --- /dev/null +++ b/java/operator/org.eclipse.theia.cloud.defaultoperator/sentry.properties @@ -0,0 +1,6 @@ +# Add your Sentry DSN here +dsn=https://a3851baf2ece0c6bec13e1c99fd3111f@sentry.aet.cit.tum.de/13 +# Add data like request headers and IP for users, +# see https://docs.sentry.io/platforms/java/data-management/data-collected/ for more info +send-default-pii=false +traces-sample-rate=1.0 \ No newline at end of file diff --git a/java/operator/org.eclipse.theia.cloud.defaultoperator/src/main/java/org/eclipse/theia/cloud/defaultoperator/DefaultTheiaCloudOperatorLauncher.java b/java/operator/org.eclipse.theia.cloud.defaultoperator/src/main/java/org/eclipse/theia/cloud/defaultoperator/DefaultTheiaCloudOperatorLauncher.java index c07b8736d..da51aa634 100644 --- a/java/operator/org.eclipse.theia.cloud.defaultoperator/src/main/java/org/eclipse/theia/cloud/defaultoperator/DefaultTheiaCloudOperatorLauncher.java +++ b/java/operator/org.eclipse.theia.cloud.defaultoperator/src/main/java/org/eclipse/theia/cloud/defaultoperator/DefaultTheiaCloudOperatorLauncher.java @@ -15,14 +15,33 @@ ********************************************************************************/ package org.eclipse.theia.cloud.defaultoperator; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.eclipse.theia.cloud.operator.LeaderElectionTheiaCloudOperatorLauncher; import org.eclipse.theia.cloud.operator.TheiaCloudOperatorArguments; import org.eclipse.theia.cloud.operator.di.AbstractTheiaCloudOperatorModule; +import io.sentry.Sentry; + public class DefaultTheiaCloudOperatorLauncher extends LeaderElectionTheiaCloudOperatorLauncher { - public static void main(String[] args) throws InterruptedException { - new DefaultTheiaCloudOperatorLauncher().runMain(args); + private static final Logger LOGGER = LogManager.getLogger(DefaultTheiaCloudOperatorLauncher.class); + + public static void main(String[] args) { + try { + Sentry.configureScope(scope -> { + scope.setTag("component", "operator"); + }); + new DefaultTheiaCloudOperatorLauncher().runMain(args); + } catch (InterruptedException e) { + LOGGER.error("Operator interrupted", e); + Thread.currentThread().interrupt(); + System.exit(1); + } catch (Throwable t) { + LOGGER.error("Fatal error starting operator", t); + t.printStackTrace(); + System.exit(1); + } } @Override diff --git a/java/operator/org.eclipse.theia.cloud.operator/pom.xml b/java/operator/org.eclipse.theia.cloud.operator/pom.xml index 8e3e081c7..c49a34589 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/pom.xml +++ b/java/operator/org.eclipse.theia.cloud.operator/pom.xml @@ -11,7 +11,7 @@ org.eclipse.theia.cloud conf - 0.12.0-SNAPSHOT + 1.2.0-SNAPSHOT ../../common/maven-conf/ @@ -19,7 +19,7 @@ org.eclipse.theia.cloud common - 0.12.0-SNAPSHOT + 1.2.0-SNAPSHOT org.json @@ -51,19 +51,38 @@ log4j-core ${log4j.version} - info.picocli picocli ${picocli.version} - org.slf4j slf4j-simple ${slf4j.version} - + + io.sentry + sentry + 8.0.0 + + + io.sentry + sentry-okhttp + 8.0.0 + + + org.junit.jupiter + junit-jupiter + ${junit-jupiter.version} + test + + + org.mockito + mockito-core + ${mockito.version} + test + @@ -72,11 +91,47 @@ maven-compiler-plugin ${maven-compiler-plugin.version} - 17 - 17 + 21 + 21 - \ No newline at end of file + + + sentry + + + sentry + true + + + + + + io.sentry + sentry-maven-plugin + 0.10.0 + true + + aet + theia + ${env.SENTRY_AUTH_TOKEN} + https://sentry.aet.cit.tum.de + + + + + uploadSourceBundle + validateSdkDependencyVersions + + + + + + + + + + diff --git a/java/operator/org.eclipse.theia.cloud.operator/sentry.properties b/java/operator/org.eclipse.theia.cloud.operator/sentry.properties new file mode 120000 index 000000000..535aae8f0 --- /dev/null +++ b/java/operator/org.eclipse.theia.cloud.operator/sentry.properties @@ -0,0 +1 @@ +../org.eclipse.theia.cloud.defaultoperator/sentry.properties \ No newline at end of file diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/BasicTheiaCloudOperator.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/BasicTheiaCloudOperator.java index 2b8d1125b..56023f51b 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/BasicTheiaCloudOperator.java +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/BasicTheiaCloudOperator.java @@ -20,13 +20,18 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.ArrayDeque; +import java.util.Deque; import java.util.LinkedHashSet; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import org.apache.logging.log4j.LogManager; @@ -35,20 +40,30 @@ import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinition; import org.eclipse.theia.cloud.common.k8s.resource.session.Session; import org.eclipse.theia.cloud.common.k8s.resource.workspace.Workspace; +import org.eclipse.theia.cloud.common.tracing.TraceContext; +import org.eclipse.theia.cloud.common.tracing.Tracing; import org.eclipse.theia.cloud.operator.handler.appdef.AppDefinitionHandler; import org.eclipse.theia.cloud.operator.handler.session.SessionHandler; import org.eclipse.theia.cloud.operator.handler.ws.WorkspaceHandler; import org.eclipse.theia.cloud.operator.plugins.OperatorPlugin; import org.eclipse.theia.cloud.operator.util.SpecWatch; +import io.sentry.ISpan; +import io.sentry.Sentry; + import com.google.inject.Inject; import io.fabric8.kubernetes.client.Watcher; +import io.fabric8.kubernetes.client.KubernetesClientException; public class BasicTheiaCloudOperator implements TheiaCloudOperator { private static final ScheduledExecutorService STOP_EXECUTOR = Executors.newSingleThreadScheduledExecutor(); private static final ScheduledExecutorService WATCH_EXECUTOR = Executors.newSingleThreadScheduledExecutor(); + private static final int SESSION_EVENT_QUEUE_SIZE = 1000; + private static final long SESSION_EXECUTOR_SHUTDOWN_TIMEOUT_SECONDS = 30L; + private static final int APPDEF_INIT_MAX_RETRIES = 10; + private static final long APPDEF_INIT_RETRY_DELAY_MS = 1000L; private static final Logger LOGGER = LogManager.getLogger(BasicTheiaCloudOperator.class); @@ -80,9 +95,15 @@ public class BasicTheiaCloudOperator implements TheiaCloudOperator { private final Map sessionCache = new ConcurrentHashMap<>(); private final Set> watches = new LinkedHashSet<>(); + // Session event scheduling: per-session serialization, cross-session + // parallelism. + private ExecutorService sessionExecutor; + private final Map sessionStates = new ConcurrentHashMap<>(); + @Override public void start() { this.operatorPlugins.forEach(plugin -> plugin.start()); + initSessionExecutor(); watches.add(initAppDefinitionsAndWatchForChanges()); watches.add(initWorkspacesAndWatchForChanges()); watches.add(initSessionsAndWatchForChanges()); @@ -91,18 +112,71 @@ public void start() { WATCH_EXECUTOR.scheduleWithFixedDelay(this::lookForIdleWatches, 1, 1, TimeUnit.MINUTES); } + @Override + public void stop() { + if (sessionExecutor == null) { + return; + } + sessionExecutor.shutdown(); + try { + if (!sessionExecutor.awaitTermination(SESSION_EXECUTOR_SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + sessionExecutor.shutdownNow(); + } + } catch (InterruptedException e) { + sessionExecutor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + protected SpecWatch initAppDefinitionsAndWatchForChanges() { + for (int attempt = 1; attempt <= APPDEF_INIT_MAX_RETRIES; attempt++) { + try { + resourceClient.appDefinitions().list().forEach(this::initAppDefinition); + SpecWatch watcher = new SpecWatch<>(appDefinitionCache, this::handleAppDefnitionEvent, + "App Definition", COR_ID_APPDEFINITIONPREFIX); + resourceClient.appDefinitions().operation().watch(watcher); + return watcher; + } catch (Exception e) { + boolean retryable = isRetryableAppDefinitionInitError(e); + if (retryable && attempt < APPDEF_INIT_MAX_RETRIES) { + LOGGER.warn(formatLogMessage(TheiaCloudOperatorLauncher.COR_ID_INIT, + "AppDefinition watch init failed with retryable error (attempt " + attempt + + "/" + APPDEF_INIT_MAX_RETRIES + "), retrying in " + + APPDEF_INIT_RETRY_DELAY_MS + "ms"), + e); + sleepQuietly(APPDEF_INIT_RETRY_DELAY_MS); + continue; + } + + LOGGER.error(formatLogMessage(TheiaCloudOperatorLauncher.COR_ID_INIT, + "Error while initializing app definitions watch"), e); + System.exit(-1); + throw new IllegalStateException(); + } + } + + throw new IllegalStateException("Unreachable"); + } + + private boolean isRetryableAppDefinitionInitError(Exception e) { + Throwable current = e; + while (current != null) { + if (current instanceof KubernetesClientException kce) { + if (kce.getCode() == 429 && kce.getMessage() != null + && kce.getMessage().contains("storage is (re)initializing")) { + return true; + } + } + current = current.getCause(); + } + return false; + } + + private void sleepQuietly(long millis) { try { - resourceClient.appDefinitions().list().forEach(this::initAppDefinition); - SpecWatch watcher = new SpecWatch<>(appDefinitionCache, this::handleAppDefnitionEvent, - "App Definition", COR_ID_APPDEFINITIONPREFIX); - resourceClient.appDefinitions().operation().watch(watcher); - return watcher; - } catch (Exception e) { - LOGGER.error(formatLogMessage(TheiaCloudOperatorLauncher.COR_ID_INIT, - "Error while initializing app definitions watch"), e); - System.exit(-1); - throw new IllegalStateException(); + Thread.sleep(millis); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); } } @@ -124,7 +198,7 @@ protected SpecWatch initWorkspacesAndWatchForChanges() { protected SpecWatch initSessionsAndWatchForChanges() { try { resourceClient.sessions().list().forEach(this::initSession); - SpecWatch watcher = new SpecWatch<>(sessionCache, this::handleSessionEvent, "Session", + SpecWatch watcher = new SpecWatch<>(sessionCache, this::scheduleSessionEvent, "Session", COR_ID_SESSIONPREFIX); resourceClient.sessions().operation().watch(watcher); return watcher; @@ -152,28 +226,35 @@ protected void initWorkspace(Workspace resource) { protected void initSession(Session resource) { sessionCache.put(resource.getMetadata().getUid(), resource); String uid = resource.getMetadata().getUid(); - handleSessionEvent(Watcher.Action.ADDED, uid, TheiaCloudOperatorLauncher.COR_ID_INIT); + scheduleSessionEvent(Watcher.Action.ADDED, uid, TheiaCloudOperatorLauncher.COR_ID_INIT); + } + + protected void scheduleSessionEvent(Watcher.Action action, String uid, String correlationId) { + Session session = sessionCache.get(uid); + SessionState state = sessionStates.computeIfAbsent(uid, k -> new SessionState()); + boolean shouldSchedule = false; + synchronized (state.lock) { + state.queue.add(new SessionEvent(action, correlationId, session)); + // Ensure only one runner per session UID. + if (!state.running) { + state.running = true; + shouldSchedule = true; + } + } + if (shouldSchedule) { + sessionExecutor.execute(() -> drainSessionEvents(uid, state)); + } } protected void handleAppDefnitionEvent(Watcher.Action action, String uid, String correlationId) { try { AppDefinition appDefinition = appDefinitionCache.get(uid); switch (action) { - case ADDED: - appDefinitionAddedHandler.appDefinitionAdded(appDefinition, correlationId); - break; - case DELETED: - appDefinitionAddedHandler.appDefinitionDeleted(appDefinition, correlationId); - break; - case MODIFIED: - appDefinitionAddedHandler.appDefinitionModified(appDefinition, correlationId); - break; - case ERROR: - appDefinitionAddedHandler.appDefinitionErrored(appDefinition, correlationId); - break; - case BOOKMARK: - appDefinitionAddedHandler.appDefinitionBookmarked(appDefinition, correlationId); - break; + case ADDED -> appDefinitionAddedHandler.appDefinitionAdded(appDefinition, correlationId); + case DELETED -> appDefinitionAddedHandler.appDefinitionDeleted(appDefinition, correlationId); + case MODIFIED -> appDefinitionAddedHandler.appDefinitionModified(appDefinition, correlationId); + case ERROR -> appDefinitionAddedHandler.appDefinitionErrored(appDefinition, correlationId); + case BOOKMARK -> appDefinitionAddedHandler.appDefinitionBookmarked(appDefinition, correlationId); } } catch (Exception e) { LOGGER.error(formatLogMessage(correlationId, "Error while handling app definitions"), e); @@ -181,52 +262,153 @@ protected void handleAppDefnitionEvent(Watcher.Action action, String uid, String } protected void handleSessionEvent(Watcher.Action action, String uid, String correlationId) { + Session session = sessionCache.get(uid); + if (session == null) { + return; + } + + // Extract trace context from Session annotations (if propagated from service) + Optional traceContext = TraceContext.fromMetadata(session.getMetadata()); + + ISpan span = null; try { - Session session = sessionCache.get(uid); + String name = "session." + action.name().toLowerCase(); + String operation = action.name() + " session " + session.getSpec().getName(); + span = Tracing.continueTraceAsync(traceContext, name, operation); + span.setTag("session.name", session.getSpec().getName()); + span.setTag("app_definition", session.getSpec().getAppDefinition()); + span.setTag("user", session.getSpec().getUser()); + + span.setTag("action", action.name()); + span.setData("correlation_id", correlationId); + switch (action) { - case ADDED: - sessionHandler.sessionAdded(session, correlationId); - break; - case DELETED: - sessionHandler.sessionDeleted(session, correlationId); - break; - case MODIFIED: - sessionHandler.sessionModified(session, correlationId); - break; - case ERROR: - sessionHandler.sessionErrored(session, correlationId); - break; - case BOOKMARK: - sessionHandler.sessionBookmarked(session, correlationId); - break; + case ADDED -> sessionHandler.sessionAdded(session, correlationId, span); + case DELETED -> sessionHandler.sessionDeleted(session, correlationId, span); + case MODIFIED -> sessionHandler.sessionModified(session, correlationId, span); + case ERROR -> sessionHandler.sessionErrored(session, correlationId, span); + case BOOKMARK -> sessionHandler.sessionBookmarked(session, correlationId, span); } + + Tracing.finishSuccess(span); } catch (Exception e) { LOGGER.error(formatLogMessage(correlationId, "Error while handling sessions"), e); + if (span != null) { + Tracing.finishError(span, e); + } + if (!arguments.isContinueOnException()) { + System.exit(-1); + } + } + } + + private void handleSessionEvent(SessionEvent event) { + if (event.session == null) { + throw new IllegalArgumentException("Session must not be null"); + } + // Extract trace context from Session annotations (if propagated from service) + Optional traceContext = TraceContext.fromMetadata(event.session.getMetadata()); + + ISpan span = null; + try { + String name = "session." + event.action.name().toLowerCase(); + String operation = event.action.name() + " session " + event.session.getSpec().getName(); + span = Tracing.continueTraceAsync(traceContext, name, operation); + span.setTag("session.name", event.session.getSpec().getName()); + span.setTag("app_definition", event.session.getSpec().getAppDefinition()); + span.setTag("user", event.session.getSpec().getUser()); + + span.setTag("action", event.action.name()); + span.setData("correlation_id", event.correlationId); + + switch (event.action) { + case ADDED -> sessionHandler.sessionAdded(event.session, event.correlationId, span); + case DELETED -> sessionHandler.sessionDeleted(event.session, event.correlationId, span); + case MODIFIED -> sessionHandler.sessionModified(event.session, event.correlationId, span); + case ERROR -> sessionHandler.sessionErrored(event.session, event.correlationId, span); + case BOOKMARK -> sessionHandler.sessionBookmarked(event.session, event.correlationId, span); + } + Tracing.finishSuccess(span); + } catch (Exception e) { + LOGGER.error(formatLogMessage(event.correlationId, "Error while handling sessions"), e); + if (span != null) { + Tracing.finishError(span, e); + } if (!arguments.isContinueOnException()) { System.exit(-1); } } } + private void initSessionExecutor() { + int threads = arguments.getSessionHandlerThreads(); + sessionExecutor = new ThreadPoolExecutor(threads, threads, 0L, TimeUnit.MILLISECONDS, + new ArrayBlockingQueue<>(SESSION_EVENT_QUEUE_SIZE), new ThreadPoolExecutor.CallerRunsPolicy()); + } + + private void drainSessionEvents(String uid, SessionState state) { + while (true) { + SessionEvent event; + synchronized (state.lock) { + event = state.queue.poll(); + if (event == null) { + state.running = false; + break; + } + } + try { + handleSessionEvent(event); + } catch (IllegalArgumentException e) { + LOGGER.error(formatLogMessage(event.correlationId, "Error while handling sessions"), e); + Sentry.captureException(e); + } + } + tryCleanupSessionState(uid, state); + } + + private void tryCleanupSessionState(String uid, SessionState state) { + SessionState currentState = sessionStates.get(uid); + if (currentState == null) { + return; + } + synchronized (currentState.lock) { + if (currentState != state) { + return; + } + if (!currentState.queue.isEmpty() || currentState.running) { + return; + } + sessionStates.remove(uid, currentState); + } + } + + private static final class SessionState { + private final Deque queue = new ArrayDeque<>(); + private final Object lock = new Object(); + private boolean running; + } + + private static final class SessionEvent { + private final Watcher.Action action; + private final String correlationId; + private final Session session; + + private SessionEvent(Watcher.Action action, String correlationId, Session session) { + this.action = action; + this.correlationId = correlationId; + this.session = session; + } + } + protected void handleWorkspaceEvent(Watcher.Action action, String uid, String correlationId) { try { Workspace workspace = workspaceCache.get(uid); switch (action) { - case ADDED: - workspaceHandler.workspaceAdded(workspace, correlationId); - break; - case DELETED: - workspaceHandler.workspaceDeleted(workspace, correlationId); - break; - case MODIFIED: - workspaceHandler.workspaceModified(workspace, correlationId); - break; - case ERROR: - workspaceHandler.workspaceErrored(workspace, correlationId); - break; - case BOOKMARK: - workspaceHandler.workspaceBookmarked(workspace, correlationId); - break; + case ADDED -> workspaceHandler.workspaceAdded(workspace, correlationId); + case DELETED -> workspaceHandler.workspaceDeleted(workspace, correlationId); + case MODIFIED -> workspaceHandler.workspaceModified(workspace, correlationId); + case ERROR -> workspaceHandler.workspaceErrored(workspace, correlationId); + case BOOKMARK -> workspaceHandler.workspaceBookmarked(workspace, correlationId); } } catch (Exception e) { LOGGER.error(formatLogMessage(correlationId, "Error while handling workspaces"), e); @@ -260,8 +442,10 @@ protected void stopTimedOutSessions() { } /** - * If watches have not been called at all (neither reconnecting calls or actual actions), this might mean that the - * watch can't communicate with the kube API anymore. In this case we want to hand over to a different operator + * If watches have not been called at all (neither reconnecting calls or actual + * actions), this might mean that the + * watch can't communicate with the kube API anymore. In this case we want to + * hand over to a different operator * which will start up fresh watches. */ protected void lookForIdleWatches() { diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/LeaderElectionTheiaCloudOperatorLauncher.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/LeaderElectionTheiaCloudOperatorLauncher.java index 8fbd61092..3e9d6d895 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/LeaderElectionTheiaCloudOperatorLauncher.java +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/LeaderElectionTheiaCloudOperatorLauncher.java @@ -36,6 +36,10 @@ import io.fabric8.kubernetes.client.extended.leaderelection.LeaderElectionConfigBuilder; import io.fabric8.kubernetes.client.extended.leaderelection.LeaderElector; import io.fabric8.kubernetes.client.extended.leaderelection.resourcelock.LeaseLock; +import io.fabric8.kubernetes.client.okhttp.OkHttpClientFactory; +import io.sentry.okhttp.SentryOkHttpEventListener; +import io.sentry.okhttp.SentryOkHttpInterceptor; +import okhttp3.OkHttpClient; public abstract class LeaderElectionTheiaCloudOperatorLauncher extends TheiaCloudOperatorLauncher { @@ -64,8 +68,18 @@ protected void runLeaderElection(TheiaCloudOperatorArguments args) { Config k8sConfig = new ConfigBuilder().build(); - try (KubernetesClient k8sClient = new KubernetesClientBuilder().withConfig(k8sConfig).build()) { + try (KubernetesClient k8sClient = new KubernetesClientBuilder() + .withConfig(k8sConfig) + .withHttpClientFactory(new OkHttpClientFactory() { + @Override + protected void additionalConfig(OkHttpClient.Builder builder) { + builder.addInterceptor(new SentryOkHttpInterceptor()); + builder.eventListener(new SentryOkHttpEventListener()); + } + }) + .build()) { String leaseLockNamespace = k8sClient.getNamespace(); + LOGGER.info(formatLogMessage(COR_ID_INIT, "Using namespace: " + leaseLockNamespace)); LeaderElectionConfig leaderElectionConfig = new LeaderElectionConfigBuilder()// .withReleaseOnCancel(true)// @@ -88,12 +102,20 @@ protected void runLeaderElection(TheiaCloudOperatorArguments args) { .build(); LeaderElector leaderElector = k8sClient.leaderElector().withConfig(leaderElectionConfig).build(); leaderElector.run(); + } catch (Exception e) { + LOGGER.error(formatLogMessage(COR_ID_INIT, "Error during leader election"), e); + throw new RuntimeException("Failed to start leader election", e); } } protected void onStartLeading() { LOGGER.info(formatLogMessage(COR_ID_INIT, "Elected as new leader!")); - startOperatorAsLeader(args); + try { + startOperatorAsLeader(args); + } catch (Exception e) { + LOGGER.error(formatLogMessage(COR_ID_INIT, "Error starting operator as leader"), e); + throw new RuntimeException("Failed to start operator as leader", e); + } } protected void onStopLeading() { @@ -106,14 +128,19 @@ protected void onNewLeader(String newLeader) { } protected void startOperatorAsLeader(TheiaCloudOperatorArguments arguments) { - AbstractTheiaCloudOperatorModule module = createModule(arguments); - LOGGER.info(formatLogMessage(COR_ID_INIT, "Using " + module.getClass().getName() + " as DI module")); - - Injector injector = Guice.createInjector(module); - TheiaCloudOperator theiaCloud = injector.getInstance(TheiaCloudOperator.class); - - LOGGER.info(formatLogMessage(COR_ID_INIT, "Launching Theia Cloud Now")); - theiaCloud.start(); + try { + AbstractTheiaCloudOperatorModule module = createModule(arguments); + LOGGER.info(formatLogMessage(COR_ID_INIT, "Using " + module.getClass().getName() + " as DI module")); + + Injector injector = Guice.createInjector(module); + TheiaCloudOperator theiaCloud = injector.getInstance(TheiaCloudOperator.class); + + LOGGER.info(formatLogMessage(COR_ID_INIT, "Launching Theia Cloud Now")); + theiaCloud.start(); + } catch (Exception e) { + LOGGER.error(formatLogMessage(COR_ID_INIT, "Error during operator initialization"), e); + throw e; + } } @Override diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/TheiaCloudOperator.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/TheiaCloudOperator.java index eb32d73f9..aafb1fc58 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/TheiaCloudOperator.java +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/TheiaCloudOperator.java @@ -20,4 +20,5 @@ public interface TheiaCloudOperator { void start(); + void stop(); } diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/TheiaCloudOperatorArguments.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/TheiaCloudOperatorArguments.java index 35ef3b8a6..b7c27361e 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/TheiaCloudOperatorArguments.java +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/TheiaCloudOperatorArguments.java @@ -17,6 +17,7 @@ package org.eclipse.theia.cloud.operator; import picocli.CommandLine.Option; +import java.util.logging.Logger; public class TheiaCloudOperatorArguments { @@ -64,7 +65,10 @@ public enum BandwidthLimiter { "--sessionsPerUser" }, description = "Number of active sessions a single user is allowed to start.", required = false) private Integer sessionsPerUser; - @Option(names = { "--appId" }, description = "Application ID necessary for service calls", required = false) + @Option(names = { "--serviceAuthToken" }, description = "Service authentication token necessary for service calls", required = false) + private String serviceAuthToken; + + @Option(names = { "--appId" }, description = "(Deprecated) Use --serviceAuthToken instead. Application ID necessary for service calls", required = false, hidden = true) private String appId; @Option(names = { @@ -114,6 +118,10 @@ public enum BandwidthLimiter { "--maxWatchIdleTime" }, description = "When a kubernetes watcher is idle for more than this time (in milliseconds) we assume that there is a problem and restart.", required = false) private long maxWatchIdleTime = 1000 * 60 * 60; // 1 Hour + @Option(names = { + "--sessionHandlerThreads" }, description = "Number of threads used to handle session events.", required = false) + private Integer sessionHandlerThreads; + @Option(names = { "--continueOnException" }, description = "Whether the operator will continue to run in case of unexpected exceptions.", required = false) private boolean continueOnException; @@ -122,6 +130,27 @@ public enum BandwidthLimiter { "--oAuth2ProxyVersion" }, description = "The version to use of the quay.io/oauth2-proxy/oauth2-proxy image.", required = false, defaultValue = "latest") private String oAuth2ProxyVersion; + @Option(names = { + "--enableBuildCaching" }, description = "Whether to enable caching of Theia application builds.", required = false) + private boolean enableBuildCaching = false; + + @Option(names = { "--buildCacheUrl", "--gradleBuildCacheUrl" }, description = "The URL of the remote Gradle build cache server.", required = false) + private String buildCacheUrl; + + @Option(names = { "--bazelBuildCacheUrl" }, description = "The URL of the remote Bazel build cache server.", required = false) + private String bazelBuildCacheUrl; + + @Option(names = { + "--enableBuildCachePush" }, description = "Whether sessions are allowed to push to the build cache.", required = false) + private boolean enableBuildCachePush = false; + + @Option(names = { + "--enableDependencyCaching" }, description = "Whether to enable the dependency cache (Reposilite).", required = false) + private boolean enableDependencyCaching = false; + + @Option(names = { "--dependencyCacheUrl" }, description = "The URL of the dependency cache server (Reposilite).", required = false) + private String dependencyCacheUrl; + public boolean isUseKeycloak() { return useKeycloak; } @@ -162,8 +191,20 @@ public Integer getMonitorInterval() { return monitorInterval; } + /** + * @return the configured service auth token + */ + public String getServiceAuthToken() { + return getServiceAuthTokenWithFallback(); + } + + /** + * @deprecated Use {@link #getServiceAuthToken()} instead. This method is maintained for backwards compatibility. + * @return the configured service auth token + */ + @Deprecated(since = "1.2.0", forRemoval = true) public String getAppId() { - return appId; + return getServiceAuthTokenWithFallback(); } public String getInstancesHost() { @@ -213,6 +254,24 @@ public int getLeaderRetryPeriod() { public long getMaxWatchIdleTime() { return maxWatchIdleTime; } + /** + * Gets the number of threads to use for session event handling. + * + * If a specific thread count is configured and greater than zero, returns that value. + * Otherwise, calculates a default based on the number of available CPU cores, + * with a formula of (cores * 2) bounded within the range [4, 32]. + * + * @return the number of session handler threads + */ + public int getSessionHandlerThreads() { + if (sessionHandlerThreads != null && sessionHandlerThreads > 0) { + return sessionHandlerThreads; + } + int cores = Runtime.getRuntime().availableProcessors(); + // Number of cores * 2 + // Bounded within [4, 32] + return Math.min(32, Math.max(4, cores * 2)); + } public boolean isContinueOnException() { return continueOnException; @@ -221,11 +280,74 @@ public boolean isContinueOnException() { public String getOAuth2ProxyVersion() { return oAuth2ProxyVersion; } + + /** + * Get the service auth token with fallback to deprecated app id argument. + * Logs a deprecation warning if the old argument is used. + */ + private String getServiceAuthTokenWithFallback() { + if (serviceAuthToken != null) { + return serviceAuthToken; + } + + if (appId != null) { + Logger logger = Logger.getLogger(TheiaCloudOperatorArguments.class.getName()); + logger.warning("Using deprecated command line argument '--appId'. " + + "Please migrate to '--serviceAuthToken' in your configuration."); + return appId; + } + + return null; + } + + /** + * Validates that required argument combinations are satisfied. + * Must be called after argument parsing. + * + * @throws IllegalArgumentException if a caching flag is enabled but its URL is missing + */ + public void validate() { + if (enableBuildCaching + && (buildCacheUrl == null || buildCacheUrl.trim().isEmpty()) + && (bazelBuildCacheUrl == null || bazelBuildCacheUrl.trim().isEmpty())) { + throw new IllegalArgumentException( + "At least one of --buildCacheUrl or --bazelBuildCacheUrl is required when --enableBuildCaching is set"); + } + if (enableDependencyCaching && (dependencyCacheUrl == null || dependencyCacheUrl.trim().isEmpty())) { + throw new IllegalArgumentException( + "--dependencyCacheUrl is required when --enableDependencyCaching is set"); + } + } + + public boolean isEnableBuildCaching() { + return enableBuildCaching; + } + + public String getBuildCacheUrl() { + return buildCacheUrl; + } + + public String getBazelBuildCacheUrl() { + return bazelBuildCacheUrl; + } + + public boolean isEnableBuildCachePush() { + return enableBuildCachePush; + } + + public boolean isEnableDependencyCaching() { + return enableDependencyCaching; + } + + public String getDependencyCacheUrl() { + return dependencyCacheUrl; + } @Override public int hashCode() { final int prime = 31; int result = 1; + result = prime * result + ((serviceAuthToken == null) ? 0 : serviceAuthToken.hashCode()); result = prime * result + ((appId == null) ? 0 : appId.hashCode()); result = prime * result + ((bandwidthLimiter == null) ? 0 : bandwidthLimiter.hashCode()); result = prime * result + ((cloudProvider == null) ? 0 : cloudProvider.hashCode()); @@ -244,6 +366,7 @@ public int hashCode() { result = prime * result + (int) (maxWatchIdleTime ^ (maxWatchIdleTime >>> 32)); result = prime * result + ((monitorInterval == null) ? 0 : monitorInterval.hashCode()); result = prime * result + ((requestedStorage == null) ? 0 : requestedStorage.hashCode()); + result = prime * result + ((sessionHandlerThreads == null) ? 0 : sessionHandlerThreads.hashCode()); result = prime * result + ((serviceUrl == null) ? 0 : serviceUrl.hashCode()); result = prime * result + ((sessionsPerUser == null) ? 0 : sessionsPerUser.hashCode()); result = prime * result + ((storageClassName == null) ? 0 : storageClassName.hashCode()); @@ -251,6 +374,12 @@ public int hashCode() { result = prime * result + (usePaths ? 1231 : 1237); result = prime * result + ((wondershaperImage == null) ? 0 : wondershaperImage.hashCode()); result = prime * result + ((oAuth2ProxyVersion == null) ? 0 : oAuth2ProxyVersion.hashCode()); + result = prime * result + (enableBuildCaching ? 1231 : 1237); + result = prime * result + ((buildCacheUrl == null) ? 0 : buildCacheUrl.hashCode()); + result = prime * result + ((bazelBuildCacheUrl == null) ? 0 : bazelBuildCacheUrl.hashCode()); + result = prime * result + (enableBuildCachePush ? 1231 : 1237); + result = prime * result + (enableDependencyCaching ? 1231 : 1237); + result = prime * result + ((dependencyCacheUrl == null) ? 0 : dependencyCacheUrl.hashCode()); return result; } @@ -263,6 +392,11 @@ public boolean equals(Object obj) { if (getClass() != obj.getClass()) return false; TheiaCloudOperatorArguments other = (TheiaCloudOperatorArguments) obj; + if (serviceAuthToken == null) { + if (other.serviceAuthToken != null) + return false; + } else if (!serviceAuthToken.equals(other.serviceAuthToken)) + return false; if (appId == null) { if (other.appId != null) return false; @@ -318,6 +452,11 @@ public boolean equals(Object obj) { return false; } else if (!monitorInterval.equals(other.monitorInterval)) return false; + if (sessionHandlerThreads == null) { + if (other.sessionHandlerThreads != null) + return false; + } else if (!sessionHandlerThreads.equals(other.sessionHandlerThreads)) + return false; if (requestedStorage == null) { if (other.requestedStorage != null) return false; @@ -352,6 +491,27 @@ public boolean equals(Object obj) { return false; } else if (!oAuth2ProxyVersion.equals(other.oAuth2ProxyVersion)) return false; + if (enableBuildCaching != other.enableBuildCaching) + return false; + if (buildCacheUrl == null) { + if (other.buildCacheUrl != null) + return false; + } else if (!buildCacheUrl.equals(other.buildCacheUrl)) + return false; + if (bazelBuildCacheUrl == null) { + if (other.bazelBuildCacheUrl != null) + return false; + } else if (!bazelBuildCacheUrl.equals(other.bazelBuildCacheUrl)) + return false; + if (enableBuildCachePush != other.enableBuildCachePush) + return false; + if (enableDependencyCaching != other.enableDependencyCaching) + return false; + if (dependencyCacheUrl == null) { + if (other.dependencyCacheUrl != null) + return false; + } else if (!dependencyCacheUrl.equals(other.dependencyCacheUrl)) + return false; return true; } @@ -361,13 +521,18 @@ public String toString() { + enableMonitor + ", enableActivityTracker=" + enableActivityTracker + ", monitorInterval=" + monitorInterval + ", cloudProvider=" + cloudProvider + ", bandwidthLimiter=" + bandwidthLimiter + ", wondershaperImage=" + wondershaperImage + ", serviceUrl=" + serviceUrl + ", sessionsPerUser=" - + sessionsPerUser + ", appId=" + appId + ", instancesHost=" + instancesHost + ", usePaths=" + usePaths - + ", instancesPath=" + instancesPath + ", storageClassName=" + storageClassName + ", requestedStorage=" - + requestedStorage + ", keycloakURL=" + keycloakURL + ", keycloakRealm=" + keycloakRealm + + sessionsPerUser + ", serviceAuthToken=" + serviceAuthToken + ", appId=" + appId + ", instancesHost=" + + instancesHost + ", usePaths=" + usePaths + ", instancesPath=" + instancesPath + ", storageClassName=" + + storageClassName + ", requestedStorage=" + requestedStorage + ", sessionHandlerThreads=" + + sessionHandlerThreads + ", keycloakURL=" + keycloakURL + ", keycloakRealm=" + keycloakRealm + ", keycloakClientId=" + keycloakClientId + ", leaderLeaseDuration=" + leaderLeaseDuration + ", leaderRenewDeadline=" + leaderRenewDeadline + ", leaderRetryPeriod=" + leaderRetryPeriod + ", maxWatchIdleTime=" + maxWatchIdleTime + ", continueOnException=" + continueOnException - + ", oAuth2ProxyVersion=" + oAuth2ProxyVersion + "]"; + + ", oAuth2ProxyVersion=" + oAuth2ProxyVersion + ", enableBuildCaching=" + enableBuildCaching + + ", buildCacheUrl=" + buildCacheUrl + ", bazelBuildCacheUrl=" + bazelBuildCacheUrl + + ", enableBuildCachePush=" + enableBuildCachePush + + ", enableDependencyCaching=" + enableDependencyCaching + ", dependencyCacheUrl=" + + dependencyCacheUrl + "]"; } } diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/TheiaCloudOperatorLauncher.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/TheiaCloudOperatorLauncher.java index 0d3fc893b..6ac0829bf 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/TheiaCloudOperatorLauncher.java +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/TheiaCloudOperatorLauncher.java @@ -26,6 +26,7 @@ import com.google.inject.Injector; import picocli.CommandLine; +import picocli.CommandLine.ParameterException; public abstract class TheiaCloudOperatorLauncher { @@ -36,22 +37,43 @@ public abstract class TheiaCloudOperatorLauncher { protected TheiaCloudOperatorArguments args; public void runMain(String[] args) throws InterruptedException { - this.args = createArguments(args); - AbstractTheiaCloudOperatorModule module = createModule(this.args); - LOGGER.info(formatLogMessage(COR_ID_INIT, "Using " + module.getClass().getName() + " as DI module")); - - Injector injector = Guice.createInjector(module); - TheiaCloudOperator theiaCloud = injector.getInstance(TheiaCloudOperator.class); - LOGGER.info(formatLogMessage(COR_ID_INIT, "Launching Theia Cloud Now")); - theiaCloud.start(); + try { + this.args = createArguments(args); + AbstractTheiaCloudOperatorModule module = createModule(this.args); + LOGGER.info(formatLogMessage(COR_ID_INIT, "Using " + module.getClass().getName() + " as DI module")); + + Injector injector = Guice.createInjector(module); + TheiaCloudOperator theiaCloud = injector.getInstance(TheiaCloudOperator.class); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + try { + theiaCloud.stop(); + } catch (Exception e) { + LOGGER.error(formatLogMessage(COR_ID_INIT, "Error during operator shutdown"), e); + } + }, "theia-cloud-operator-shutdown")); + LOGGER.info(formatLogMessage(COR_ID_INIT, "Launching Theia Cloud Now")); + theiaCloud.start(); + } catch (Exception e) { + LOGGER.error(formatLogMessage(COR_ID_INIT, "Error during operator startup"), e); + throw e; + } } public TheiaCloudOperatorArguments createArguments(String[] args) { - TheiaCloudOperatorArguments arguments = new TheiaCloudOperatorArguments(); - CommandLine commandLine = new CommandLine(arguments).setTrimQuotes(true); - commandLine.parseArgs(args); - LOGGER.info(formatLogMessage(COR_ID_INIT, "Parsed args: " + arguments)); - return arguments; + try { + TheiaCloudOperatorArguments arguments = new TheiaCloudOperatorArguments(); + CommandLine commandLine = new CommandLine(arguments).setTrimQuotes(true); + commandLine.parseArgs(args); + arguments.validate(); + LOGGER.info(formatLogMessage(COR_ID_INIT, "Parsed args: " + arguments)); + return arguments; + } catch (ParameterException e) { + LOGGER.error(formatLogMessage(COR_ID_INIT, "Failed to parse command line arguments"), e); + throw e; + } catch (Exception e) { + LOGGER.error(formatLogMessage(COR_ID_INIT, "Unexpected error parsing arguments"), e); + throw e; + } } abstract AbstractTheiaCloudOperatorModule createModule(TheiaCloudOperatorArguments arguments); diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/databridge/AsyncDataInjector.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/databridge/AsyncDataInjector.java new file mode 100644 index 000000000..d6b8d4d61 --- /dev/null +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/databridge/AsyncDataInjector.java @@ -0,0 +1,191 @@ +package org.eclipse.theia.cloud.operator.databridge; + +import static org.eclipse.theia.cloud.common.util.LogMessageUtil.formatLogMessage; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.theia.cloud.common.k8s.client.DataBridgeClient; +import org.eclipse.theia.cloud.common.k8s.resource.session.DataInjectionResponse; +import org.eclipse.theia.cloud.common.k8s.resource.session.Session; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import io.sentry.ISpan; +import io.sentry.SpanStatus; +import org.eclipse.theia.cloud.common.tracing.TraceContext; +import org.eclipse.theia.cloud.common.tracing.Tracing; + +/** + * Handles asynchronous data injection into sessions via the data bridge. Polls + * the health endpoint until the data + * bridge is ready, then injects data once. + */ +@Singleton +public class AsyncDataInjector { + + private static final Logger LOGGER = LogManager.getLogger(AsyncDataInjector.class); + + /** Maximum number of health check attempts (1 per second for 60 seconds) */ + private static final int MAX_HEALTH_CHECKS = 60; + + /** Interval between health checks in milliseconds */ + private static final int HEALTH_CHECK_INTERVAL_MS = 1000; + + private final ScheduledExecutorService scheduler; + + @Inject + private DataBridgeClient dataBridgeClient; + + public AsyncDataInjector() { + this.scheduler = Executors.newScheduledThreadPool(4); + } + + /** + * Schedules asynchronous data injection for a session. Polls the health + * endpoint until ready, then injects data + * once. + * + * @param parentSpan The parent span to create a child span from for tracing + * @param session The session to inject data into + * @param envVars The environment variables to inject + * @param correlationId For logging/tracing + */ + public void scheduleInjection(ISpan parentSpan, Session session, Map envVars, + String correlationId) { + String sessionName = session.getSpec().getName(); + String appDef = session.getSpec().getAppDefinition(); + int envVarCount = envVars.size(); + + LOGGER.info(formatLogMessage(correlationId, "Scheduling async data injection for session: " + sessionName)); + + // Extract trace context BEFORE scheduling - the parent span will be finished by + // the time scheduler runs + Optional traceContext = TraceContext.fromSpan(parentSpan); + + scheduler.submit(() -> { + // Create a new transaction linked to the same trace (parent span is already + // finished) + ISpan span = Tracing.continueTraceAsync(traceContext, "databridge.inject", "Data bridge injection"); + span.setTag("session.name", sessionName); + span.setTag("app_definition", appDef); + span.setData("correlation_id", correlationId); + span.setData("env_var_count", envVarCount); + + pollHealthThenInject(span, session, envVars, correlationId, 0); + }); + } + + /** + * Polls the health endpoint and injects data when ready. Recursively schedules + * itself for retries. Aggregate metrics (health check attempts, outcome) are tracked + * on the parent span rather than creating individual spans for each health check. + * + * @param span The parent span from scheduleInjection - aggregate metrics are tracked here + * @param session The session to inject data into + * @param envVars The environment variables to inject + * @param correlationId For logging/tracing + * @param attempt The current attempt number (0-indexed) + */ + private void pollHealthThenInject(ISpan span, Session session, Map envVars, + String correlationId, int attempt) { + String sessionName = session.getSpec().getName(); + + if (attempt >= MAX_HEALTH_CHECKS) { + LOGGER.error(formatLogMessage(correlationId, + "Data bridge not ready after " + MAX_HEALTH_CHECKS + "s for session: " + sessionName)); + + span.setTag("outcome", "timeout"); + span.setData("health_check_attempts", attempt); + Tracing.finish(span, SpanStatus.DEADLINE_EXCEEDED); + return; + } + + // Check if data bridge is healthy (no individual span, just aggregate metrics) + boolean healthy = dataBridgeClient.healthCheck(sessionName, correlationId); + + if (healthy) { + // Ready - inject data once + LOGGER.info(formatLogMessage(correlationId, + "Data bridge ready for session: " + sessionName + " (attempt " + (attempt + 1) + ")")); + + // Track aggregate metrics on the span + span.setData("health_check_attempts", attempt + 1); + span.setData("time_to_ready_ms", (attempt + 1) * HEALTH_CHECK_INTERVAL_MS); + + ISpan injectSpan = Tracing.childSpan(span, "databridge.inject_data", "Inject environment data"); + injectSpan.setData("env_var_count", envVars.size()); + + try { + Optional response = dataBridgeClient.injectData(session, envVars, correlationId); + + if (response.isEmpty()) { + LOGGER.error(formatLogMessage(correlationId, + "Data injection failed - no response for session: " + sessionName)); + injectSpan.setTag("outcome", "no_response"); + Tracing.finish(injectSpan, SpanStatus.INTERNAL_ERROR); + span.setTag("outcome", "injection_failed"); + span.setTag("failure.reason", "no_response"); + Tracing.finish(span, SpanStatus.INTERNAL_ERROR); + } else if (!response.get().isSuccess()) { + String error = response.get().getError(); + LOGGER.error(formatLogMessage(correlationId, + "Data injection failed for session: " + sessionName + " - " + error)); + injectSpan.setTag("outcome", "error"); + injectSpan.setData("error_message", error); + Tracing.finish(injectSpan, SpanStatus.INTERNAL_ERROR); + span.setTag("outcome", "injection_failed"); + span.setTag("failure.reason", "api_error"); + span.setData("error_message", error); + Tracing.finish(span, SpanStatus.INTERNAL_ERROR); + } else { + LOGGER.info( + formatLogMessage(correlationId, "Data injected successfully for session: " + sessionName)); + injectSpan.setTag("outcome", "success"); + Tracing.finishSuccess(injectSpan); + span.setTag("outcome", "success"); + Tracing.finishSuccess(span); + } + } catch (Exception e) { + LOGGER.error(formatLogMessage(correlationId, "Exception during data injection"), e); + injectSpan.setTag("outcome", "exception"); + Tracing.finishError(injectSpan, e); + span.setTag("outcome", "injection_failed"); + span.setTag("failure.reason", "exception"); + Tracing.finish(span, SpanStatus.INTERNAL_ERROR); + } + return; + } + + // Not ready - schedule next health check + if (attempt == 0) { + LOGGER.debug(formatLogMessage(correlationId, + "Data bridge not ready yet for session: " + sessionName + ", polling...")); + } + + scheduler.schedule(() -> pollHealthThenInject(span, session, envVars, correlationId, attempt + 1), + HEALTH_CHECK_INTERVAL_MS, TimeUnit.MILLISECONDS); + } + + /** + * Shuts down the scheduler. Should be called when the operator is shutting + * down. + */ + public void shutdown() { + scheduler.shutdown(); + try { + if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) { + scheduler.shutdownNow(); + } + } catch (InterruptedException e) { + scheduler.shutdownNow(); + Thread.currentThread().interrupt(); + } + } +} diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/di/AbstractTheiaCloudOperatorModule.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/di/AbstractTheiaCloudOperatorModule.java index d6bdcfd66..8865de5ec 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/di/AbstractTheiaCloudOperatorModule.java +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/di/AbstractTheiaCloudOperatorModule.java @@ -18,6 +18,7 @@ import java.util.function.Consumer; +import org.eclipse.theia.cloud.common.k8s.client.DataBridgeClient; import org.eclipse.theia.cloud.common.k8s.client.DefaultTheiaCloudClient; import org.eclipse.theia.cloud.common.k8s.client.TheiaCloudClient; import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinition; @@ -31,7 +32,7 @@ import org.eclipse.theia.cloud.operator.handler.appdef.AppDefinitionHandler; import org.eclipse.theia.cloud.operator.handler.appdef.EagerStartAppDefinitionAddedHandler; import org.eclipse.theia.cloud.operator.handler.appdef.LazyStartAppDefinitionHandler; -import org.eclipse.theia.cloud.operator.handler.session.EagerStartSessionHandler; +import org.eclipse.theia.cloud.operator.handler.session.EagerWithLazyFallbackSessionHandler; import org.eclipse.theia.cloud.operator.handler.session.LazySessionHandler; import org.eclipse.theia.cloud.operator.handler.session.SessionHandler; import org.eclipse.theia.cloud.operator.handler.ws.LazyWorkspaceHandler; @@ -46,9 +47,13 @@ import org.eclipse.theia.cloud.operator.pv.MinikubePersistentVolumeCreator; import org.eclipse.theia.cloud.operator.pv.PersistentVolumeCreator; import org.eclipse.theia.cloud.operator.replacements.DefaultDeploymentTemplateReplacements; +import org.eclipse.theia.cloud.operator.databridge.AsyncDataInjector; +import org.eclipse.theia.cloud.operator.sidecar.SidecarManager; +import org.eclipse.theia.cloud.operator.sidecar.SidecarResourceFactory; import org.eclipse.theia.cloud.operator.replacements.DefaultPersistentVolumeTemplateReplacements; import org.eclipse.theia.cloud.operator.replacements.DeploymentTemplateReplacements; import org.eclipse.theia.cloud.operator.replacements.PersistentVolumeTemplateReplacements; +import org.eclipse.theia.cloud.operator.util.SessionEnvCollector; import com.google.inject.AbstractModule; import com.google.inject.Provides; @@ -81,6 +86,11 @@ protected void configure() { configure(MultiBinding.create(OperatorPlugin.class), this::bindOperatorPlugins); bind(MonitorMessagingService.class).to(bindMonitorMessagingService()).in(Singleton.class); + bind(DataBridgeClient.class).toProvider(DataBridgeClientProvider.class).in(Singleton.class); + bind(SidecarResourceFactory.class).in(Singleton.class); + bind(SidecarManager.class).in(Singleton.class); + bind(SessionEnvCollector.class).in(Singleton.class); + bind(AsyncDataInjector.class).in(Singleton.class); bind(TheiaCloudOperatorArguments.class).toInstance(arguments); } @@ -143,7 +153,7 @@ protected Class bindWorkspaceHandler() { protected Class bindSessionHandler() { if (arguments.isEagerStart()) { - return EagerStartSessionHandler.class; + return EagerWithLazyFallbackSessionHandler.class; } else { return LazySessionHandler.class; } diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/di/DataBridgeClientProvider.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/di/DataBridgeClientProvider.java new file mode 100644 index 000000000..a641215dc --- /dev/null +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/di/DataBridgeClientProvider.java @@ -0,0 +1,25 @@ +package org.eclipse.theia.cloud.operator.di; + +import org.eclipse.theia.cloud.common.k8s.client.DataBridgeClient; +import org.eclipse.theia.cloud.common.k8s.client.DefaultDataBridgeClient; +import org.eclipse.theia.cloud.common.k8s.client.TheiaCloudClient; + +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; + +/** + * Guice provider for {@link DataBridgeClient}. + */ +@Singleton +public class DataBridgeClientProvider implements Provider { + + @Inject + private TheiaCloudClient theiaCloudClient; + + @Override + public DataBridgeClient get() { + return new DefaultDataBridgeClient(theiaCloudClient); + } +} + diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/AddedHandlerUtil.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/AddedHandlerUtil.java index 377696e4f..f2f838fe7 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/AddedHandlerUtil.java +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/AddedHandlerUtil.java @@ -63,6 +63,11 @@ import io.fabric8.kubernetes.api.model.SecretEnvSource; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.client.NamespacedKubernetesClient; +import org.eclipse.theia.cloud.operator.TheiaCloudOperatorArguments; +import io.sentry.ISpan; +import io.sentry.SpanStatus; +import org.eclipse.theia.cloud.common.tracing.Tracing; +import org.eclipse.theia.cloud.common.tracing.TraceContext; public final class AddedHandlerUtil { @@ -72,6 +77,7 @@ public final class AddedHandlerUtil { public static final String TEMPLATE_SERVICE_YAML = "/templateService.yaml"; public static final String TEMPLATE_SERVICE_WITHOUT_AOUTH2_PROXY_YAML = "/templateServiceWithoutOAuthProxy.yaml"; + public static final String TEMPLATE_INTERNAL_SERVICE_YAML = "/templateInternalService.yaml"; public static final String TEMPLATE_CONFIGMAP_EMAILS_YAML = "/templateConfigmapEmails.yaml"; public static final String TEMPLATE_CONFIGMAP_YAML = "/templateConfigmap.yaml"; public static final String TEMPLATE_DEPLOYMENT_YAML = "/templateDeployment.yaml"; @@ -87,6 +93,7 @@ public final class AddedHandlerUtil { public static final String FILENAME_AUTHENTICATED_EMAILS_LIST = "authenticated-emails-list"; public static final String INGRESS_REWRITE_PATH = "(/|$)(.*)"; + public static final String INGRESS_PATH_TYPE = "ImplementationSpecific"; private static final HostnameVerifier ALL_GOOD_HOSTNAME_VERIFIER = new HostnameVerifier() { @Override @@ -129,83 +136,144 @@ public static void updateProxyConfigMap(NamespacedKubernetesClient client, Strin } public static void updateSessionURLAsync(SessionResourceClient sessions, Session session, String namespace, - String url, String correlationId) { + String url, String correlationId, ISpan parentSpan) { + String sessionName = session.getSpec().getName(); + String appDef = session.getSpec().getAppDefinition(); + + // Extract trace context BEFORE scheduling - the parent span will be finished by the time executor runs + Optional traceContext = + TraceContext.fromSpan(parentSpan); + EXECUTOR.execute(() -> { + // Create a new transaction linked to the same trace (parent span is already finished) + ISpan span = Tracing.continueTraceAsync(traceContext, "session.url_availability", "URL availability check"); + span.setTag("session.name", sessionName); + span.setTag("app_definition", appDef); + span.setData("correlation_id", correlationId); + span.setData("url", url); + boolean updateURL = false; - for (int i = 1; i <= 100; i++) { - try { + int lastResponseCode = -1; + String failureReason = null; + + try { + for (int i = 1; i <= 309; i++) { /* - * On the first 15 loops we will check every 2.5s whether URL is available. This will take at least - * 37.5s. On the second 15 loops we will check every 5s. This will take at least 75s. On the next 15 - * loops we will check every 10s. This will take at least further 150s. If the pod has not started - * within the first 4-5 minutes, we will continue to check every minute. We give up after an hour. + * On the first 60 loops we will check every 500ms whether URL is available. This will take at + * least 30s. On the next 60 loops we will check every 1000ms. This will take at least 60s. On + * the next 24 loops we will check every 2500ms. This will take at least further 60s. On the + * next 60 loops we will check every 5000ms. This will take at least further 5 minutes. If the + * pod has not started within the first ~7 minutes, we will continue to check every 30 seconds. + * We give up after an hour. */ - if (i <= 15) { - Thread.sleep(2500); - } else if (i <= 30) { - Thread.sleep(5000); - } else if (i <= 45) { - Thread.sleep(10000); + long sleepDuration; + if (i <= 60) { + sleepDuration = 500; + } else if (i <= 120) { + sleepDuration = 1000; + } else if (i <= 144) { + sleepDuration = 2500; + } else if (i <= 204) { + sleepDuration = 5000; } else { - Thread.sleep(60000); + sleepDuration = 30000; + } + + try { + Thread.sleep(sleepDuration); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + span.setTag("outcome", "cancelled"); + Tracing.finishError(span, e); + return; } - } catch (InterruptedException e) { - /* silent */ - } - HttpsURLConnection connection; - try { - connection = (HttpsURLConnection) new URL(HOST_PROTOCOL + url).openConnection(); - } catch (IOException e) { - LOGGER.error(formatLogMessage(correlationId, "Error while checking session availability."), e); - continue; - } - int code; - - try { - connection.setHostnameVerifier(ALL_GOOD_HOSTNAME_VERIFIER); - SSLContext sc = SSLContext.getInstance("SSL"); - sc.init(null, new TrustManager[] { TRUST_ALL_MANAGER }, new java.security.SecureRandom()); - HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); - connection.setSSLSocketFactory(sc.getSocketFactory()); - connection.connect(); - code = connection.getResponseCode(); - } catch (IOException e) { - LOGGER.error(formatLogMessage(correlationId, url + " is NOT available yet."), e); - continue; - } catch (NoSuchAlgorithmException | KeyManagementException e) { - LOGGER.error(formatLogMessage(correlationId, - "Error while checking session availability with SSL ignore."), e); - continue; - } + HttpsURLConnection connection; + try { + connection = (HttpsURLConnection) new URL(HOST_PROTOCOL + url).openConnection(); + } catch (IOException e) { + LOGGER.error(formatLogMessage(correlationId, "Error while checking session availability."), e); + failureReason = "connection_error"; + continue; + } + int code; + + try { + connection.setHostnameVerifier(ALL_GOOD_HOSTNAME_VERIFIER); + SSLContext sc = SSLContext.getInstance("SSL"); + sc.init(null, new TrustManager[] { TRUST_ALL_MANAGER }, new java.security.SecureRandom()); + HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); + connection.setSSLSocketFactory(sc.getSocketFactory()); + connection.connect(); + code = connection.getResponseCode(); + lastResponseCode = code; + } catch (IOException e) { + LOGGER.error(formatLogMessage(correlationId, url + " is NOT available yet."), e); + failureReason = "io_error"; + continue; + } catch (NoSuchAlgorithmException | KeyManagementException e) { + LOGGER.error(formatLogMessage(correlationId, + "Error while checking session availability with SSL ignore."), e); + failureReason = "ssl_error"; + continue; + } - LOGGER.trace(formatLogMessage(correlationId, url + " has response code " + code)); + LOGGER.trace(formatLogMessage(correlationId, url + " has response code " + code)); + + if (code == 200) { + updateURL = true; + } else if (code != 404 && code != 503 && code != 500 && !updateURL) { + /* + * we don't get a 404, 500 or 503, so something is available. Try accessing the URL once more then + * update URL anyway + */ + updateURL = true; + continue; + } - if (code == 200) { - updateURL = true; - } else if (code != 404 && code != 503 && !updateURL) { - /* - * we don't get a 404 or 503, so something is available. Try accessing the URL once more then update - * URL anyway - */ - updateURL = true; - continue; + if (updateURL) { + LOGGER.info(formatLogMessage(correlationId, url + " is available.")); + + ISpan updateSpan = Tracing.childSpan(span, "session.update_status", "Update session URL status"); + try { + sessions.updateStatus(correlationId, session, status -> status.setUrl(url)); + Tracing.finishSuccess(updateSpan); + } catch (Exception e) { + Tracing.finishError(updateSpan, e); + // Propagate failure to outer span + span.setTag("outcome", "error"); + span.setData("attempts", i); + span.setData("final_response_code", code); + Tracing.finishError(span, e); + return; + } + + LOGGER.info(formatMetric(correlationId, "Running session for " + appDef)); + + span.setTag("outcome", "success"); + span.setData("attempts", i); + span.setData("final_response_code", code); + Tracing.finishSuccess(span); + return; + } else { + LOGGER.trace(formatLogMessage(correlationId, url + " is NOT available yet.")); + } } - if (updateURL) { - LOGGER.info(formatLogMessage(correlationId, url + " is available.")); - sessions.updateStatus(correlationId, session, status -> status.setUrl(url)); - LOGGER.info( - formatMetric(correlationId, "Running session for " + session.getSpec().getAppDefinition())); - break; - } else { - LOGGER.trace(formatLogMessage(correlationId, url + " is NOT available yet.")); + // Exhausted all attempts + span.setTag("outcome", "timeout"); + span.setData("attempts", 309); + span.setData("last_response_code", lastResponseCode); + if (failureReason != null) { + span.setTag("failure.reason", failureReason); } + Tracing.finish(span, SpanStatus.DEADLINE_EXCEEDED); + } catch (Exception e) { + span.setTag("outcome", "error"); + Tracing.finishError(span, e); } - }); - } public static void removeEmptyResources(Deployment deployment) { diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/appdef/EagerStartAppDefinitionAddedHandler.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/appdef/EagerStartAppDefinitionAddedHandler.java index c0e7efef6..c57728ff4 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/appdef/EagerStartAppDefinitionAddedHandler.java +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/appdef/EagerStartAppDefinitionAddedHandler.java @@ -1,250 +1,202 @@ -/******************************************************************************** - * Copyright (C) 2022 EclipseSource, Lockular, Ericsson, STMicroelectronics and - * others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ package org.eclipse.theia.cloud.operator.handler.appdef; import static org.eclipse.theia.cloud.common.util.LogMessageUtil.formatLogMessage; -import java.io.IOException; -import java.net.URISyntaxException; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.eclipse.theia.cloud.common.k8s.client.TheiaCloudClient; import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinition; import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinitionSpec; -import org.eclipse.theia.cloud.operator.TheiaCloudOperatorArguments; -import org.eclipse.theia.cloud.operator.bandwidth.BandwidthLimiter; -import org.eclipse.theia.cloud.operator.handler.AddedHandlerUtil; -import org.eclipse.theia.cloud.operator.ingress.IngressPathProvider; -import org.eclipse.theia.cloud.operator.replacements.DeploymentTemplateReplacements; -import org.eclipse.theia.cloud.operator.util.JavaResourceUtil; -import org.eclipse.theia.cloud.operator.util.K8sUtil; -import org.eclipse.theia.cloud.operator.util.TheiaCloudConfigMapUtil; -import org.eclipse.theia.cloud.operator.util.TheiaCloudDeploymentUtil; +import org.eclipse.theia.cloud.operator.sidecar.SidecarManager; +import org.eclipse.theia.cloud.operator.pool.PrewarmedResourcePool; +import org.eclipse.theia.cloud.common.tracing.Tracing; +import io.sentry.Sentry; import org.eclipse.theia.cloud.operator.util.TheiaCloudIngressUtil; -import org.eclipse.theia.cloud.operator.util.TheiaCloudServiceUtil; import com.google.inject.Inject; -import io.fabric8.kubernetes.api.model.ConfigMap; -import io.fabric8.kubernetes.api.model.Service; -import io.fabric8.kubernetes.api.model.apps.Deployment; -import io.fabric8.kubernetes.client.NamespacedKubernetesClient; +import io.sentry.ISpan; +import io.sentry.ITransaction; +import io.sentry.SpanStatus; /** - * A {@link AppDefinitionHandler} that will eagerly start up deployments ahead of usage time which will later be used as - * sessions. + * A {@link AppDefinitionHandler} that manages a pool of prewarmed deployments for eager start sessions. This handler + * delegates pool management to {@link PrewarmedResourcePool}. */ public class EagerStartAppDefinitionAddedHandler implements AppDefinitionHandler { private static final Logger LOGGER = LogManager.getLogger(EagerStartAppDefinitionAddedHandler.class); - public static final String LABEL_KEY = "theiacloud"; - public static final String LABEL_VALUE_PROXY = "proxy"; - public static final String LABEL_VALUE_EMAILS = "emails"; - - @Inject - protected TheiaCloudClient client; - - @Inject - protected TheiaCloudOperatorArguments arguments; - @Inject - protected IngressPathProvider ingressPathProvider; + private TheiaCloudClient client; @Inject - protected BandwidthLimiter bandwidthLimiter; + private PrewarmedResourcePool pool; @Inject - protected DeploymentTemplateReplacements deploymentReplacements; + private SidecarManager sidecarManager; @Override public boolean appDefinitionAdded(AppDefinition appDefinition, String correlationId) { AppDefinitionSpec spec = appDefinition.getSpec(); - LOGGER.info(formatLogMessage(correlationId, "Handling " + spec)); - - String appDefinitionResourceName = appDefinition.getMetadata().getName(); - String appDefinitionResourceUID = appDefinition.getMetadata().getUid(); - int instances = spec.getMinInstances(); - - /* Create ingress if not existing */ - if (!TheiaCloudIngressUtil.checkForExistingIngressAndAddOwnerReferencesIfMissing(client.kubernetes(), - client.namespace(), appDefinition, correlationId)) { - LOGGER.error(formatLogMessage(correlationId, - "Expected ingress '" + spec.getIngressname() + "' for app definition '" + appDefinitionResourceName - + "' does not exist. Abort handling app definition.")); - return false; - } else { - LOGGER.trace(formatLogMessage(correlationId, "Ingress available already")); - } + String appDefName = appDefinition.getMetadata().getName(); + int minInstances = spec.getMinInstances(); - /* Get existing services for this app definition */ - List existingServices = K8sUtil.getExistingServices(client.kubernetes(), client.namespace(), - appDefinitionResourceName, appDefinitionResourceUID); + ITransaction tx = Tracing.startTransaction("appdef.added", "appdef"); + tx.setTag("app_definition", appDefName); + tx.setData("min_instances", minInstances); + tx.setData("correlation_id", correlationId); + tx.setTag("appdef.action", "added"); - /* Compute missing services */ - Set missingServiceIds = TheiaCloudServiceUtil.computeIdsOfMissingServices(appDefinition, correlationId, - instances, existingServices); - - /* Create missing services for this app definition */ - for (int instance : missingServiceIds) { - createAndApplyService(client.kubernetes(), client.namespace(), correlationId, appDefinitionResourceName, - appDefinitionResourceUID, instance, appDefinition, arguments.isUseKeycloak()); - } + LOGGER.info(formatLogMessage(correlationId, "Handling " + spec)); - if (arguments.isUseKeycloak()) { - /* Get existing configmaps for this app definition */ - List existingConfigMaps = K8sUtil.getExistingConfigMaps(client.kubernetes(), client.namespace(), - appDefinitionResourceName, appDefinitionResourceUID); - List existingProxyConfigMaps = existingConfigMaps.stream()// - .filter(configmap -> LABEL_VALUE_PROXY.equals(configmap.getMetadata().getLabels().get(LABEL_KEY)))// - .collect(Collectors.toList()); - List existingEmailsConfigMaps = existingConfigMaps.stream()// - .filter(configmap -> LABEL_VALUE_EMAILS.equals(configmap.getMetadata().getLabels().get(LABEL_KEY)))// - .collect(Collectors.toList()); - - /* Compute missing configmaps */ - Set missingProxyIds = TheiaCloudConfigMapUtil.computeIdsOfMissingProxyConfigMaps(appDefinition, - correlationId, instances, existingProxyConfigMaps); - Set missingEmailIds = TheiaCloudConfigMapUtil.computeIdsOfMissingEmailConfigMaps(appDefinition, - correlationId, instances, existingEmailsConfigMaps); - - /* Create missing configmaps for this app definition */ - for (int instance : missingProxyIds) { - createAndApplyProxyConfigMap(client.kubernetes(), client.namespace(), correlationId, - appDefinitionResourceName, appDefinitionResourceUID, instance, appDefinition); + try { + // Verify ingress exists + ISpan ingressSpan = Tracing.childSpan(tx, "appdef.verify_ingress", "Verify ingress exists"); + ingressSpan.setData("route_name", spec.getIngressname()); + + if (!TheiaCloudIngressUtil.checkForExistingIngressAndAddOwnerReferencesIfMissing(client.kubernetes(), + client.namespace(), appDefinition, correlationId)) { + LOGGER.error(formatLogMessage(correlationId, + "Expected HTTPRoute '" + spec.getIngressname() + "' for app definition '" + appDefName + + "' does not exist. Abort handling app definition.")); + ingressSpan.setTag("outcome", "not_found"); + Tracing.finish(ingressSpan, SpanStatus.NOT_FOUND); + tx.setTag("error.reason", "ingress_not_found"); + tx.setTag("outcome", "failure"); + Tracing.finish(tx, SpanStatus.NOT_FOUND); + return false; } - for (int instance : missingEmailIds) { - createAndApplyEmailConfigMap(client.kubernetes(), client.namespace(), correlationId, - appDefinitionResourceName, appDefinitionResourceUID, instance, appDefinition); + Tracing.finishSuccess(ingressSpan); + + ISpan sidecarValidationSpan = Tracing.childSpan(tx, "appdef.validate_sidecars", + "Validate sidecar configuration"); + if (!sidecarManager.validateUniqueSidecarNames(appDefinition, correlationId)) { + sidecarValidationSpan.setTag("outcome", "invalid"); + Tracing.finish(sidecarValidationSpan, SpanStatus.INVALID_ARGUMENT); + tx.setTag("error.reason", "duplicate_sidecar_names"); + tx.setTag("outcome", "failure"); + Tracing.finish(tx, SpanStatus.INVALID_ARGUMENT); + return false; } - } + Tracing.finishSuccess(sidecarValidationSpan); - /* Get existing deployments for this app definition */ - List existingDeployments = K8sUtil.getExistingDeployments(client.kubernetes(), client.namespace(), - appDefinitionResourceName, appDefinitionResourceUID); + LOGGER.trace(formatLogMessage(correlationId, "HTTPRoute available")); - /* Compute missing deployments */ - Set missingDeploymentIds = TheiaCloudDeploymentUtil.computeIdsOfMissingDeployments(appDefinition, - correlationId, instances, existingDeployments); + // Ensure pool has minimum capacity + ISpan poolSpan = Tracing.childSpan(tx, "appdef.ensure_capacity", "Ensure pool capacity"); + poolSpan.setData("min_instances", minInstances); - /* Create missing deployments for this app definition */ - for (int instance : missingDeploymentIds) { - createAndApplyDeployment(client.kubernetes(), client.namespace(), correlationId, appDefinitionResourceName, - appDefinitionResourceUID, instance, appDefinition, arguments.isUseKeycloak()); - } - return true; - } + boolean success = pool.ensureCapacity(appDefinition, minInstances, correlationId); - protected void createAndApplyService(NamespacedKubernetesClient client, String namespace, String correlationId, - String appDefinitionResourceName, String appDefinitionResourceUID, int instance, - AppDefinition appDefinition, boolean useOAuth2Proxy) { - Map replacements = TheiaCloudServiceUtil.getServiceReplacements(namespace, appDefinition, - instance); - String templateYaml = useOAuth2Proxy ? AddedHandlerUtil.TEMPLATE_SERVICE_YAML - : AddedHandlerUtil.TEMPLATE_SERVICE_WITHOUT_AOUTH2_PROXY_YAML; - String serviceYaml; - try { - serviceYaml = JavaResourceUtil.readResourceAndReplacePlaceholders(templateYaml, replacements, - correlationId); - } catch (IOException | URISyntaxException e) { - LOGGER.error( - formatLogMessage(correlationId, "Error while adjusting template for instance number " + instance), - e); - return; - } - K8sUtil.loadAndCreateServiceWithOwnerReference(client, namespace, correlationId, serviceYaml, AppDefinition.API, - AppDefinition.KIND, appDefinitionResourceName, appDefinitionResourceUID, 0); - } + poolSpan.setTag("pool.operation", "ensure_capacity"); + poolSpan.setTag("outcome", success ? "success" : "failure"); + Tracing.finish(poolSpan, success ? SpanStatus.OK : SpanStatus.INTERNAL_ERROR); - protected void createAndApplyDeployment(NamespacedKubernetesClient client, String namespace, String correlationId, - String appDefinitionResourceName, String appDefinitionResourceUID, int instance, - AppDefinition appDefinition, boolean useOAuth2Proxy) { - Map replacements = deploymentReplacements.getReplacements(namespace, appDefinition, instance); - String templateYaml = useOAuth2Proxy ? AddedHandlerUtil.TEMPLATE_DEPLOYMENT_YAML - : AddedHandlerUtil.TEMPLATE_DEPLOYMENT_WITHOUT_AOUTH2_PROXY_YAML; - String deploymentYaml; - try { - deploymentYaml = JavaResourceUtil.readResourceAndReplacePlaceholders(templateYaml, replacements, - correlationId); - } catch (IOException | URISyntaxException e) { - LOGGER.error( - formatLogMessage(correlationId, "Error while adjusting template for instance number " + instance), - e); - return; + tx.setData("min_instances", minInstances); + tx.setTag("outcome", success ? "success" : "failure"); + Tracing.finish(tx, success ? SpanStatus.OK : SpanStatus.INTERNAL_ERROR); + return success; + + } catch (Exception e) { + Tracing.finishError(tx, e); + throw e; } - K8sUtil.loadAndCreateDeploymentWithOwnerReference(client, namespace, correlationId, deploymentYaml, - AppDefinition.API, AppDefinition.KIND, appDefinitionResourceName, appDefinitionResourceUID, 0, - deployment -> { - bandwidthLimiter.limit(deployment, appDefinition.getSpec().getDownlinkLimit(), - appDefinition.getSpec().getUplinkLimit(), correlationId); - AddedHandlerUtil.removeEmptyResources(deployment); - if (appDefinition.getSpec().getPullSecret() != null - && !appDefinition.getSpec().getPullSecret().isEmpty()) { - AddedHandlerUtil.addImagePullSecret(deployment, appDefinition.getSpec().getPullSecret()); - } - }); } - protected void createAndApplyProxyConfigMap(NamespacedKubernetesClient client, String namespace, - String correlationId, String appDefinitionResourceName, String appDefinitionResourceUID, int instance, - AppDefinition appDefinition) { - Map replacements = TheiaCloudConfigMapUtil.getProxyConfigMapReplacements(namespace, - appDefinition, instance); - String configMapYaml; + @Override + public boolean appDefinitionDeleted(AppDefinition appDefinition, String correlationId) { + AppDefinitionSpec spec = appDefinition.getSpec(); + String appDefName = appDefinition.getMetadata().getName(); + + ITransaction tx = Tracing.startTransaction("appdef.deleted", "appdef"); + tx.setTag("app_definition", appDefName); + tx.setData("min_instances", spec.getMinInstances()); + tx.setData("correlation_id", correlationId); + tx.setTag("appdef.action", "deleted"); + + LOGGER.info(formatLogMessage(correlationId, "Deleting resources for " + spec)); + try { - configMapYaml = JavaResourceUtil.readResourceAndReplacePlaceholders( - AddedHandlerUtil.TEMPLATE_CONFIGMAP_YAML, replacements, correlationId); - } catch (IOException | URISyntaxException e) { - LOGGER.error( - formatLogMessage(correlationId, "Error while adjusting template for instance number " + instance), - e); - return; + // Release all pool resources + ISpan releaseSpan = Tracing.childSpan(tx, "appdef.release_all", "Release all pool resources"); + releaseSpan.setTag("pool.operation", "release_all"); + + boolean success = pool.releaseAll(appDefinition, correlationId); + + releaseSpan.setTag("outcome", success ? "success" : "failure"); + Tracing.finish(releaseSpan, success ? SpanStatus.OK : SpanStatus.INTERNAL_ERROR); + tx.setTag("outcome", success ? "success" : "failure"); + Tracing.finish(tx, success ? SpanStatus.OK : SpanStatus.INTERNAL_ERROR); + return success; + + } catch (Exception e) { + Tracing.finishError(tx, e); + throw e; } - K8sUtil.loadAndCreateConfigMapWithOwnerReference(client, namespace, correlationId, configMapYaml, - AppDefinition.API, AppDefinition.KIND, appDefinitionResourceName, appDefinitionResourceUID, 0, - configMap -> { - String host = arguments.getInstancesHost() + ingressPathProvider.getPath(appDefinition, instance); - int port = appDefinition.getSpec().getPort(); - AddedHandlerUtil.updateProxyConfigMap(client, namespace, configMap, host, port); - }); } - protected void createAndApplyEmailConfigMap(NamespacedKubernetesClient client, String namespace, - String correlationId, String appDefinitionResourceName, String appDefinitionResourceUID, int instance, - AppDefinition appDefinition) { - Map replacements = TheiaCloudConfigMapUtil.getEmailConfigMapReplacements(namespace, - appDefinition, instance); - String configMapYaml; + @Override + public boolean appDefinitionModified(AppDefinition appDefinition, String correlationId) { + AppDefinitionSpec spec = appDefinition.getSpec(); + String appDefName = appDefinition.getMetadata().getName(); + int minInstances = spec.getMinInstances(); + + ITransaction tx = Tracing.startTransaction("appdef.modified", "appdef"); + tx.setTag("app_definition", appDefName); + tx.setData("min_instances", minInstances); + tx.setData("correlation_id", correlationId); + tx.setTag("appdef.action", "modified"); + tx.setData("generation", appDefinition.getMetadata().getGeneration()); + + LOGGER.info(formatLogMessage(correlationId, "Reconciling " + spec)); + try { - configMapYaml = JavaResourceUtil.readResourceAndReplacePlaceholders( - AddedHandlerUtil.TEMPLATE_CONFIGMAP_EMAILS_YAML, replacements, correlationId); - } catch (IOException | URISyntaxException e) { - LOGGER.error( - formatLogMessage(correlationId, "Error while adjusting template for instance number " + instance), - e); - return; + // Verify ingress exists + ISpan ingressSpan = Tracing.childSpan(tx, "appdef.verify_ingress", "Verify ingress exists"); + ingressSpan.setData("route_name", spec.getIngressname()); + + if (!TheiaCloudIngressUtil.checkForExistingIngressAndAddOwnerReferencesIfMissing(client.kubernetes(), + client.namespace(), appDefinition, correlationId)) { + LOGGER.error(formatLogMessage(correlationId, "Expected HTTPRoute '" + spec.getIngressname() + + "' for app definition '" + appDefName + "' does not exist. Abort handling.")); + ingressSpan.setTag("outcome", "not_found"); + Tracing.finish(ingressSpan, SpanStatus.NOT_FOUND); + tx.setTag("error.reason", "ingress_not_found"); + tx.setTag("outcome", "failure"); + Tracing.finish(tx, SpanStatus.NOT_FOUND); + return false; + } + Tracing.finishSuccess(ingressSpan); + + ISpan sidecarValidationSpan = Tracing.childSpan(tx, "appdef.validate_sidecars", + "Validate sidecar configuration"); + if (!sidecarManager.validateUniqueSidecarNames(appDefinition, correlationId)) { + sidecarValidationSpan.setTag("outcome", "invalid"); + Tracing.finish(sidecarValidationSpan, SpanStatus.INVALID_ARGUMENT); + tx.setTag("error.reason", "duplicate_sidecar_names"); + tx.setTag("outcome", "failure"); + Tracing.finish(tx, SpanStatus.INVALID_ARGUMENT); + return false; + } + Tracing.finishSuccess(sidecarValidationSpan); + + // Reconcile pool to target instance count + ISpan reconcileSpan = Tracing.childSpan(tx, "appdef.reconcile_pool", "Reconcile pool to target"); + reconcileSpan.setData("target_instances", minInstances); + reconcileSpan.setTag("pool.operation", "reconcile"); + + boolean success = pool.reconcile(appDefinition, minInstances, correlationId); + + reconcileSpan.setTag("outcome", success ? "success" : "failure"); + Tracing.finish(reconcileSpan, success ? SpanStatus.OK : SpanStatus.INTERNAL_ERROR); + tx.setTag("outcome", success ? "success" : "failure"); + Tracing.finish(tx, success ? SpanStatus.OK : SpanStatus.INTERNAL_ERROR); + return success; + + } catch (Exception e) { + Tracing.finishError(tx, e); + throw e; } - K8sUtil.loadAndCreateConfigMapWithOwnerReference(client, namespace, correlationId, configMapYaml, - AppDefinition.API, AppDefinition.KIND, appDefinitionResourceName, appDefinitionResourceUID, 0); } - } diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/appdef/LazyStartAppDefinitionHandler.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/appdef/LazyStartAppDefinitionHandler.java index 1597d0e4e..abdd18e2d 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/appdef/LazyStartAppDefinitionHandler.java +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/appdef/LazyStartAppDefinitionHandler.java @@ -29,6 +29,7 @@ import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinitionSpec; import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinitionStatus; import org.eclipse.theia.cloud.operator.ingress.IngressPathProvider; +import org.eclipse.theia.cloud.operator.sidecar.SidecarManager; import org.eclipse.theia.cloud.operator.util.TheiaCloudIngressUtil; import com.google.inject.Inject; @@ -43,6 +44,9 @@ public class LazyStartAppDefinitionHandler implements AppDefinitionHandler { @Inject protected IngressPathProvider ingressPathProvider; + @Inject + protected SidecarManager sidecarManager; + @Override public boolean appDefinitionAdded(AppDefinition appDefinition, String correlationId) { try { @@ -99,15 +103,23 @@ protected boolean doAppDefinitionAdded(AppDefinition appDefinition, String corre if (!TheiaCloudIngressUtil.checkForExistingIngressAndAddOwnerReferencesIfMissing(client.kubernetes(), client.namespace(), appDefinition, correlationId)) { LOGGER.error(formatLogMessage(correlationId, - "Expected ingress '" + spec.getIngressname() + "' for app definition '" + appDefinitionResourceName + "Expected HTTPRoute '" + spec.getIngressname() + "' for app definition '" + appDefinitionResourceName + "' does not exist. Abort handling app definition.")); client.appDefinitions().updateStatus(correlationId, appDefinition, s -> { s.setOperatorStatus(OperatorStatus.ERROR); - s.setOperatorMessage("Ingress does not exist."); + s.setOperatorMessage("HTTPRoute does not exist."); }); return false; } else { - LOGGER.trace(formatLogMessage(correlationId, "Ingress available already")); + LOGGER.trace(formatLogMessage(correlationId, "HTTPRoute available already")); + } + + if (!sidecarManager.validateUniqueSidecarNames(appDefinition, correlationId)) { + client.appDefinitions().updateStatus(correlationId, appDefinition, s -> { + s.setOperatorStatus(OperatorStatus.ERROR); + s.setOperatorMessage("Duplicate sidecar names found in AppDefinition; sidecar names must be unique."); + }); + return false; } client.appDefinitions().updateStatus(correlationId, appDefinition, s -> { diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/session/EagerSessionHandler.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/session/EagerSessionHandler.java new file mode 100644 index 000000000..7e6909309 --- /dev/null +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/session/EagerSessionHandler.java @@ -0,0 +1,368 @@ +/******************************************************************************** + * Copyright (C) 2022-2025 EclipseSource, Lockular, Ericsson, STMicroelectronics and + * others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.operator.handler.session; + +import static org.eclipse.theia.cloud.common.util.LogMessageUtil.formatLogMessage; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.theia.cloud.common.k8s.client.TheiaCloudClient; +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinition; +import org.eclipse.theia.cloud.common.k8s.resource.session.Session; +import org.eclipse.theia.cloud.common.k8s.resource.session.SessionSpec; +import org.eclipse.theia.cloud.common.util.DataBridgeUtil; +import org.eclipse.theia.cloud.operator.databridge.AsyncDataInjector; +import org.eclipse.theia.cloud.operator.handler.AddedHandlerUtil; +import org.eclipse.theia.cloud.operator.ingress.IngressManager; +import org.eclipse.theia.cloud.operator.sidecar.SidecarManager; +import org.eclipse.theia.cloud.operator.pool.PrewarmedResourcePool; +import org.eclipse.theia.cloud.common.tracing.Tracing; +import org.eclipse.theia.cloud.operator.util.SessionEnvCollector; +import org.eclipse.theia.cloud.operator.pool.PrewarmedResourcePool.PoolInstance; +import org.eclipse.theia.cloud.operator.pool.PrewarmedResourcePool.ReservationOutcome; +import org.eclipse.theia.cloud.operator.pool.PrewarmedResourcePool.ReservationResult; + +import com.google.inject.Inject; + +import io.fabric8.kubernetes.api.model.gatewayapi.v1.HTTPRoute; +import io.fabric8.kubernetes.client.KubernetesClientException; +import io.sentry.ISpan; +import io.sentry.Sentry; +import io.sentry.SpanStatus; + +/** + * A {@link SessionHandler} that uses prewarmed instances from the pool. This handler delegates to + * {@link PrewarmedResourcePool} for instance management and {@link IngressManager} for ingress operations. + */ +public class EagerSessionHandler implements SessionHandler { + + private static final Logger LOGGER = LogManager.getLogger(EagerSessionHandler.class); + + public static final String SESSION_START_STRATEGY_ANNOTATION = "theia-cloud.io/session-start-strategy"; + public static final String SESSION_START_STRATEGY_EAGER = "eager"; + public static final String SESSION_INSTANCE_ID_ANNOTATION = "theia-cloud.io/instance-id"; + + /** + * Outcome of trying to handle a session with eager start. + */ + public enum EagerSessionAddedOutcome { + HANDLED, NO_CAPACITY, ERROR + } + + @Inject + private TheiaCloudClient client; + + @Inject + private PrewarmedResourcePool pool; + + @Inject + private IngressManager ingressManager; + + @Inject + private SidecarManager sidecarManager; + + @Inject + private SessionEnvCollector sessionEnvCollector; + + @Inject + private AsyncDataInjector asyncDataInjector; + + @Override + public boolean sessionAdded(Session session, String correlationId, ISpan span) { + return trySessionAdded(session, correlationId, span) == EagerSessionAddedOutcome.HANDLED; + } + + /** + * Tries to handle a session using eager start. Returns the outcome so callers can fall back to lazy start if + * needed. + */ + public EagerSessionAddedOutcome trySessionAdded(Session session, String correlationId, ISpan parentSpan) { + SessionSpec spec = session.getSpec(); + String sessionName = spec.getName(); + String appDefinitionID = spec.getAppDefinition(); + + // Create child span from parent span + ISpan span = Tracing.childSpan(parentSpan, "eager.setup", "Eager session setup"); + + span.setData("session_name", sessionName); + span.setData("app_definition", appDefinitionID); + span.setData("user", spec.getUser()); + + LOGGER.info(formatLogMessage(correlationId, "Handling sessionAdded " + spec)); + + try { + SessionStatusUtil.PreHandleResult preHandle = SessionStatusUtil.evaluateStatus(session, client, + correlationId, LOGGER); + if (preHandle == SessionStatusUtil.PreHandleResult.ALREADY_HANDLED) { + span.setTag("outcome", "already_handled"); + Tracing.finish(span, SpanStatus.OK); + return EagerSessionAddedOutcome.HANDLED; + } + if (preHandle == SessionStatusUtil.PreHandleResult.INTERRUPTED + || preHandle == SessionStatusUtil.PreHandleResult.PREVIOUS_ERROR) { + span.setTag("outcome", "previous_error"); + Tracing.finish(span, SpanStatus.ABORTED); + return EagerSessionAddedOutcome.ERROR; + } + + boolean markedHandling = true; + try { + SessionStatusUtil.markHandling(client, session, correlationId); + } catch (RuntimeException ex) { + LOGGER.warn(formatLogMessage(correlationId, + "Unable to mark session as HANDLING. Treating as interrupted/race."), ex); + markedHandling = false; + } + // Check if the session was already handled by another instance + if (!markedHandling) { + Optional latestSession = client.sessions().get(session.getMetadata().getName()); + if (latestSession.isPresent()) { + SessionStatusUtil.PreHandleResult latestPreHandle = SessionStatusUtil.evaluateStatus( + latestSession.get(), client, correlationId, LOGGER); + if (latestPreHandle == SessionStatusUtil.PreHandleResult.ALREADY_HANDLED) { + span.setTag("outcome", "already_handled"); + Tracing.finish(span, SpanStatus.OK); + return EagerSessionAddedOutcome.HANDLED; + } + if (latestPreHandle == SessionStatusUtil.PreHandleResult.INTERRUPTED + || latestPreHandle == SessionStatusUtil.PreHandleResult.PREVIOUS_ERROR) { + span.setTag("outcome", "interrupted"); + Tracing.finish(span, SpanStatus.ABORTED); + return EagerSessionAddedOutcome.ERROR; + } + } + span.setTag("outcome", "interrupted"); + Tracing.finish(span, SpanStatus.ABORTED); + return EagerSessionAddedOutcome.ERROR; + } + + // Find app definition + ISpan appDefSpan = Tracing.childSpan(span, "eager.find_appdef", "Find app definition"); + Optional appDefOpt = client.appDefinitions().get(appDefinitionID); + if (appDefOpt.isEmpty()) { + LOGGER.error( + formatLogMessage(correlationId, "No App Definition with name " + appDefinitionID + " found.")); + SessionStatusUtil.markError(client, session, correlationId, "App Definition not found."); + appDefSpan.setTag("outcome", "not_found"); + Tracing.finish(appDefSpan, SpanStatus.NOT_FOUND); + span.setTag("outcome", "error"); + Tracing.finish(span, SpanStatus.NOT_FOUND); + return EagerSessionAddedOutcome.ERROR; + } + AppDefinition appDef = appDefOpt.get(); + Tracing.finishSuccess(appDefSpan); + + // Find HTTPRoute + ISpan ingressSpan = Tracing.childSpan(span, "eager.find_route", "Find HTTPRoute"); + Optional routeOpt = ingressManager.getIngress(appDef, correlationId); + if (routeOpt.isEmpty()) { + LOGGER.error(formatLogMessage(correlationId, + "No HTTPRoute for app definition " + appDefinitionID + " found.")); + SessionStatusUtil.markError(client, session, correlationId, "HTTPRoute not available."); + ingressSpan.setTag("outcome", "not_found"); + Tracing.finish(ingressSpan, SpanStatus.NOT_FOUND); + span.setTag("outcome", "error"); + Tracing.finish(span, SpanStatus.NOT_FOUND); + return EagerSessionAddedOutcome.ERROR; + } + HTTPRoute route = routeOpt.get(); + Tracing.finishSuccess(ingressSpan); + + // Reserve an instance from the pool + ISpan reserveSpan = Tracing.childSpan(span, "eager.reserve_instance", "Reserve pool instance"); + ReservationResult reservation = pool.reserveInstance(session, appDef, correlationId); + reserveSpan.setTag("pool.outcome", reservation.getOutcome().name().toLowerCase()); + + if (reservation.getOutcome() == ReservationOutcome.NO_CAPACITY) { + reserveSpan.setTag("outcome", "no_capacity"); + Tracing.finish(reserveSpan, SpanStatus.RESOURCE_EXHAUSTED); + span.setTag("outcome", "no_capacity"); + Tracing.finish(span, SpanStatus.RESOURCE_EXHAUSTED); + return EagerSessionAddedOutcome.NO_CAPACITY; + } + if (reservation.getOutcome() == ReservationOutcome.ERROR) { + SessionStatusUtil.markError(client, session, correlationId, "Failed to reserve prewarmed instance."); + reserveSpan.setTag("outcome", "error"); + Tracing.finish(reserveSpan, SpanStatus.INTERNAL_ERROR); + span.setTag("outcome", "error"); + Tracing.finish(span, SpanStatus.INTERNAL_ERROR); + return EagerSessionAddedOutcome.ERROR; + } + + PoolInstance instance = reservation.getInstance().get(); + reserveSpan.setData("instance_id", instance.getInstanceId()); + Tracing.finishSuccess(reserveSpan); + + // Annotate session with start strategy and instance ID + ISpan annotateSpan = Tracing.childSpan(span, "eager.annotate_session", "Annotate session"); + annotateSession(session, correlationId, instance.getInstanceId()); + Tracing.finishSuccess(annotateSpan); + + // Complete session setup (labels, deployment ownership, email config) + ISpan setupSpan = Tracing.childSpan(span, "eager.complete_setup", "Complete session setup"); + setupSpan.setData("instance_id", instance.getInstanceId()); + if (!pool.completeSessionSetup(session, appDef, instance, correlationId)) { + SessionStatusUtil.markError(client, session, correlationId, "Failed to complete session setup."); + setupSpan.setTag("outcome", "failure"); + Tracing.finish(setupSpan, SpanStatus.INTERNAL_ERROR); + span.setTag("outcome", "error"); + Tracing.finish(span, SpanStatus.INTERNAL_ERROR); + return EagerSessionAddedOutcome.ERROR; + } + Tracing.finishSuccess(setupSpan); + + // Schedule async credential injection via data bridge (tracked in separate transaction) + if (DataBridgeUtil.isDataBridgeEnabled(appDef.getSpec())) { + Map envVars = sessionEnvCollector.collect(session, correlationId); + if (!envVars.isEmpty()) { + Sentry.addBreadcrumb("Scheduling data bridge injection with " + envVars.size() + " env vars", + "databridge"); + asyncDataInjector.scheduleInjection(span, session, envVars, correlationId); + } + } + + // Create the dedicated session HTTPRoute after the pool instance is fully assigned. + ISpan ingressRuleSpan = Tracing.childSpan(span, "eager.add_route_rule", "Add HTTPRoute rule"); + String host; + try { + host = ingressManager.addRuleForSession(route, instance.getExternalService(), appDef, + session, instance.getInstanceId(), correlationId); + ingressRuleSpan.setData("host", host); + Tracing.finishSuccess(ingressRuleSpan); + } catch (KubernetesClientException e) { + LOGGER.error(formatLogMessage(correlationId, "Error while editing HTTPRoute"), e); + SessionStatusUtil.markError(client, session, correlationId, "Failed to edit HTTPRoute."); + Tracing.finishError(ingressRuleSpan, e); + span.setTag("outcome", "error"); + Tracing.finish(span, SpanStatus.INTERNAL_ERROR); + return EagerSessionAddedOutcome.ERROR; + } + + Sentry.addBreadcrumb("Scheduling URL availability check for " + host, "session"); + AddedHandlerUtil.updateSessionURLAsync(client.sessions(), session, client.namespace(), host, correlationId, span); + + span.setData("instance_id", instance.getInstanceId()); + SessionStatusUtil.markHandled(client, session, correlationId, null); + Tracing.finishSuccess(span); + return EagerSessionAddedOutcome.HANDLED; + + } catch (Exception e) { + SessionStatusUtil.markError(client, session, correlationId, + "Unexpected error. Please check the logs for correlationId: " + correlationId); + Tracing.finishError(span, e); + throw e; + } + } + + @Override + public boolean sessionDeleted(Session session, String correlationId, ISpan parentSpan) { + SessionSpec spec = session.getSpec(); + String sessionName = spec.getName(); + String appDefinitionID = spec.getAppDefinition(); + + ISpan span = Tracing.childSpan(parentSpan, "eager.cleanup", "Eager session cleanup"); + + span.setData("session_name", sessionName); + span.setData("app_definition", appDefinitionID); + + LOGGER.info(formatLogMessage(correlationId, "Handling sessionDeleted " + spec)); + + try { + // Find app definition + ISpan appDefSpan = Tracing.childSpan(span, "eager.find_appdef", "Find app definition"); + Optional appDefOpt = client.appDefinitions().get(appDefinitionID); + if (appDefOpt.isEmpty()) { + LOGGER.info(formatLogMessage(correlationId, + "No App Definition found. Resources will be cleaned up by K8s garbage collection.")); + appDefSpan.setTag("outcome", "not_found"); + Tracing.finish(appDefSpan, SpanStatus.NOT_FOUND); + Tracing.finishSuccess(span); // This is OK - K8s GC handles it + return true; + } + AppDefinition appDef = appDefOpt.get(); + Tracing.finishSuccess(appDefSpan); + + // Get instance ID from session annotation + Integer instanceId = getInstanceIdFromAnnotation(session); + if (instanceId == null) { + LOGGER.error(formatLogMessage(correlationId, + "Session missing instance-id annotation. Cannot determine which instance to release.")); + span.setTag("error.reason", "missing_instance_id_annotation"); + span.setTag("outcome", "failure"); + Tracing.finish(span, SpanStatus.INTERNAL_ERROR); + return false; + } + span.setData("instance_id", instanceId); + + sidecarManager.restartPrewarmedSidecarPods(appDef, instanceId, correlationId); + + // Release instance back to pool + ISpan releaseSpan = Tracing.childSpan(span, "eager.release_instance", "Release pool instance"); + releaseSpan.setData("instance_id", instanceId); + boolean success = pool.releaseInstance(session, appDef, correlationId); + releaseSpan.setTag("outcome", success ? "success" : "failure"); + Tracing.finish(releaseSpan, success ? SpanStatus.OK : SpanStatus.INTERNAL_ERROR); + + // Reconcile the specific instance + ISpan reconcileSpan = Tracing.childSpan(span, "eager.reconcile_instance", "Reconcile released instance"); + reconcileSpan.setData("instance_id", instanceId); + pool.reconcileInstance(appDef, instanceId, correlationId); + Tracing.finishSuccess(reconcileSpan); + + span.setTag("outcome", success ? "success" : "failure"); + Tracing.finish(span, success ? SpanStatus.OK : SpanStatus.INTERNAL_ERROR); + return success; + + } catch (Exception e) { + Tracing.finishError(span, e); + throw e; + } + } + + private void annotateSession(Session session, String correlationId, int instanceId) { + String name = session.getMetadata().getName(); + client.sessions().edit(correlationId, name, s -> { + Map annotations = s.getMetadata().getAnnotations(); + if (annotations == null) { + annotations = new HashMap<>(); + s.getMetadata().setAnnotations(annotations); + } + annotations.put(SESSION_START_STRATEGY_ANNOTATION, SESSION_START_STRATEGY_EAGER); + annotations.put(SESSION_INSTANCE_ID_ANNOTATION, String.valueOf(instanceId)); + }); + } + + private Integer getInstanceIdFromAnnotation(Session session) { + Map annotations = session.getMetadata().getAnnotations(); + if (annotations == null) { + return null; + } + String idStr = annotations.get(SESSION_INSTANCE_ID_ANNOTATION); + if (idStr == null) { + return null; + } + try { + return Integer.parseInt(idStr); + } catch (NumberFormatException e) { + return null; + } + } +} diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/session/EagerStartSessionHandler.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/session/EagerStartSessionHandler.java deleted file mode 100644 index c9ddd2055..000000000 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/session/EagerStartSessionHandler.java +++ /dev/null @@ -1,247 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2022 EclipseSource, Lockular, Ericsson, STMicroelectronics and - * others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ -package org.eclipse.theia.cloud.operator.handler.session; - -import static org.eclipse.theia.cloud.common.util.LogMessageUtil.formatLogMessage; - -import java.util.Collections; -import java.util.List; -import java.util.Map.Entry; -import java.util.Optional; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.eclipse.theia.cloud.common.k8s.client.TheiaCloudClient; -import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinition; -import org.eclipse.theia.cloud.common.k8s.resource.session.Session; -import org.eclipse.theia.cloud.common.k8s.resource.session.SessionSpec; -import org.eclipse.theia.cloud.common.util.JavaUtil; -import org.eclipse.theia.cloud.operator.TheiaCloudOperatorArguments; -import org.eclipse.theia.cloud.operator.handler.AddedHandlerUtil; -import org.eclipse.theia.cloud.operator.ingress.IngressPathProvider; -import org.eclipse.theia.cloud.operator.util.K8sUtil; -import org.eclipse.theia.cloud.operator.util.TheiaCloudConfigMapUtil; -import org.eclipse.theia.cloud.operator.util.TheiaCloudDeploymentUtil; -import org.eclipse.theia.cloud.operator.util.TheiaCloudHandlerUtil; -import org.eclipse.theia.cloud.operator.util.TheiaCloudServiceUtil; - -import com.google.inject.Inject; - -import io.fabric8.kubernetes.api.model.Service; -import io.fabric8.kubernetes.api.model.networking.v1.HTTPIngressPath; -import io.fabric8.kubernetes.api.model.networking.v1.HTTPIngressRuleValue; -import io.fabric8.kubernetes.api.model.networking.v1.Ingress; -import io.fabric8.kubernetes.api.model.networking.v1.IngressBackend; -import io.fabric8.kubernetes.api.model.networking.v1.IngressRule; -import io.fabric8.kubernetes.api.model.networking.v1.IngressServiceBackend; -import io.fabric8.kubernetes.api.model.networking.v1.ServiceBackendPort; -import io.fabric8.kubernetes.client.KubernetesClientException; -import io.fabric8.kubernetes.client.NamespacedKubernetesClient; - -/** - * A {@link SessionAddedHandler} that relies on the fact that the app definition handler created spare deployments to - * use. - */ -public class EagerStartSessionHandler implements SessionHandler { - - private static final Logger LOGGER = LogManager.getLogger(EagerStartSessionHandler.class); - - @Inject - private TheiaCloudClient client; - - @Inject - protected IngressPathProvider ingressPathProvider; - - @Inject - protected TheiaCloudOperatorArguments arguments; - - @Override - public boolean sessionAdded(Session session, String correlationId) { - SessionSpec spec = session.getSpec(); - LOGGER.info(formatLogMessage(correlationId, "Handling " + spec)); - - String sessionResourceName = session.getMetadata().getName(); - String sessionResourceUID = session.getMetadata().getUid(); - - String appDefinitionID = spec.getAppDefinition(); - String userEmail = spec.getUser(); - - /* find app definition for session */ - Optional appDefinition = client.appDefinitions().get(appDefinitionID); - if (appDefinition.isEmpty()) { - LOGGER.error(formatLogMessage(correlationId, "No App Definition with name " + appDefinitionID + " found.")); - return false; - } - - String appDefinitionResourceName = appDefinition.get().getMetadata().getName(); - String appDefinitionResourceUID = appDefinition.get().getMetadata().getUid(); - int port = appDefinition.get().getSpec().getPort(); - - /* find ingress */ - Optional ingress = K8sUtil.getExistingIngress(client.kubernetes(), client.namespace(), - appDefinitionResourceName, appDefinitionResourceUID); - if (ingress.isEmpty()) { - LOGGER.error( - formatLogMessage(correlationId, "No Ingress for app definition " + appDefinitionID + " found.")); - return false; - } - - /* get a service to use */ - Entry, Boolean> reserveServiceResult = reserveService(client.kubernetes(), client.namespace(), - appDefinitionResourceName, appDefinitionResourceUID, appDefinitionID, sessionResourceName, - sessionResourceUID, correlationId); - if (reserveServiceResult.getValue()) { - LOGGER.info(formatLogMessage(correlationId, "Found an already reserved service")); - return true; - } - Optional serviceToUse = reserveServiceResult.getKey(); - if (serviceToUse.isEmpty()) { - LOGGER.error( - formatLogMessage(correlationId, "No Service for app definition " + appDefinitionID + " found.")); - return false; - } - - /* get the deployment for the service and add as owner */ - Integer instance = TheiaCloudServiceUtil.getId(correlationId, appDefinition.get(), serviceToUse.get()); - if (instance == null) { - LOGGER.error(formatLogMessage(correlationId, "Error while getting instance from Service")); - return false; - } - - try { - client.kubernetes().apps().deployments() - .withName(TheiaCloudDeploymentUtil.getDeploymentName(appDefinition.get(), instance)) - .edit(deployment -> TheiaCloudHandlerUtil.addOwnerReferenceToItem(correlationId, - sessionResourceName, sessionResourceUID, deployment)); - } catch (KubernetesClientException e) { - LOGGER.error(formatLogMessage(correlationId, "Error while editing deployment " - + (appDefinitionID + TheiaCloudDeploymentUtil.DEPLOYMENT_NAME + instance)), e); - return false; - } - - if (arguments.isUseKeycloak()) { - /* add user to allowed emails */ - try { - client.kubernetes().configMaps() - .withName(TheiaCloudConfigMapUtil.getEmailConfigName(appDefinition.get(), instance)) - .edit(configmap -> { - configmap.setData(Collections - .singletonMap(AddedHandlerUtil.FILENAME_AUTHENTICATED_EMAILS_LIST, userEmail)); - return configmap; - }); - } catch (KubernetesClientException e) { - LOGGER.error( - formatLogMessage(correlationId, - "Error while editing email configmap " - + (appDefinitionID + TheiaCloudConfigMapUtil.CONFIGMAP_EMAIL_NAME + instance)), - e); - return false; - } - } - - /* adjust the ingress */ - String host; - try { - host = updateIngress(ingress, serviceToUse, appDefinitionID, instance, port, appDefinition.get(), - correlationId); - } catch (KubernetesClientException e) { - LOGGER.error(formatLogMessage(correlationId, - "Error while editing ingress " + ingress.get().getMetadata().getName()), e); - return false; - } - - /* Update session resource */ - try { - AddedHandlerUtil.updateSessionURLAsync(client.sessions(), session, client.namespace(), host, correlationId); - } catch (KubernetesClientException e) { - LOGGER.error( - formatLogMessage(correlationId, "Error while editing session " + session.getMetadata().getName()), - e); - return false; - } - - return true; - } - - protected synchronized Entry, Boolean> reserveService(NamespacedKubernetesClient client, - String namespace, String appDefinitionResourceName, String appDefinitionResourceUID, String appDefinitionID, - String sessionResourceName, String sessionResourceUID, String correlationId) { - List existingServices = K8sUtil.getExistingServices(client, namespace, appDefinitionResourceName, - appDefinitionResourceUID); - - Optional alreadyReservedService = TheiaCloudServiceUtil.getServiceOwnedBySession(sessionResourceName, - sessionResourceUID, existingServices); - if (alreadyReservedService.isPresent()) { - return JavaUtil.tuple(alreadyReservedService, true); - } - - Optional serviceToUse = TheiaCloudServiceUtil.getUnusedService(existingServices); - if (serviceToUse.isEmpty()) { - return JavaUtil.tuple(serviceToUse, false); - } - - /* add our session as owner to the service */ - try { - client.services().inNamespace(namespace).withName(serviceToUse.get().getMetadata().getName()) - .edit(service -> TheiaCloudHandlerUtil.addOwnerReferenceToItem(correlationId, sessionResourceName, - sessionResourceUID, service)); - } catch (KubernetesClientException e) { - LOGGER.error(formatLogMessage(correlationId, - "Error while editing service " + (serviceToUse.get().getMetadata().getName())), e); - return JavaUtil.tuple(Optional.empty(), false); - } - return JavaUtil.tuple(serviceToUse, false); - } - - protected synchronized String updateIngress(Optional ingress, Optional serviceToUse, - String appDefinitionID, int instance, int port, AppDefinition appDefinition, String correlationId) { - final String host = arguments.getInstancesHost(); - String path = ingressPathProvider.getPath(appDefinition, instance); - client.ingresses().edit(correlationId, ingress.get().getMetadata().getName(), - ingressToUpdate -> addIngressRule(ingressToUpdate, serviceToUse.get(), host, port, path)); - return host + path + "/"; - } - - protected Ingress addIngressRule(Ingress ingress, Service serviceToUse, String host, int port, String path) { - IngressRule ingressRule = new IngressRule(); - ingress.getSpec().getRules().add(ingressRule); - - ingressRule.setHost(host); - - HTTPIngressRuleValue http = new HTTPIngressRuleValue(); - ingressRule.setHttp(http); - - HTTPIngressPath httpIngressPath = new HTTPIngressPath(); - http.getPaths().add(httpIngressPath); - httpIngressPath.setPath(path + AddedHandlerUtil.INGRESS_REWRITE_PATH); - httpIngressPath.setPathType("Prefix"); - - IngressBackend ingressBackend = new IngressBackend(); - httpIngressPath.setBackend(ingressBackend); - - IngressServiceBackend ingressServiceBackend = new IngressServiceBackend(); - ingressBackend.setService(ingressServiceBackend); - ingressServiceBackend.setName(serviceToUse.getMetadata().getName()); - - ServiceBackendPort serviceBackendPort = new ServiceBackendPort(); - ingressServiceBackend.setPort(serviceBackendPort); - serviceBackendPort.setNumber(port); - - return ingress; - } - -} diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/session/EagerWithLazyFallbackSessionHandler.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/session/EagerWithLazyFallbackSessionHandler.java new file mode 100644 index 000000000..903f68fe0 --- /dev/null +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/session/EagerWithLazyFallbackSessionHandler.java @@ -0,0 +1,131 @@ +package org.eclipse.theia.cloud.operator.handler.session; + +import static org.eclipse.theia.cloud.common.util.LogMessageUtil.formatLogMessage; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.theia.cloud.common.k8s.client.TheiaCloudClient; +import org.eclipse.theia.cloud.common.k8s.resource.session.Session; +import org.eclipse.theia.cloud.common.tracing.Tracing; + +import com.google.inject.Inject; + +import io.sentry.ISpan; +import io.sentry.SpanStatus; + +/** + * Tries to handle a session with {@link EagerSessionHandler} first. If there is + * no prewarmed capacity left, falls back + * to {@link LazySessionHandler}. + */ +public class EagerWithLazyFallbackSessionHandler implements SessionHandler { + + /** + * Value indicating session was started lazily after eager capacity was + * exhausted. + */ + public static final String SESSION_START_STRATEGY_LAZY_FALLBACK = "lazy-fallback"; + + private static final Logger LOGGER = LogManager.getLogger(EagerWithLazyFallbackSessionHandler.class); + + @Inject + private EagerSessionHandler eager; + + @Inject + private LazySessionHandler lazy; + + @Inject + private TheiaCloudClient client; + + @Override + public boolean sessionAdded(Session session, String correlationId, ISpan span) { + // Try eager start first + ISpan eagerSpan = Tracing.childSpan(span, "session.eager_attempt", "Attempt eager session start"); + eagerSpan.setTag("eagersession.strategy", "eager"); + + EagerSessionHandler.EagerSessionAddedOutcome eagerOutcome = eager.trySessionAdded(session, correlationId, + eagerSpan); + eagerSpan.setData("eager_outcome", eagerOutcome.name()); + + if (eagerOutcome == EagerSessionHandler.EagerSessionAddedOutcome.HANDLED) { + Tracing.finishSuccess(eagerSpan); + span.setTag("session.strategy", "eager"); + return true; + } + + if (eagerOutcome == EagerSessionHandler.EagerSessionAddedOutcome.ERROR) { + eagerSpan.setTag("outcome", "error"); + Tracing.finish(eagerSpan, SpanStatus.INTERNAL_ERROR); + span.setTag("session.strategy", "eager"); + return false; + } + + // NO_CAPACITY - fall back to lazy + eagerSpan.setTag("outcome", "no_capacity"); + Tracing.finish(eagerSpan, SpanStatus.RESOURCE_EXHAUSTED); + span.setTag("fallback", "true"); + span.setTag("fallback.reason", "no_prewarmed_capacity"); + + LOGGER.info(formatLogMessage(correlationId, + "No prewarmed capacity left. Falling back to lazy session handling.")); + + ISpan lazySpan = Tracing.childSpan(span, "session.lazy_fallback", "Fallback to lazy session start"); + lazySpan.setTag("session.strategy", "lazy-fallback"); + lazySpan.setTag("fallback.reason", "no_prewarmed_capacity"); + + boolean lazyResult = lazy.sessionAdded(session, correlationId, lazySpan); + + if (lazyResult) { + annotateSessionStrategy(session, correlationId, SESSION_START_STRATEGY_LAZY_FALLBACK); + Tracing.finishSuccess(lazySpan); + span.setTag("session.strategy", "lazy-fallback"); + } else { + lazySpan.setTag("outcome", "failure"); + Tracing.finish(lazySpan, SpanStatus.INTERNAL_ERROR); + span.setTag("session.strategy", "lazy-fallback"); + } + + return lazyResult; + } + + @Override + public boolean sessionDeleted(Session session, String correlationId, ISpan parentSpan) { + String strategy = Optional.ofNullable(session.getMetadata()).map(m -> m.getAnnotations()) + .map(a -> a.get(EagerSessionHandler.SESSION_START_STRATEGY_ANNOTATION)).orElse("unknown"); + + parentSpan.setTag("session.start_strategy", strategy); + + boolean result; + if (EagerSessionHandler.SESSION_START_STRATEGY_EAGER.equals(strategy)) { + ISpan span = Tracing.childSpan(parentSpan, "session.eager_cleanup", "Eager session cleanup"); + span.setTag("cleanup.type", "eager"); + result = eager.sessionDeleted(session, correlationId, span); + span.setTag("outcome", result ? "success" : "failure"); + Tracing.finish(span, result ? SpanStatus.OK : SpanStatus.INTERNAL_ERROR); + } else { + ISpan span = Tracing.childSpan(parentSpan, "session.lazy_cleanup", "Lazy session cleanup"); + span.setTag("cleanup.type", "lazy"); + result = lazy.sessionDeleted(session, correlationId, span); + span.setTag("outcome", result ? "success" : "failure"); + Tracing.finish(span, result ? SpanStatus.OK : SpanStatus.INTERNAL_ERROR); + } + + return result; + } + + private void annotateSessionStrategy(Session session, String correlationId, String strategy) { + String name = session.getMetadata().getName(); + client.sessions().edit(correlationId, name, s -> { + Map annotations = s.getMetadata().getAnnotations(); + if (annotations == null) { + annotations = new HashMap<>(); + s.getMetadata().setAnnotations(annotations); + } + annotations.put(EagerSessionHandler.SESSION_START_STRATEGY_ANNOTATION, strategy); + }); + } +} diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/session/LazySessionHandler.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/session/LazySessionHandler.java index 88100f956..1c8e00835 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/session/LazySessionHandler.java +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/session/LazySessionHandler.java @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (C) 2022-2023 EclipseSource, Lockular, Ericsson, STMicroelectronics and + * Copyright (C) 2022-2025 EclipseSource, Lockular, Ericsson, STMicroelectronics and * others. * * This program and the accompanying materials are made available under the @@ -19,10 +19,6 @@ import static org.eclipse.theia.cloud.common.util.LogMessageUtil.formatLogMessage; import static org.eclipse.theia.cloud.common.util.LogMessageUtil.formatMetric; -import java.io.IOException; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; @@ -30,31 +26,30 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.eclipse.theia.cloud.common.k8s.client.TheiaCloudClient; -import org.eclipse.theia.cloud.common.k8s.resource.OperatorStatus; -import org.eclipse.theia.cloud.common.k8s.resource.ResourceStatus; import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinition; import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinitionSpec; import org.eclipse.theia.cloud.common.k8s.resource.session.Session; import org.eclipse.theia.cloud.common.k8s.resource.session.SessionSpec; -import org.eclipse.theia.cloud.common.k8s.resource.session.SessionStatus; import org.eclipse.theia.cloud.common.k8s.resource.workspace.Workspace; +import org.eclipse.theia.cloud.common.util.LabelsUtil; import org.eclipse.theia.cloud.common.util.TheiaCloudError; import org.eclipse.theia.cloud.common.util.WorkspaceUtil; import org.eclipse.theia.cloud.operator.TheiaCloudOperatorArguments; -import org.eclipse.theia.cloud.operator.bandwidth.BandwidthLimiter; import org.eclipse.theia.cloud.operator.handler.AddedHandlerUtil; -import org.eclipse.theia.cloud.operator.ingress.IngressPathProvider; -import org.eclipse.theia.cloud.operator.replacements.DeploymentTemplateReplacements; -import org.eclipse.theia.cloud.operator.util.JavaResourceUtil; +import org.eclipse.theia.cloud.operator.ingress.IngressManager; +import org.eclipse.theia.cloud.operator.sidecar.SidecarManager; +import org.eclipse.theia.cloud.operator.util.K8sResourceFactory; import org.eclipse.theia.cloud.operator.util.K8sUtil; -import org.eclipse.theia.cloud.operator.util.TheiaCloudConfigMapUtil; -import org.eclipse.theia.cloud.operator.util.TheiaCloudIngressUtil; import org.eclipse.theia.cloud.operator.util.TheiaCloudK8sUtil; +import org.eclipse.theia.cloud.common.tracing.Tracing; import org.eclipse.theia.cloud.operator.util.TheiaCloudPersistentVolumeUtil; -import org.eclipse.theia.cloud.operator.util.TheiaCloudServiceUtil; import com.google.inject.Inject; +import io.sentry.ISpan; +import io.sentry.Sentry; +import io.sentry.SpanStatus; + import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.Container; import io.fabric8.kubernetes.api.model.PersistentVolumeClaimVolumeSource; @@ -63,235 +58,305 @@ import io.fabric8.kubernetes.api.model.Volume; import io.fabric8.kubernetes.api.model.VolumeMount; import io.fabric8.kubernetes.api.model.apps.Deployment; -import io.fabric8.kubernetes.api.model.networking.v1.HTTPIngressPath; -import io.fabric8.kubernetes.api.model.networking.v1.HTTPIngressRuleValue; -import io.fabric8.kubernetes.api.model.networking.v1.Ingress; -import io.fabric8.kubernetes.api.model.networking.v1.IngressBackend; -import io.fabric8.kubernetes.api.model.networking.v1.IngressRule; -import io.fabric8.kubernetes.api.model.networking.v1.IngressServiceBackend; -import io.fabric8.kubernetes.api.model.networking.v1.ServiceBackendPort; +import io.fabric8.kubernetes.api.model.gatewayapi.v1.HTTPRoute; import io.fabric8.kubernetes.client.KubernetesClientException; +/** + * A {@link SessionHandler} that creates resources on-demand (lazy start). + * + * Uses {@link K8sResourceFactory} for resource creation and + * {@link IngressManager} for ingress operations. + */ public class LazySessionHandler implements SessionHandler { private static final Logger LOGGER = LogManager.getLogger(LazySessionHandler.class); protected static final String USER_DATA = "user-data"; - @Inject - protected IngressPathProvider ingressPathProvider; @Inject protected TheiaCloudOperatorArguments arguments; + + @Inject + protected TheiaCloudClient client; + @Inject - protected BandwidthLimiter bandwidthLimiter; + protected K8sResourceFactory resourceFactory; + @Inject - protected DeploymentTemplateReplacements deploymentReplacements; + protected IngressManager ingressManager; @Inject - protected TheiaCloudClient client; + protected SidecarManager sidecarManager; @Override - public boolean sessionAdded(Session session, String correlationId) { + public boolean sessionAdded(Session session, String correlationId, ISpan parentSpan) { + ISpan span = Tracing.childSpan(parentSpan, "lazy.setup", "Lazy session setup"); try { - return doSessionAdded(session, correlationId); + return doSessionAdded(session, correlationId, span); } catch (Throwable ex) { LOGGER.error(formatLogMessage(correlationId, "An unexpected exception occurred while adding Session: " + session), ex); - client.sessions().updateStatus(correlationId, session, status -> { - status.setOperatorStatus(OperatorStatus.ERROR); - status.setOperatorMessage( - "Unexpected error. Please check the logs for correlationId: " + correlationId); - }); + Tracing.finishError(span, ex); + SessionStatusUtil.markError(client, session, correlationId, + "Unexpected error. Please check the logs for correlationId: " + correlationId); return false; } } - protected boolean doSessionAdded(Session session, String correlationId) { - /* session information */ + protected boolean doSessionAdded(Session session, String correlationId, ISpan span) { + // Session information String sessionResourceName = session.getMetadata().getName(); String sessionResourceUID = session.getMetadata().getUid(); + // Use provided span + span.setTag("session.name", sessionResourceName); + span.setTag("session.strategy", "lazy"); + span.setData("correlation_id", correlationId); + // Check current session status and ignore if handling failed or finished before - Optional status = Optional.ofNullable(session.getStatus()); - String operatorStatus = status.map(ResourceStatus::getOperatorStatus).orElse(OperatorStatus.NEW); - if (OperatorStatus.HANDLED.equals(operatorStatus)) { - LOGGER.trace(formatLogMessage(correlationId, - "Session was successfully handled before and is skipped now. Session: " + session)); + SessionStatusUtil.PreHandleResult preHandle = SessionStatusUtil.evaluateStatus(session, client, correlationId, + LOGGER); + if (preHandle == SessionStatusUtil.PreHandleResult.ALREADY_HANDLED) { + span.setTag("outcome", "already_handled"); + Tracing.finish(span, SpanStatus.OK); return true; } - if (OperatorStatus.HANDLING.equals(operatorStatus)) { - // TODO We should not return but continue where we left off. - LOGGER.warn(formatLogMessage(correlationId, - "Session handling was unexpectedly interrupted before. Session is skipped now and its status is set to ERROR. Session: " - + session)); - client.sessions().updateStatus(correlationId, session, s -> { - s.setOperatorStatus(OperatorStatus.ERROR); - s.setOperatorMessage("Handling was unexpectedly interrupted before. CorrelationId: " + correlationId); - }); + if (preHandle == SessionStatusUtil.PreHandleResult.INTERRUPTED) { + span.setTag("outcome", "interrupted"); + Tracing.finish(span, SpanStatus.ABORTED); return false; } - if (OperatorStatus.ERROR.equals(operatorStatus)) { - LOGGER.warn(formatLogMessage(correlationId, - "Session could not be handled before and is skipped now. Session: " + session)); + if (preHandle == SessionStatusUtil.PreHandleResult.PREVIOUS_ERROR) { + span.setTag("outcome", "previous_error"); + Tracing.finish(span, SpanStatus.ABORTED); return false; } - // Set session status to being handled - client.sessions().updateStatus(correlationId, session, s -> { - s.setOperatorStatus(OperatorStatus.HANDLING); - }); + // Set status to handling + SessionStatusUtil.markHandling(client, session, correlationId); SessionSpec sessionSpec = session.getSpec(); - /* find app definition for session */ + // Find app definition + ISpan appDefSpan = Tracing.childSpan(span, "lazy.find_appdef", "Find app definition"); String appDefinitionID = sessionSpec.getAppDefinition(); - Optional optionalAppDefinition = client.appDefinitions().get(appDefinitionID); - if (optionalAppDefinition.isEmpty()) { - LOGGER.error(formatLogMessage(correlationId, "No App Definition with name " + appDefinitionID + " found.")); - client.sessions().updateStatus(correlationId, session, s -> { - s.setOperatorStatus(OperatorStatus.ERROR); - s.setOperatorMessage("App Definition not found."); - }); + span.setTag("app_definition", appDefinitionID); + Optional appDefOpt = client.appDefinitions().get(appDefinitionID); + if (appDefOpt.isEmpty()) { + LOGGER.error(formatLogMessage(correlationId, + "No App Definition with name " + appDefinitionID + " found.")); + SessionStatusUtil.markError(client, session, correlationId, "App Definition not found."); + appDefSpan.setTag("outcome", "not_found"); + Tracing.finish(appDefSpan, SpanStatus.NOT_FOUND); + span.setTag("outcome", "appdef_not_found"); + Tracing.finish(span, SpanStatus.NOT_FOUND); return false; } - AppDefinition appDefinition = optionalAppDefinition.get(); - - if (hasMaxInstancesReached(appDefinition, session, correlationId)) { - client.sessions().updateStatus(correlationId, session, s -> { - s.setOperatorStatus(OperatorStatus.ERROR); - s.setOperatorMessage("Max instances reached."); - }); + AppDefinition appDef = appDefOpt.get(); + AppDefinitionSpec appDefSpec = appDef.getSpec(); + Tracing.finishSuccess(appDefSpan); + + // Create labels + Map labels = LabelsUtil.createSessionLabels(session, appDef); + + // Check limits + ISpan limitsSpan = Tracing.childSpan(span, "lazy.check_limits", "Check instance and session limits"); + if (hasMaxInstancesReached(appDef, session, correlationId)) { + SessionStatusUtil.markError(client, session, correlationId, "Max instances reached."); + limitsSpan.setTag("outcome", "max_instances"); + Tracing.finish(limitsSpan, SpanStatus.RESOURCE_EXHAUSTED); + span.setTag("outcome", "max_instances_reached"); + Tracing.finish(span, SpanStatus.RESOURCE_EXHAUSTED); return false; } - if (hasMaxSessionsReached(session, correlationId)) { - client.sessions().updateStatus(correlationId, session, s -> { - s.setOperatorStatus(OperatorStatus.ERROR); - s.setOperatorMessage("Max sessions reached."); - }); + SessionStatusUtil.markError(client, session, correlationId, "Max sessions reached."); + limitsSpan.setTag("outcome", "max_sessions"); + Tracing.finish(limitsSpan, SpanStatus.RESOURCE_EXHAUSTED); + span.setTag("outcome", "max_sessions_reached"); + Tracing.finish(span, SpanStatus.RESOURCE_EXHAUSTED); return false; } - - Optional ingress = getIngress(appDefinition, correlationId); - if (ingress.isEmpty()) { - client.sessions().updateStatus(correlationId, session, s -> { - s.setOperatorStatus(OperatorStatus.ERROR); - s.setOperatorMessage("Ingress not available."); - }); + Tracing.finishSuccess(limitsSpan); + + // Get shared HTTPRoute template + ISpan ingressSpan = Tracing.childSpan(span, "lazy.get_route", "Get HTTPRoute for app definition"); + Optional routeOpt = ingressManager.getIngress(appDef, correlationId); + if (routeOpt.isEmpty()) { + SessionStatusUtil.markError(client, session, correlationId, "HTTPRoute not available."); + ingressSpan.setTag("outcome", "not_found"); + Tracing.finish(ingressSpan, SpanStatus.NOT_FOUND); + span.setTag("outcome", "route_not_found"); + Tracing.finish(span, SpanStatus.NOT_FOUND); return false; } + Tracing.finishSuccess(ingressSpan); syncSessionDataToWorkspace(session, correlationId); - /* Create service for this session */ - List existingServices = K8sUtil.getExistingServices(client.kubernetes(), client.namespace(), - sessionResourceName, sessionResourceUID); + // Check for existing service (idempotency) + List existingServices = K8sUtil.getExistingServices( + client.kubernetes(), client.namespace(), sessionResourceName, sessionResourceUID); if (!existingServices.isEmpty()) { - LOGGER.warn(formatLogMessage(correlationId, - "Existing service for " + sessionSpec + ". Session already running?")); - client.sessions().updateStatus(correlationId, session, s -> { - s.setOperatorStatus(OperatorStatus.HANDLED); - s.setOperatorMessage("Service already exists."); - }); - // TODO do not return true if the sessions was in handling state at the start of - // this handler + LOGGER.warn(formatLogMessage(correlationId, "Service already exists for session.")); + SessionStatusUtil.markHandled(client, session, correlationId, "Service already exists."); + span.setTag("outcome", "idempotent_service_exists"); + Tracing.finish(span, SpanStatus.OK); return true; } - Optional serviceToUse = createAndApplyService(correlationId, sessionResourceName, sessionResourceUID, - session, appDefinition.getSpec(), arguments.isUseKeycloak()); - if (serviceToUse.isEmpty()) { - LOGGER.error(formatLogMessage(correlationId, "Unable to create service for session " + sessionSpec)); - client.sessions().updateStatus(correlationId, session, s -> { - s.setOperatorStatus(OperatorStatus.ERROR); - s.setOperatorMessage("Failed to create service."); - }); + // Create service + ISpan serviceSpan = Tracing.childSpan(span, "lazy.create_service", "Create service"); + Optional serviceOpt = resourceFactory.createServiceForLazySession( + session, appDef, labels, correlationId); + if (serviceOpt.isEmpty()) { + LOGGER.error(formatLogMessage(correlationId, "Unable to create service for session.")); + SessionStatusUtil.markError(client, session, correlationId, "Failed to create service."); + serviceSpan.setTag("outcome", "failed"); + Tracing.finish(serviceSpan, SpanStatus.INTERNAL_ERROR); + span.setTag("outcome", "service_creation_failed"); + Tracing.finish(span, SpanStatus.INTERNAL_ERROR); + return false; + } + Tracing.finishSuccess(serviceSpan); + + // Create internal service + ISpan internalServiceSpan = Tracing.childSpan(span, "lazy.create_internal_service", "Create internal service"); + Optional internalServiceOpt = resourceFactory.createInternalServiceForLazySession( + session, appDef, labels, correlationId); + if (internalServiceOpt.isEmpty()) { + LOGGER.error(formatLogMessage(correlationId, "Unable to create internal service.")); + SessionStatusUtil.markError(client, session, correlationId, "Failed to create internal service."); + internalServiceSpan.setTag("outcome", "failed"); + Tracing.finish(internalServiceSpan, SpanStatus.INTERNAL_ERROR); + span.setTag("outcome", "internal_service_failed"); + Tracing.finish(span, SpanStatus.INTERNAL_ERROR); return false; } + Tracing.finishSuccess(internalServiceSpan); + // Create configmaps (if using Keycloak) if (arguments.isUseKeycloak()) { - /* Create config maps for this session */ - List existingConfigMaps = K8sUtil.getExistingConfigMaps(client.kubernetes(), client.namespace(), - sessionResourceName, sessionResourceUID); + ISpan configMapSpan = Tracing.childSpan(span, "lazy.create_configmaps", "Create OAuth2 configmaps"); + List existingConfigMaps = K8sUtil.getExistingConfigMaps( + client.kubernetes(), client.namespace(), sessionResourceName, sessionResourceUID); if (!existingConfigMaps.isEmpty()) { - LOGGER.warn(formatLogMessage(correlationId, - "Existing configmaps for " + sessionSpec + ". Session already running?")); - client.sessions().updateStatus(correlationId, session, s -> { - s.setOperatorStatus(OperatorStatus.HANDLED); - s.setOperatorMessage("Configmaps already exist."); - }); - // TODO do not return true if the sessions was in handling state at the start of - // this handler + LOGGER.warn(formatLogMessage(correlationId, "ConfigMaps already exist for session.")); + SessionStatusUtil.markHandled(client, session, correlationId, "ConfigMaps already exist."); + configMapSpan.setTag("outcome", "already_exists"); + Tracing.finish(configMapSpan, SpanStatus.OK); + span.setTag("outcome", "idempotent_configmaps_exist"); + Tracing.finish(span, SpanStatus.OK); return true; } - createAndApplyEmailConfigMap(correlationId, sessionResourceName, sessionResourceUID, session); - createAndApplyProxyConfigMap(correlationId, sessionResourceName, sessionResourceUID, session, - appDefinition); + resourceFactory.createEmailConfigMapForLazySession(session, labels, correlationId); + resourceFactory.createProxyConfigMapForLazySession(session, appDef, labels, correlationId); + Tracing.finishSuccess(configMapSpan); } - /* Create deployment for this session */ - List existingDeployments = K8sUtil.getExistingDeployments(client.kubernetes(), client.namespace(), - sessionResourceName, sessionResourceUID); + // Check for existing deployment (idempotency) + List existingDeployments = K8sUtil.getExistingDeployments( + client.kubernetes(), client.namespace(), sessionResourceName, sessionResourceUID); if (!existingDeployments.isEmpty()) { - LOGGER.warn(formatLogMessage(correlationId, - "Existing deployments for " + sessionSpec + ". Session already running?")); - client.sessions().updateStatus(correlationId, session, s -> { - s.setOperatorStatus(OperatorStatus.HANDLED); - s.setOperatorMessage("Deployment already exists."); - }); + LOGGER.warn(formatLogMessage(correlationId, "Deployment already exists for session.")); + SessionStatusUtil.markHandled(client, session, correlationId, "Deployment already exists."); + span.setTag("outcome", "idempotent_deployment_exists"); + Tracing.finish(span, SpanStatus.OK); return true; } + // Create deployment + ISpan deploymentSpan = Tracing.childSpan(span, "lazy.create_deployment", "Create deployment"); Optional storageName = getStorageName(session, correlationId); - createAndApplyDeployment(correlationId, sessionResourceName, sessionResourceUID, session, appDefinition, - storageName, arguments.isUseKeycloak()); + deploymentSpan.setData("has_storage", storageName.isPresent()); + resourceFactory.createDeploymentForLazySession( + session, appDef, storageName, labels, + deployment -> { + storageName.ifPresent(name -> addVolumeClaim(deployment, name, appDefSpec)); + sidecarManager.injectSidecarEnvVars(session, deployment, appDef, correlationId); + }, + correlationId); + Tracing.finishSuccess(deploymentSpan); - /* adjust the ingress */ + // Sidecar setup is best-effort: the session remains usable even if sidecar creation fails. + if (!sidecarManager.createSidecars(session, appDef, storageName, correlationId)) { + LOGGER.warn(formatLogMessage(correlationId, + "Sidecar creation failed for session " + session.getMetadata().getName() + + "; session will continue without sidecar support.")); + } + + // Create the dedicated session HTTPRoute. + ISpan ingressRuleSpan = Tracing.childSpan(span, "lazy.add_route_rule", "Add HTTPRoute rule"); String host; try { - host = updateIngress(ingress, serviceToUse, session, appDefinition, correlationId); + host = ingressManager.addRuleForSession( + routeOpt.get(), serviceOpt.get(), appDef, session, correlationId); + ingressRuleSpan.setData("host", host); + Tracing.finishSuccess(ingressRuleSpan); } catch (KubernetesClientException e) { - LOGGER.error(formatLogMessage(correlationId, - "Error while editing ingress " + ingress.get().getMetadata().getName()), e); - client.sessions().updateStatus(correlationId, session, s -> { - s.setOperatorStatus(OperatorStatus.ERROR); - s.setOperatorMessage("Failed to edit ingress"); - }); + LOGGER.error(formatLogMessage(correlationId, "Error while editing HTTPRoute"), e); + SessionStatusUtil.markError(client, session, correlationId, "Failed to edit HTTPRoute."); + Tracing.finishError(ingressRuleSpan, e); + span.setTag("outcome", "route_rule_failed"); + Tracing.finish(span, SpanStatus.INTERNAL_ERROR); return false; } - /* Update session resource */ + Sentry.addBreadcrumb("Scheduling URL availability check for " + host, "session"); + AddedHandlerUtil.updateSessionURLAsync(client.sessions(), session, client.namespace(), host, correlationId, + span); + + SessionStatusUtil.markHandled(client, session, correlationId, null); + span.setTag("outcome", "success"); + Tracing.finishSuccess(span); + return true; + } + + @Override + public synchronized boolean sessionDeleted(Session session, String correlationId, ISpan parentSpan) { + ISpan span = Tracing.childSpan(parentSpan, "lazy.cleanup", "Lazy session cleanup"); try { - AddedHandlerUtil.updateSessionURLAsync(client.sessions(), session, client.namespace(), host, correlationId); - } catch (KubernetesClientException e) { - LOGGER.error( - formatLogMessage(correlationId, "Error while editing session " + session.getMetadata().getName()), - e); - client.sessions().updateStatus(correlationId, session, s -> { - s.setOperatorStatus(OperatorStatus.ERROR); - s.setOperatorMessage("Failed to set session URL."); - }); - return false; + boolean success = doSessionDeleted(session, correlationId, span); + if (success) { + Tracing.finishSuccess(span); + } + return success; + } catch (Exception e) { + Tracing.finishError(span, e); + throw e; } + } - client.sessions().updateStatus(correlationId, session, s -> { - s.setOperatorStatus(OperatorStatus.HANDLED); - }); + protected boolean doSessionDeleted(Session session, String correlationId, ISpan span) { + SessionSpec sessionSpec = session.getSpec(); + String sessionName = session.getMetadata().getName(); + String appDefinitionID = sessionSpec.getAppDefinition(); + + // Use provided span + span.setTag("session.name", sessionName); + span.setTag("session.strategy", "lazy"); + span.setTag("app_definition", appDefinitionID); + span.setData("correlation_id", correlationId); + + sidecarManager.deleteSidecars(session, correlationId); + + span.setTag("outcome", "success"); return true; } + // ========== Helper Methods ========== + protected void syncSessionDataToWorkspace(Session session, String correlationId) { if (!session.getSpec().isEphemeral() && session.getSpec().hasAppDefinition()) { - // update last used workspace client.workspaces().edit(correlationId, session.getSpec().getWorkspace(), workspace -> { workspace.getSpec().setAppDefinition(session.getSpec().getAppDefinition()); }); } } - protected boolean hasMaxInstancesReached(AppDefinition appDefinition, Session session, String correlationId) { - if (TheiaCloudK8sUtil.checkIfMaxInstancesReached(client.kubernetes(), client.namespace(), session.getSpec(), - appDefinition.getSpec(), correlationId)) { - LOGGER.info(formatMetric(correlationId, "Max instances reached for " + appDefinition.getSpec().getName())); + protected boolean hasMaxInstancesReached(AppDefinition appDef, Session session, String correlationId) { + if (TheiaCloudK8sUtil.checkIfMaxInstancesReached( + client.kubernetes(), client.namespace(), session.getSpec(), appDef.getSpec(), correlationId)) { + LOGGER.info(formatMetric(correlationId, "Max instances reached for " + appDef.getSpec().getName())); client.sessions().updateStatus(correlationId, session, status -> { status.setError(TheiaCloudError.SESSION_SERVER_LIMIT_REACHED); }); @@ -301,40 +366,26 @@ protected boolean hasMaxInstancesReached(AppDefinition appDefinition, Session se } protected boolean hasMaxSessionsReached(Session session, String correlationId) { - /* check if max sessions reached already */ - if (arguments.getSessionsPerUser() != null && arguments.getSessionsPerUser() >= 0) { - if (arguments.getSessionsPerUser() == 0) { - LOGGER.info(formatLogMessage(correlationId, - "No sessions allowed for this user. Could not create session " + session.getSpec())); - client.sessions().updateStatus(correlationId, session, status -> { - status.setError(TheiaCloudError.SESSION_USER_NO_SESSIONS); - }); - return true; - } - - long userSessions = client.sessions().list(session.getSpec().getUser()).size(); - if (userSessions > arguments.getSessionsPerUser()) { - LOGGER.info(formatLogMessage(correlationId, - "No more sessions allowed for this user, limit is " + arguments.getSessionsPerUser())); - client.sessions().updateStatus(correlationId, session, status -> { - status.setError(TheiaCloudError.SESSION_USER_LIMIT_REACHED); - }); - return true; - } + if (arguments.getSessionsPerUser() == null || arguments.getSessionsPerUser() < 0) { + return false; } - return false; - } - - protected Optional getIngress(AppDefinition appDefinition, String correlationId) { - String appDefinitionResourceName = appDefinition.getMetadata().getName(); - String appDefinitionResourceUID = appDefinition.getMetadata().getUid(); - Optional ingress = K8sUtil.getExistingIngress(client.kubernetes(), client.namespace(), - appDefinitionResourceName, appDefinitionResourceUID); - if (ingress.isEmpty()) { - LOGGER.error(formatLogMessage(correlationId, - "No Ingress for app definition " + appDefinition.getSpec().getName() + " found.")); + if (arguments.getSessionsPerUser() == 0) { + LOGGER.info(formatLogMessage(correlationId, "No sessions allowed for this user.")); + client.sessions().updateStatus(correlationId, session, status -> { + status.setError(TheiaCloudError.SESSION_USER_NO_SESSIONS); + }); + return true; } - return ingress; + long userSessions = client.sessions().list(session.getSpec().getUser()).size(); + if (userSessions > arguments.getSessionsPerUser()) { + LOGGER.info(formatLogMessage(correlationId, + "Session limit reached for user: " + arguments.getSessionsPerUser())); + client.sessions().updateStatus(correlationId, session, status -> { + status.setError(TheiaCloudError.SESSION_USER_LIMIT_REACHED); + }); + return true; + } + return false; } protected Optional getStorageName(Session session, String correlationId) { @@ -342,210 +393,42 @@ protected Optional getStorageName(Session session, String correlationId) return Optional.empty(); } Optional workspace = client.workspaces().get(session.getSpec().getWorkspace()); - if (!workspace.isPresent()) { - LOGGER.info(formatLogMessage(correlationId, "No workspace with name " + session.getSpec().getWorkspace() - + " found for session " + session.getSpec().getName(), correlationId)); + if (workspace.isEmpty()) { + LOGGER.info(formatLogMessage(correlationId, + "No workspace with name " + session.getSpec().getWorkspace() + " found for session " + + session.getSpec().getName())); return Optional.empty(); - } if (!session.getSpec().getUser().equals(workspace.get().getSpec().getUser())) { // the workspace is owned by a different user. do not mount and go ephemeral // should get prevented by service, but we need to be sure to not expose data - LOGGER.error(formatLogMessage(correlationId, - "Workspace is owned by " + workspace.get().getSpec().getUser() + ", but requesting user is " - + session.getSpec().getUser())); + LOGGER.error(formatLogMessage(correlationId, "Workspace is owned by " + workspace.get().getSpec().getUser() + + ", but requesting user is " + session.getSpec().getUser())); return Optional.empty(); } - String storageName = WorkspaceUtil.getStorageName(workspace.get()); if (!client.persistentVolumeClaimsClient().has(storageName)) { - LOGGER.info(formatLogMessage(correlationId, - "No storage found for started session, will use ephemeral storage instead", correlationId)); + LOGGER.info(formatLogMessage(correlationId, "No storage found. Using ephemeral storage.")); return Optional.empty(); } return Optional.of(storageName); } - protected Optional createAndApplyService(String correlationId, String sessionResourceName, - String sessionResourceUID, Session session, AppDefinitionSpec appDefinitionSpec, boolean useOAuth2Proxy) { - Map replacements = TheiaCloudServiceUtil.getServiceReplacements(client.namespace(), session, - appDefinitionSpec); - String templateYaml = useOAuth2Proxy ? AddedHandlerUtil.TEMPLATE_SERVICE_YAML - : AddedHandlerUtil.TEMPLATE_SERVICE_WITHOUT_AOUTH2_PROXY_YAML; - String serviceYaml; - try { - serviceYaml = JavaResourceUtil.readResourceAndReplacePlaceholders(templateYaml, replacements, - correlationId); - } catch (IOException | URISyntaxException e) { - LOGGER.error(formatLogMessage(correlationId, "Error while adjusting template for session " + session), e); - return Optional.empty(); - } - return K8sUtil.loadAndCreateServiceWithOwnerReference(client.kubernetes(), client.namespace(), correlationId, - serviceYaml, Session.API, Session.KIND, sessionResourceName, sessionResourceUID, 0); - } - - protected void createAndApplyEmailConfigMap(String correlationId, String sessionResourceName, - String sessionResourceUID, Session session) { - Map replacements = TheiaCloudConfigMapUtil.getEmailConfigMapReplacements(client.namespace(), - session); - String configMapYaml; - try { - configMapYaml = JavaResourceUtil.readResourceAndReplacePlaceholders( - AddedHandlerUtil.TEMPLATE_CONFIGMAP_EMAILS_YAML, replacements, correlationId); - } catch (IOException | URISyntaxException e) { - LOGGER.error(formatLogMessage(correlationId, "Error while adjusting template for session " + session), e); - return; - } - K8sUtil.loadAndCreateConfigMapWithOwnerReference(client.kubernetes(), client.namespace(), correlationId, - configMapYaml, Session.API, Session.KIND, sessionResourceName, sessionResourceUID, 0, configmap -> { - configmap.setData(Collections.singletonMap(AddedHandlerUtil.FILENAME_AUTHENTICATED_EMAILS_LIST, - session.getSpec().getUser())); - }); - } - - protected void createAndApplyProxyConfigMap(String correlationId, String sessionResourceName, - String sessionResourceUID, Session session, AppDefinition appDefinition) { - Map replacements = TheiaCloudConfigMapUtil.getProxyConfigMapReplacements(client.namespace(), - session); - String configMapYaml; - try { - configMapYaml = JavaResourceUtil.readResourceAndReplacePlaceholders( - AddedHandlerUtil.TEMPLATE_CONFIGMAP_YAML, replacements, correlationId); - } catch (IOException | URISyntaxException e) { - LOGGER.error(formatLogMessage(correlationId, "Error while adjusting template for session " + session), e); - return; - } - K8sUtil.loadAndCreateConfigMapWithOwnerReference(client.kubernetes(), client.namespace(), correlationId, - configMapYaml, Session.API, Session.KIND, sessionResourceName, sessionResourceUID, 0, configMap -> { - String host = arguments.getInstancesHost() + ingressPathProvider.getPath(appDefinition, session); - int port = appDefinition.getSpec().getPort(); - AddedHandlerUtil.updateProxyConfigMap(client.kubernetes(), client.namespace(), configMap, host, - port); - }); - } - - protected void createAndApplyDeployment(String correlationId, String sessionResourceName, String sessionResourceUID, - Session session, AppDefinition appDefinition, Optional pvName, boolean useOAuth2Proxy) { - Map replacements = deploymentReplacements.getReplacements(client.namespace(), appDefinition, - session); - String templateYaml = useOAuth2Proxy ? AddedHandlerUtil.TEMPLATE_DEPLOYMENT_YAML - : AddedHandlerUtil.TEMPLATE_DEPLOYMENT_WITHOUT_AOUTH2_PROXY_YAML; - String deploymentYaml; - try { - deploymentYaml = JavaResourceUtil.readResourceAndReplacePlaceholders(templateYaml, replacements, - correlationId); - } catch (IOException | URISyntaxException e) { - LOGGER.error(formatLogMessage(correlationId, "Error while adjusting template for session " + session), e); - return; - } - K8sUtil.loadAndCreateDeploymentWithOwnerReference(client.kubernetes(), client.namespace(), correlationId, - deploymentYaml, Session.API, Session.KIND, sessionResourceName, sessionResourceUID, 0, deployment -> { - pvName.ifPresent(name -> addVolumeClaim(deployment, name, appDefinition.getSpec())); - bandwidthLimiter.limit(deployment, appDefinition.getSpec().getDownlinkLimit(), - appDefinition.getSpec().getUplinkLimit(), correlationId); - AddedHandlerUtil.removeEmptyResources(deployment); - - AddedHandlerUtil.addCustomEnvVarsToDeploymentFromSession(correlationId, deployment, session, - appDefinition); - - if (appDefinition.getSpec().getPullSecret() != null - && !appDefinition.getSpec().getPullSecret().isEmpty()) { - AddedHandlerUtil.addImagePullSecret(deployment, appDefinition.getSpec().getPullSecret()); - } - }); - } - - protected void addVolumeClaim(Deployment deployment, String pvcName, AppDefinitionSpec appDefinition) { + protected void addVolumeClaim(Deployment deployment, String pvcName, AppDefinitionSpec appDef) { PodSpec podSpec = deployment.getSpec().getTemplate().getSpec(); Volume volume = new Volume(); podSpec.getVolumes().add(volume); volume.setName(USER_DATA); - PersistentVolumeClaimVolumeSource persistentVolumeClaim = new PersistentVolumeClaimVolumeSource(); - volume.setPersistentVolumeClaim(persistentVolumeClaim); - persistentVolumeClaim.setClaimName(pvcName); + PersistentVolumeClaimVolumeSource pvc = new PersistentVolumeClaimVolumeSource(); + volume.setPersistentVolumeClaim(pvc); + pvc.setClaimName(pvcName); - Container theiaContainer = TheiaCloudPersistentVolumeUtil.getTheiaContainer(podSpec, appDefinition); + Container theiaContainer = TheiaCloudPersistentVolumeUtil.getTheiaContainer(podSpec, appDef); VolumeMount volumeMount = new VolumeMount(); theiaContainer.getVolumeMounts().add(volumeMount); volumeMount.setName(USER_DATA); - volumeMount.setMountPath(TheiaCloudPersistentVolumeUtil.getMountPath(appDefinition)); - } - - protected synchronized String updateIngress(Optional ingress, Optional serviceToUse, - Session session, AppDefinition appDefinition, String correlationId) { - List hostsToAdd = new ArrayList<>(); - final String instancesHost = arguments.getInstancesHost(); - hostsToAdd.add(instancesHost); - List ingressHostnamePrefixes = appDefinition.getSpec().getIngressHostnamePrefixes() != null - ? appDefinition.getSpec().getIngressHostnamePrefixes() - : Collections.emptyList(); - for (String prefix : ingressHostnamePrefixes) { - hostsToAdd.add(prefix + instancesHost); - } - String path = ingressPathProvider.getPath(appDefinition, session); - client.ingresses().edit(correlationId, ingress.get().getMetadata().getName(), ingressToUpdate -> { - for (String host : hostsToAdd) { - IngressRule ingressRule = new IngressRule(); - ingressToUpdate.getSpec().getRules().add(ingressRule); - - ingressRule.setHost(host); - - HTTPIngressRuleValue http = new HTTPIngressRuleValue(); - ingressRule.setHttp(http); - - HTTPIngressPath httpIngressPath = new HTTPIngressPath(); - http.getPaths().add(httpIngressPath); - httpIngressPath.setPath(path + AddedHandlerUtil.INGRESS_REWRITE_PATH); - httpIngressPath.setPathType("Prefix"); - - IngressBackend ingressBackend = new IngressBackend(); - httpIngressPath.setBackend(ingressBackend); - - IngressServiceBackend ingressServiceBackend = new IngressServiceBackend(); - ingressBackend.setService(ingressServiceBackend); - ingressServiceBackend.setName(serviceToUse.get().getMetadata().getName()); - - ServiceBackendPort serviceBackendPort = new ServiceBackendPort(); - ingressServiceBackend.setPort(serviceBackendPort); - serviceBackendPort.setNumber(appDefinition.getSpec().getPort()); - } - - }); - return instancesHost + path + "/"; - } - - @Override - public boolean sessionDeleted(Session session, String correlationId) { - /* session information */ - SessionSpec sessionSpec = session.getSpec(); - - /* find appDefinition for session */ - String appDefinitionID = sessionSpec.getAppDefinition(); - - Optional optionalAppDefinition = client.appDefinitions().get(appDefinitionID); - if (optionalAppDefinition.isEmpty()) { - LOGGER.error(formatLogMessage(correlationId, "No App Definition with name " + appDefinitionID + " found.")); - return false; - } - - /* find ingress */ - String appDefinitionResourceName = optionalAppDefinition.get().getMetadata().getName(); - String appDefinitionResourceUID = optionalAppDefinition.get().getMetadata().getUid(); - Optional ingress = K8sUtil.getExistingIngress(client.kubernetes(), client.namespace(), - appDefinitionResourceName, appDefinitionResourceUID); - if (ingress.isEmpty()) { - LOGGER.error( - formatLogMessage(correlationId, "No Ingress for app definition " + appDefinitionID + " found.")); - return false; - } - - String path = ingressPathProvider.getPath(optionalAppDefinition.get(), session); - TheiaCloudIngressUtil.removeIngressRule(client.kubernetes(), client.namespace(), ingress.get(), path, - correlationId); - - return true; + volumeMount.setMountPath(TheiaCloudPersistentVolumeUtil.getMountPath(appDef)); } - } diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/session/README.md b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/session/README.md new file mode 100644 index 000000000..99344c5f8 --- /dev/null +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/session/README.md @@ -0,0 +1,31 @@ +# Session Handling + +## Approaches + +There are currently two approaches to session handling, represented by the `SessionHandler` interface. + +### Lazy Session Handler (default) + +Creates sessions on-demand, i.e. when a session is requested, the operator will create the necessary resources to run the session. + +The session is created with all environment variables and options set. + +### Eager Session Handler + +Creates generic sessions ahead of time, according to the `AppDefinition` spec. + +The session is created with all environment variables known ahead of time, like the data bridge port. + +#### Data Bridge component + +As the environment can't be personalized for prewarmed sessions, we need a way of injecting configuration at runtime. + +This is handled by the data bridge component, a VSCode extension exposing HTTP endpoints for injection data. + +#### Environment Configuration for prewarmed sessionsw + +Prewarmed sessions need to be configured in advance to allow successful data injection later. + +- `SCORPIO_THEIA_ENV_STRATEGY`: Sets the strategy to use for environment configuration. Must be set to `data-bridge` for prewarmed sessions. +- `DATA_BRIDGE_PORT` (optional): Overrides the default port the data bridge component should listen on. +- `DATA_BRIDGE_ENABLED`: Must be set to `1` or `true` to enable the data bridge component in prewarmed sessions. diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/session/SessionHandler.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/session/SessionHandler.java index 3f61d1896..9107dfeb6 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/session/SessionHandler.java +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/session/SessionHandler.java @@ -17,24 +17,26 @@ import org.eclipse.theia.cloud.common.k8s.resource.session.Session; +import io.sentry.ISpan; + public interface SessionHandler { - default boolean sessionAdded(Session session, String correlationId) { + default boolean sessionAdded(Session session, String correlationId, ISpan span) { return true; } - default boolean sessionDeleted(Session session, String correlationId) { + default boolean sessionDeleted(Session session, String correlationId, ISpan span) { return true; } - default boolean sessionModified(Session session, String correlationId) { + default boolean sessionModified(Session session, String correlationId, ISpan span) { return true; } - default boolean sessionErrored(Session session, String correlationId) { + default boolean sessionErrored(Session session, String correlationId, ISpan span) { return true; } - default boolean sessionBookmarked(Session session, String correlationId) { + default boolean sessionBookmarked(Session session, String correlationId, ISpan span) { return true; } } diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/session/SessionStatusUtil.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/session/SessionStatusUtil.java new file mode 100644 index 000000000..466930dc4 --- /dev/null +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/session/SessionStatusUtil.java @@ -0,0 +1,117 @@ +package org.eclipse.theia.cloud.operator.handler.session; + +import java.time.Instant; +import java.util.Optional; + +import org.apache.logging.log4j.Logger; +import org.eclipse.theia.cloud.common.k8s.client.TheiaCloudClient; +import org.eclipse.theia.cloud.common.k8s.resource.OperatorStatus; +import org.eclipse.theia.cloud.common.k8s.resource.ResourceStatus; +import org.eclipse.theia.cloud.common.k8s.resource.session.Session; +import org.eclipse.theia.cloud.common.k8s.resource.session.SessionStatus; + +public final class SessionStatusUtil { + + public enum PreHandleResult { + PROCEED, ALREADY_HANDLED, INTERRUPTED, PREVIOUS_ERROR + } + + private SessionStatusUtil() { + } + + /** + * Evaluates the session status and returns the appropriate pre-handle result. + * + * @param session the session to evaluate + * @param client the Kubernetes client + * @param correlationId the correlation ID for tracking + * @param logger the logger instance + * @return the pre-handle result indicating how to proceed + */ + public static PreHandleResult evaluateStatus(Session session, TheiaCloudClient client, String correlationId, + Logger logger) { + if (session == null) { + throw new IllegalArgumentException("Session must not be null"); + } + if (client == null) { + throw new IllegalArgumentException("Client must not be null"); + } + Optional status = Optional.ofNullable(session.getStatus()); + String operatorStatus = status.map(ResourceStatus::getOperatorStatus).orElse(OperatorStatus.NEW); + + if (OperatorStatus.HANDLED.equals(operatorStatus)) { + return PreHandleResult.ALREADY_HANDLED; + } + if (OperatorStatus.HANDLING.equals(operatorStatus)) { + markError(client, session, correlationId, + "Handling was unexpectedly interrupted. CorrelationId: " + correlationId); + return PreHandleResult.INTERRUPTED; + } + if (OperatorStatus.ERROR.equals(operatorStatus)) { + return PreHandleResult.PREVIOUS_ERROR; + } + return PreHandleResult.PROCEED; + } + + /** + * Marks the session as currently being handled. + * + * @param client the Kubernetes client + * @param session the session to mark + * @param correlationId the correlation ID for tracking + */ + public static void markHandling(TheiaCloudClient client, Session session, String correlationId) { + if (session == null) { + throw new IllegalArgumentException("Session must not be null"); + } + if (client == null) { + throw new IllegalArgumentException("Client must not be null"); + } + client.sessions().updateStatus(correlationId, session, s -> s.setOperatorStatus(OperatorStatus.HANDLING)); + } + + /** + * Marks the session as successfully handled. + * + * @param client the Kubernetes client + * @param session the session to mark + * @param correlationId the correlation ID for tracking + * @param message optional message to set on the session status + */ + public static void markHandled(TheiaCloudClient client, Session session, String correlationId, String message) { + if (session == null) { + throw new IllegalArgumentException("Session must not be null"); + } + if (client == null) { + throw new IllegalArgumentException("Client must not be null"); + } + client.sessions().updateStatus(correlationId, session, s -> { + s.setOperatorStatus(OperatorStatus.HANDLED); + if (message != null) { + s.setOperatorMessage(message); + } + s.setLastActivity(Instant.now().toEpochMilli()); + }); + } + + /** + * Marks the session with an error status. + * + * @param client the Kubernetes client + * @param session the session to mark + * @param correlationId the correlation ID for tracking + * @param message the error message to set + */ + public static void markError(TheiaCloudClient client, Session session, String correlationId, String message) { + if (session == null) { + throw new IllegalArgumentException("Session must not be null"); + } + if (client == null) { + throw new IllegalArgumentException("Client must not be null"); + } + client.sessions().updateStatus(correlationId, session, s -> { + s.setOperatorStatus(OperatorStatus.ERROR); + s.setOperatorMessage(message); + }); + } +} diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/ingress/IngressManager.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/ingress/IngressManager.java new file mode 100644 index 000000000..76a7e12d8 --- /dev/null +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/ingress/IngressManager.java @@ -0,0 +1,309 @@ +package org.eclipse.theia.cloud.operator.ingress; + +import static org.eclipse.theia.cloud.common.util.LogMessageUtil.formatLogMessage; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.theia.cloud.common.k8s.client.TheiaCloudClient; +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinition; +import org.eclipse.theia.cloud.common.k8s.resource.session.Session; +import org.eclipse.theia.cloud.common.util.LabelsUtil; +import org.eclipse.theia.cloud.operator.TheiaCloudOperatorArguments; +import org.eclipse.theia.cloud.operator.util.K8sUtil; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.api.model.OwnerReference; +import io.fabric8.kubernetes.api.model.OwnerReferenceBuilder; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.gatewayapi.v1.HTTPBackendRef; +import io.fabric8.kubernetes.api.model.gatewayapi.v1.HTTPBackendRefBuilder; +import io.fabric8.kubernetes.api.model.gatewayapi.v1.HTTPHeader; +import io.fabric8.kubernetes.api.model.gatewayapi.v1.HTTPHeaderBuilder; +import io.fabric8.kubernetes.api.model.gatewayapi.v1.HTTPHeaderFilterBuilder; +import io.fabric8.kubernetes.api.model.gatewayapi.v1.HTTPPathMatch; +import io.fabric8.kubernetes.api.model.gatewayapi.v1.HTTPPathMatchBuilder; +import io.fabric8.kubernetes.api.model.gatewayapi.v1.HTTPPathModifierBuilder; +import io.fabric8.kubernetes.api.model.gatewayapi.v1.HTTPRequestRedirectFilterBuilder; +import io.fabric8.kubernetes.api.model.gatewayapi.v1.HTTPRoute; +import io.fabric8.kubernetes.api.model.gatewayapi.v1.HTTPRouteBuilder; +import io.fabric8.kubernetes.api.model.gatewayapi.v1.HTTPRouteFilter; +import io.fabric8.kubernetes.api.model.gatewayapi.v1.HTTPRouteFilterBuilder; +import io.fabric8.kubernetes.api.model.gatewayapi.v1.HTTPRouteMatch; +import io.fabric8.kubernetes.api.model.gatewayapi.v1.HTTPRouteMatchBuilder; +import io.fabric8.kubernetes.api.model.gatewayapi.v1.HTTPRouteRule; +import io.fabric8.kubernetes.api.model.gatewayapi.v1.HTTPRouteRuleBuilder; +import io.fabric8.kubernetes.api.model.gatewayapi.v1.HTTPRouteSpec; +import io.fabric8.kubernetes.api.model.gatewayapi.v1.HTTPRouteSpecBuilder; +import io.fabric8.kubernetes.api.model.gatewayapi.v1.HTTPURLRewriteFilterBuilder; +import io.fabric8.kubernetes.client.KubernetesClientException; + +/** + * Centralized manager for HTTPRoute operations (Gateway API). + * + * Caller intent: + * - find the shared route template for an app definition + * - expose a session path via a dedicated session-owned HTTPRoute + */ +@Singleton +public class IngressManager { + + private static final Logger LOGGER = LogManager.getLogger(IngressManager.class); + /** + * Envoy Gateway runtime expression for the current request path. + * This value is interpreted by Envoy; other Gateway API implementations may + * treat it as a plain literal string. + */ + private static final String ENVOY_REQUEST_PATH_EXPRESSION = "%REQ(:PATH)%"; + private static final int HTTP_CONFLICT = 409; + private static final String SESSION_ROUTE_SUFFIX = "-route"; + + @Inject + private TheiaCloudClient client; + + @Inject + private TheiaCloudOperatorArguments arguments; + + @Inject + private IngressPathProvider pathProvider; + + /** + * Gets the shared app-level HTTPRoute for an app definition. + * + * This route is treated as static configuration managed outside the operator: + * it provides the parentRefs and hostnames that every session-specific + * HTTPRoute should inherit. + */ + public Optional getIngress(AppDefinition appDefinition, String correlationId) { + Optional route = K8sUtil.getExistingHttpRoute( + client.kubernetes(), + client.namespace(), + appDefinition.getMetadata().getName(), + appDefinition.getMetadata().getUid()); + if (route.isEmpty()) { + LOGGER.debug(formatLogMessage(correlationId, + "No HTTPRoute found for app definition " + appDefinition.getMetadata().getName())); + } + return route; + } + + /** + * Exposes an eager session through a dedicated session-owned HTTPRoute. + * + * @return the full URL for the session + */ + public String addRuleForSession( + HTTPRoute routeTemplate, + Service service, + AppDefinition appDefinition, + Session session, + int instance, + String correlationId) { + + String path = pathProvider.getPath(appDefinition, instance); + return createOrUpdateSessionRoute(routeTemplate, service, appDefinition, session, path, correlationId); + } + + /** + * Exposes a lazy session through a dedicated session-owned HTTPRoute. + * + * @return the full URL for the session + */ + public String addRuleForSession( + HTTPRoute routeTemplate, + Service service, + AppDefinition appDefinition, + Session session, + String correlationId) { + + String path = pathProvider.getPath(appDefinition, session); + return createOrUpdateSessionRoute(routeTemplate, service, appDefinition, session, path, correlationId); + } + + private String createOrUpdateSessionRoute( + HTTPRoute routeTemplate, + Service service, + AppDefinition appDefinition, + Session session, + String path, + String correlationId) { + + String routeName = buildSessionRouteName(session); + String serviceName = service.getMetadata().getName(); + int port = appDefinition.getSpec().getPort(); + HTTPRoute desiredRoute = buildSessionRoute(routeTemplate, serviceName, port, path, session, appDefinition); + + try { + client.httpRoutes().operation().resource(desiredRoute).create(); + } catch (KubernetesClientException e) { + if (e.getCode() != HTTP_CONFLICT) { + LOGGER.error(formatLogMessage(correlationId, + "Failed to create session HTTPRoute " + routeName + " for path " + path), e); + throw e; + } + + // Retries for the same Session should converge on one stable route name. + HTTPRoute updatedRoute = client.httpRoutes().edit(correlationId, routeName, existingRoute -> { + existingRoute.setSpec(desiredRoute.getSpec()); + existingRoute.getMetadata().setLabels(desiredRoute.getMetadata().getLabels()); + existingRoute.getMetadata().setOwnerReferences(desiredRoute.getMetadata().getOwnerReferences()); + }); + if (updatedRoute == null) { + throw new KubernetesClientException("HTTPRoute " + routeName + " not found for update"); + } + } + + LOGGER.info(formatLogMessage(correlationId, + "Configured session HTTPRoute " + routeName + " for path " + path + + " and backend service " + serviceName)); + return arguments.getInstancesHost() + ensureTrailingSlash(path); + } + + private HTTPRoute buildSessionRoute(HTTPRoute routeTemplate, String serviceName, int port, String path, + Session session, AppDefinition appDefinition) { + HTTPRouteSpec templateSpec = getOrCreateSpec(routeTemplate); + List hosts = buildRouteHosts(appDefinition, templateSpec.getHostnames()); + + return new HTTPRouteBuilder() + .withMetadata(new ObjectMetaBuilder() + // Use the Session UID so retries and reconciliations keep targeting the same route while + // staying well within Kubernetes object name limits. + .withName(buildSessionRouteName(session)) + .withNamespace(client.namespace()) + .withLabels(LabelsUtil.createSessionLabels(session, appDefinition)) + .withOwnerReferences(buildSessionOwnerReference(session)) + .build()) + .withSpec(new HTTPRouteSpecBuilder() + // Keep gateway attachment and hostname configuration in the shared app-level route so Helm + // remains the single source of truth for ingress topology. + .withParentRefs(templateSpec.getParentRefs()) + .withHostnames(hosts) + .withRules(createRedirectRule(path), createRouteRule(serviceName, port, path)) + .build()) + .build(); + } + + private List buildRouteHosts(AppDefinition appDefinition, List templateHostnames) { + if (templateHostnames != null && !templateHostnames.isEmpty()) { + return new ArrayList<>(templateHostnames); + } + + String instancesHost = arguments.getInstancesHost(); + List hosts = new ArrayList<>(); + hosts.add(instancesHost); + + List prefixes = appDefinition.getSpec().getIngressHostnamePrefixes(); + if (prefixes != null) { + for (String prefix : prefixes) { + hosts.add(prefix + instancesHost); + } + } + + return hosts; + } + + private OwnerReference buildSessionOwnerReference(Session session) { + return new OwnerReferenceBuilder() + .withApiVersion(Session.API) + .withKind(Session.KIND) + .withName(session.getMetadata().getName()) + .withUid(session.getMetadata().getUid()) + .build(); + } + + private String buildSessionRouteName(Session session) { + return session.getMetadata().getUid() + SESSION_ROUTE_SUFFIX; + } + + private HTTPRouteSpec getOrCreateSpec(HTTPRoute route) { + HTTPRouteSpec spec = route.getSpec(); + if (spec == null) { + spec = new HTTPRouteSpecBuilder().build(); + route.setSpec(spec); + } + return spec; + } + + /** + * Creates the backend routing rule for a session path. + * + * The X-Forwarded-Uri header uses an Envoy Gateway runtime expression and + * therefore requires Envoy Gateway for correct behavior. + */ + private HTTPRouteRule createRouteRule(String serviceName, int port, String path) { + HTTPRouteMatch pathPrefixMatch = new HTTPRouteMatchBuilder() + .withPath(new HTTPPathMatchBuilder() + .withType("PathPrefix") + .withValue(ensureTrailingSlash(path)) + .build()) + .build(); + + HTTPHeader forwardedUriHeader = new HTTPHeaderBuilder() + .withName("X-Forwarded-Uri") + .withValue(ENVOY_REQUEST_PATH_EXPRESSION) + .build(); + + HTTPRouteFilter requestHeaderFilter = new HTTPRouteFilterBuilder() + .withType("RequestHeaderModifier") + .withRequestHeaderModifier(new HTTPHeaderFilterBuilder() + .withSet(forwardedUriHeader) + .build()) + .build(); + + HTTPRouteFilter urlRewriteFilter = new HTTPRouteFilterBuilder() + .withType("URLRewrite") + .withUrlRewrite(new HTTPURLRewriteFilterBuilder() + .withPath(new HTTPPathModifierBuilder() + .withType("ReplacePrefixMatch") + .withReplacePrefixMatch("/") + .build()) + .build()) + .build(); + + HTTPBackendRef backendRef = new HTTPBackendRefBuilder() + .withName(serviceName) + .withPort(port) + .build(); + + return new HTTPRouteRuleBuilder() + .withMatches(pathPrefixMatch) + .withFilters(requestHeaderFilter, urlRewriteFilter) + .withBackendRefs(backendRef) + .build(); + } + + private HTTPRouteRule createRedirectRule(String path) { + HTTPRouteMatch exactMatch = new HTTPRouteMatchBuilder() + .withPath(new HTTPPathMatchBuilder() + .withType("Exact") + .withValue(path) + .build()) + .build(); + + HTTPRouteFilter redirectFilter = new HTTPRouteFilterBuilder() + .withType("RequestRedirect") + .withRequestRedirect(new HTTPRequestRedirectFilterBuilder() + .withStatusCode(302) + .withPath(new HTTPPathModifierBuilder() + .withType("ReplaceFullPath") + .withReplaceFullPath(ensureTrailingSlash(path)) + .build()) + .build()) + .build(); + + return new HTTPRouteRuleBuilder() + .withMatches(exactMatch) + .withFilters(redirectFilter) + .build(); + } + + private String ensureTrailingSlash(String value) { + return value.endsWith("/") ? value : value + "/"; + } +} diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/messaging/MonitorMessagingServiceImpl.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/messaging/MonitorMessagingServiceImpl.java index c05fa574d..f89573627 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/messaging/MonitorMessagingServiceImpl.java +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/messaging/MonitorMessagingServiceImpl.java @@ -22,7 +22,9 @@ import org.apache.logging.log4j.Logger; import org.eclipse.theia.cloud.common.k8s.client.TheiaCloudClient; import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinition; +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinitionSpec; import org.eclipse.theia.cloud.common.k8s.resource.session.Session; +import org.eclipse.theia.cloud.common.util.SessionUtil; import org.eclipse.theia.cloud.operator.TheiaCloudOperatorArguments; import org.json.JSONObject; @@ -33,6 +35,9 @@ import okhttp3.Request; import okhttp3.RequestBody; +import io.sentry.okhttp.SentryOkHttpEventListener; +import io.sentry.okhttp.SentryOkHttpInterceptor; + public class MonitorMessagingServiceImpl implements MonitorMessagingService { private static final String MONITOR_BASE_PATH = "/monitor"; @@ -46,6 +51,11 @@ public class MonitorMessagingServiceImpl implements MonitorMessagingService { @Inject private TheiaCloudOperatorArguments arguments; + private final OkHttpClient httpClient = new OkHttpClient.Builder() + .addInterceptor(new SentryOkHttpInterceptor()) + .eventListener(new SentryOkHttpEventListener()) + .build(); + @Override public void sendMessage(Session session, String level, String message) { if (isEnabled()) { @@ -70,20 +80,18 @@ public void sendTimeoutMessage(Session session, String detail) { } protected void postMessage(Session session, String level, String message, boolean fullscreen, String detail) { - OkHttpClient client = new OkHttpClient().newBuilder().build(); - Optional postMessageURL = getURL(session); if (postMessageURL.isPresent()) { MediaType mediaType = MediaType.parse("application/json; charset=utf-8"); String json = new JSONObject().put("message", message).put("level", level).put("fullscreen", fullscreen) .put("detail", detail).toString(); - RequestBody body = RequestBody.create(mediaType, json); + RequestBody body = RequestBody.create(json, mediaType); Request postRequest = new Request.Builder().url(postMessageURL.get()) .addHeader("Authorization", "Bearer " + session.getSpec().getSessionSecret()).method("POST", body) .build(); try { - client.newCall(postRequest).execute(); + httpClient.newCall(postRequest).execute(); } catch (IOException e) { LOGGER.info("[" + session.getSpec().getName() + "] Could not send message to extension:" + e); } @@ -91,7 +99,7 @@ protected void postMessage(Session session, String level, String message, boolea } protected Optional getURL(Session session) { - Optional ip = getIP(session); + Optional ip = SessionUtil.getClusterIP(resourceClient, session); Optional port = getPort(session); if (ip.isPresent() && port.isPresent()) { return Optional.of("http://" + ip.get() + ":" + port.get() + MONITOR_BASE_PATH + POST_MESSAGE); @@ -99,18 +107,12 @@ protected Optional getURL(Session session) { return Optional.empty(); } - protected Optional getIP(Session session) { - Optional sessionIP = resourceClient.getClusterIPFromSessionName(session.getSpec().getName()); - return sessionIP; - } - protected Optional getPort(Session session) { String appDefinitionId = session.getSpec().getAppDefinition(); - Optional optionalAppDefinition = resourceClient.appDefinitions().get(appDefinitionId); - if (optionalAppDefinition.isPresent()) { - return Optional.of(optionalAppDefinition.get().getSpec().getMonitor().getPort()); - } - return Optional.empty(); + return resourceClient.appDefinitions().get(appDefinitionId)// + .map(AppDefinition::getSpec)// + .map(AppDefinitionSpec::getMonitor)// + .map(AppDefinitionSpec.Monitor::getPort); } protected boolean isEnabled() { diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/plugins/MonitorActivityTracker.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/plugins/MonitorActivityTracker.java index 9303b893e..35b41bfc6 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/plugins/MonitorActivityTracker.java +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/plugins/MonitorActivityTracker.java @@ -33,6 +33,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.eclipse.theia.cloud.common.k8s.client.TheiaCloudClient; +import org.eclipse.theia.cloud.common.k8s.resource.OperatorStatus; import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinition; import org.eclipse.theia.cloud.common.k8s.resource.session.Session; import org.eclipse.theia.cloud.operator.TheiaCloudOperatorArguments; @@ -47,6 +48,14 @@ import okhttp3.Response; import okhttp3.ResponseBody; +import io.sentry.ISpan; +import io.sentry.ITransaction; +import io.sentry.Sentry; +import io.sentry.SpanStatus; +import io.sentry.okhttp.SentryOkHttpEventListener; +import io.sentry.okhttp.SentryOkHttpInterceptor; +import org.eclipse.theia.cloud.common.tracing.Tracing; + public class MonitorActivityTracker implements OperatorPlugin { private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(); @@ -68,6 +77,9 @@ public class MonitorActivityTracker implements OperatorPlugin { @Inject private TheiaCloudOperatorArguments arguments; + private final OkHttpClient httpClient = new OkHttpClient.Builder().addInterceptor(new SentryOkHttpInterceptor()) + .eventListener(new SentryOkHttpEventListener()).build(); + @Override public void start() { if (arguments.isEnableMonitor() && arguments.isEnableActivityTracker()) { @@ -78,85 +90,138 @@ public void start() { } protected void pingAllSessions() { - List sessions = resourceClient.sessions().list(); - String correlationId = generateCorrelationId(); - - LOGGER.debug("Pinging sessions: " + sessions); - - for (Session session : sessions) { - Optional sessionIP = resourceClient.getClusterIPFromSessionName(session.getSpec().getName()); - if (sessionIP.isPresent()) { - String appDefinitionName = session.getSpec().getAppDefinition(); - Optional appDefinitionOptional = resourceClient.appDefinitions().get(appDefinitionName); - if (appDefinitionOptional.isPresent()) { - AppDefinition appDefinition = appDefinitionOptional.get(); - int timeoutAfter = appDefinition.getSpec().getMonitor().getActivityTracker().getTimeoutAfter(); - int notifyAfter = appDefinition.getSpec().getMonitor().getActivityTracker().getNotifyAfter(); - int port = appDefinition.getSpec().getMonitor().getPort(); - - pingSession(correlationId, session, sessionIP.get(), port, timeoutAfter, notifyAfter); + ITransaction transaction = Tracing.startTransaction("monitor.activity-check", "monitor"); + try { + // Only look at handled sessions (handled sessions have a lastActivity) + List sessions = resourceClient.sessions().list().stream() + .filter(session -> OperatorStatus.HANDLED.equals(session.getStatus().getOperatorStatus())).toList(); + String correlationId = generateCorrelationId(); + + LOGGER.debug("Pinging sessions: " + sessions); + + transaction.setData("session_count", sessions.size()); + + int successCount = 0; + int failureCount = 0; + int missingIPCount = 0; + + for (Session session : sessions) { + Optional sessionIP = resourceClient.getClusterIPFromSessionName(session.getSpec().getName()); + if (sessionIP.isPresent()) { + String appDefinitionName = session.getSpec().getAppDefinition(); + Optional appDefinitionOptional = resourceClient.appDefinitions() + .get(appDefinitionName); + if (appDefinitionOptional.isPresent()) { + AppDefinition appDefinition = appDefinitionOptional.get(); + int timeoutAfter = appDefinition.getSpec().getMonitor().getActivityTracker().getTimeoutAfter(); + int notifyAfter = appDefinition.getSpec().getMonitor().getActivityTracker().getNotifyAfter(); + int port = appDefinition.getSpec().getMonitor().getPort(); + + boolean success = pingSession(transaction, correlationId, session, sessionIP.get(), port, + timeoutAfter, notifyAfter); + if (success) { + successCount++; + } else { + failureCount++; + } + } + } else { + missingIPCount++; + LOGGER.error("No ClusterIP found for session " + session.getSpec().getName()); + Sentry.addBreadcrumb("No ClusterIP found for session " + session.getSpec().getName()); } - } else { - LOGGER.error("No ClusterIP found for session " + session.getSpec().getName()); } + + transaction.setData("success_count", successCount); + transaction.setData("failure_count", failureCount); + transaction.setData("missing_ip_count", missingIPCount); + transaction.setStatus(SpanStatus.OK); + } catch (Exception e) { + transaction.setStatus(SpanStatus.INTERNAL_ERROR); + transaction.setThrowable(e); + throw e; + } finally { + transaction.finish(); } } - protected void pingSession(String correlationId, Session session, String sessionURL, int port, int shutdownAfter, - int notifyAfter) { + protected boolean pingSession(ITransaction transaction, String correlationId, Session session, String sessionURL, + int port, int shutdownAfter, int notifyAfter) { String sessionName = session.getSpec().getName(); - logInfo(sessionName, "Pinging session at " + sessionURL); - OkHttpClient client = new OkHttpClient().newBuilder().build(); - - String getActivityURL = getURL(sessionURL, port, GET_ACTIVITY); - logInfo(sessionName, "GET " + getActivityURL); + ISpan sessionSpan = transaction.startChild("monitor.ping-session", "monitor.session"); + sessionSpan.setData("session_name", sessionName); + sessionSpan.setData("session_url", sessionURL); - Request getActivityRequest = new Request.Builder().url(getActivityURL) - .addHeader("Authorization", "Bearer " + session.getSpec().getSessionSecret()).get().build(); - Response getActivityResponse; try { - getActivityResponse = client.newCall(getActivityRequest).execute(); - ResponseBody body = getActivityResponse.body(); - if (getActivityResponse.code() == 200 && body != null) { - long lastReportedMilliseconds = Long.valueOf(body.string()); + logInfo(sessionName, "Pinging session at " + sessionURL); - session = updateLastActivity(correlationId, session, lastReportedMilliseconds); - } else { - logInfo(sessionName, - "REQUEST FAILED (Returned " + getActivityResponse.code() + ": " + "GET " + getActivityURL); + String getActivityURL = getURL(sessionURL, port, GET_ACTIVITY); + logInfo(sessionName, "GET " + getActivityURL); + + boolean success = false; + try { + Request getActivityRequest = new Request.Builder().url(getActivityURL) + .addHeader("Authorization", "Bearer " + session.getSpec().getSessionSecret()).get().build(); + Response getActivityResponse = httpClient.newCall(getActivityRequest).execute(); + ResponseBody body = getActivityResponse.body(); + + if (getActivityResponse.code() == 200 && body != null) { + long lastReportedMilliseconds = Long.valueOf(body.string()); + session = updateLastActivity(correlationId, session, lastReportedMilliseconds); + success = true; + } else { + logInfo(sessionName, + "REQUEST FAILED (Returned " + getActivityResponse.code() + ": " + "GET " + getActivityURL); + } + } catch (IOException e) { + logInfo(sessionName, "REQUEST FAILED: " + "GET " + getActivityURL + ". Error: " + e); } - } catch (IOException e) { - logInfo(sessionName, "REQUEST FAILED: " + "GET " + getActivityURL + ". Error: " + e); - } - Date lastActivityDate = new Date(session.getNonNullStatus().getLastActivity()); - Date currentDate = new Date(OffsetDateTime.now(ZoneOffset.UTC).toInstant().toEpochMilli()); - - long minutesPassed = getMinutesPassed(lastActivityDate, currentDate); - - String minutes = minutesPassed == 1 ? "minute" : "minutes"; - logInfo(sessionName, "Last reported activity was: " + formatDate(lastActivityDate) + " (" + minutesPassed + " " - + minutes + " ago)"); - - if (minutesPassed < shutdownAfter) { - if (minutesPassed >= notifyAfter) { - logInfo(sessionName, "Notifying session as timeout of " + notifyAfter + " minutes was reached!"); - String postPopupURL = getURL(sessionURL, port, POST_POPUP); - logInfo(sessionName, "POST " + postPopupURL); - MediaType mediaType = MediaType.parse("text/plain"); - RequestBody body = RequestBody.create(mediaType, ""); - Request postRequest = new Request.Builder().url(postPopupURL) - .addHeader("Authorization", "Bearer " + session.getSpec().getSessionSecret()).post(body) - .build(); - try { - client.newCall(postRequest).execute(); - } catch (IOException e) { - logInfo(sessionName, "REQUEST FAILED: " + "POST " + postPopupURL); + Date lastActivityDate = new Date(session.getNonNullStatus().getLastActivity()); + Date currentDate = new Date(OffsetDateTime.now(ZoneOffset.UTC).toInstant().toEpochMilli()); + long minutesPassed = getMinutesPassed(lastActivityDate, currentDate); + + sessionSpan.setData("minutes_since_activity", minutesPassed); + String minutes = minutesPassed == 1 ? "minute" : "minutes"; + logInfo(sessionName, "Last reported activity was: " + formatDate(lastActivityDate) + " (" + minutesPassed + + " " + minutes + " ago)"); + + if (minutesPassed < shutdownAfter) { + if (minutesPassed >= notifyAfter) { + logInfo(sessionName, "Notifying session as timeout of " + notifyAfter + " minutes was reached!"); + + String postPopupURL = getURL(sessionURL, port, POST_POPUP); + logInfo(sessionName, "POST " + postPopupURL); + try { + MediaType mediaType = MediaType.parse("text/plain"); + RequestBody body = RequestBody.create("", mediaType); + Request postRequest = new Request.Builder().url(postPopupURL) + .addHeader("Authorization", "Bearer " + session.getSpec().getSessionSecret()).post(body) + .build(); + httpClient.newCall(postRequest).execute(); + } catch (IOException e) { + logInfo(sessionName, "REQUEST FAILED: " + "POST " + postPopupURL); + } } + sessionSpan.setStatus(SpanStatus.OK); + } else { + // Timeout reached + sessionSpan.setData("action", "timeout_shutdown"); + stopNonActiveSession(sessionSpan, correlationId, session, shutdownAfter); + sessionSpan.setStatus(SpanStatus.OK); } - } else { - // Timeout reached - stopNonActiveSession(correlationId, session, shutdownAfter); + + return success; + } catch (Exception e) { + LOGGER.error( + formatLogMessage(correlationId, correlationId, "Exception while pinging session " + sessionName), + e); + sessionSpan.setStatus(SpanStatus.INTERNAL_ERROR); + sessionSpan.setThrowable(e); + Sentry.captureException(e); + return false; + } finally { + sessionSpan.finish(); } } @@ -174,16 +239,28 @@ protected long getMinutesPassed(Date lastActivity, Date currentTime) { return TimeUnit.MILLISECONDS.toMinutes(timePassed); } - protected void stopNonActiveSession(String correlationId, Session session, int shutdownAfter) { + protected void stopNonActiveSession(ISpan parentSpan, String correlationId, Session session, int shutdownAfter) { String sessionName = session.getSpec().getName(); + ISpan deleteSpan = parentSpan.startChild("monitor.delete-session", "monitor.cleanup"); + deleteSpan.setData("session_name", sessionName); + deleteSpan.setData("timeout_minutes", shutdownAfter); + try { this.messagingService.sendTimeoutMessage(session, "Timeout of " + shutdownAfter + " minutes of inactivity was reached!"); logInfo(sessionName, "Deleting session as timeout of " + shutdownAfter + " minutes was reached!"); resourceClient.sessions().delete(COR_ID_NOACTIVITYPREFIX + correlationId, sessionName); + deleteSpan.setStatus(SpanStatus.OK); + Sentry.captureMessage( + "Session stopped due to inactivity: " + sessionName + " (timeout: " + shutdownAfter + " minutes)"); } catch (Exception e) { LOGGER.error(formatLogMessage(COR_ID_NOACTIVITYPREFIX, correlationId, "Exception trying to delete session"), e); + deleteSpan.setStatus(SpanStatus.INTERNAL_ERROR); + deleteSpan.setThrowable(e); + Sentry.captureException(e); + } finally { + deleteSpan.finish(); } } diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/pool/PrewarmedResourcePool.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/pool/PrewarmedResourcePool.java new file mode 100644 index 000000000..8300a20a0 --- /dev/null +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/pool/PrewarmedResourcePool.java @@ -0,0 +1,1404 @@ +package org.eclipse.theia.cloud.operator.pool; + +import static org.eclipse.theia.cloud.common.util.LogMessageUtil.formatLogMessage; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.time.Instant; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.theia.cloud.common.k8s.client.TheiaCloudClient; +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinition; +import org.eclipse.theia.cloud.common.k8s.resource.session.Session; +import org.eclipse.theia.cloud.common.util.LabelsUtil; +import org.eclipse.theia.cloud.common.util.NamingUtil; +import org.eclipse.theia.cloud.operator.TheiaCloudOperatorArguments; +import org.eclipse.theia.cloud.operator.handler.AddedHandlerUtil; +import org.eclipse.theia.cloud.operator.sidecar.SidecarConfig; +import org.eclipse.theia.cloud.operator.sidecar.SidecarManager; +import org.eclipse.theia.cloud.operator.replacements.DefaultPersistentVolumeTemplateReplacements; +import org.eclipse.theia.cloud.operator.util.JavaResourceUtil; +import org.eclipse.theia.cloud.operator.util.K8sResourceFactory; +import org.eclipse.theia.cloud.operator.util.K8sUtil; +import org.eclipse.theia.cloud.operator.util.OwnershipManager; +import org.eclipse.theia.cloud.operator.util.OwnershipManager.OwnerContext; +import org.eclipse.theia.cloud.operator.util.ResourceLifecycleManager; +import org.eclipse.theia.cloud.common.tracing.Tracing; +import org.eclipse.theia.cloud.operator.util.TheiaCloudConfigMapUtil; +import org.eclipse.theia.cloud.operator.util.TheiaCloudDeploymentUtil; +import org.eclipse.theia.cloud.operator.util.TheiaCloudHandlerUtil; +import org.eclipse.theia.cloud.operator.util.TheiaCloudK8sUtil; +import org.eclipse.theia.cloud.operator.util.TheiaCloudServiceUtil; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.client.KubernetesClientException; +import io.fabric8.kubernetes.client.dsl.PodResource; +import io.sentry.ISpan; +import io.sentry.SpanStatus; + +/** + * Manages a pool of prewarmed (eager start) resources for an AppDefinition. Responsibilities: - Creating/scaling the + * pool of prewarmed instances - Reserving instances for sessions - Releasing instances back to the pool - Cleaning up + * pool resources + */ +@Singleton +public class PrewarmedResourcePool { + + private static final Logger LOGGER = LogManager.getLogger(PrewarmedResourcePool.class); + + public static final String EAGER_START_REFRESH_ANNOTATION = "theia-cloud.io/eager-start-refresh"; + public static final String APPDEFINITION_GENERATION_LABEL = "theia-cloud.io/appdefinition-generation"; + + @Inject + private TheiaCloudClient client; + + @Inject + private K8sResourceFactory resourceFactory; + + @Inject + private SidecarManager sidecarManager; + + @Inject + private TheiaCloudOperatorArguments arguments; + + /** + * Represents a reserved instance from the pool. + */ + public static class PoolInstance { + private final int instanceId; + private final Service externalService; + private final Service internalService; + private final String deploymentName; + + public PoolInstance(int instanceId, Service externalService, Service internalService, String deploymentName) { + this.instanceId = instanceId; + this.externalService = externalService; + this.internalService = internalService; + this.deploymentName = deploymentName; + } + + public int getInstanceId() { + return instanceId; + } + + public Service getExternalService() { + return externalService; + } + + public Service getInternalService() { + return internalService; + } + + public String getDeploymentName() { + return deploymentName; + } + } + + /** + * Result of a reservation attempt. + */ + public static class ReservationResult { + private final ReservationOutcome outcome; + private final PoolInstance instance; + + private ReservationResult(ReservationOutcome outcome, PoolInstance instance) { + this.outcome = outcome; + this.instance = instance; + } + + public ReservationOutcome getOutcome() { + return outcome; + } + + public Optional getInstance() { + return Optional.ofNullable(instance); + } + + public static ReservationResult success(PoolInstance instance) { + return new ReservationResult(ReservationOutcome.SUCCESS, instance); + } + + public static ReservationResult noCapacity() { + return new ReservationResult(ReservationOutcome.NO_CAPACITY, null); + } + + public static ReservationResult error() { + return new ReservationResult(ReservationOutcome.ERROR, null); + } + } + + public enum ReservationOutcome { + SUCCESS, NO_CAPACITY, ERROR + } + + // ========== Pool Management ========== + + /** + * Ensures the pool has the specified minimum number of instances. Creates missing resources (services, configmaps, + * deployments). + */ + public boolean ensureCapacity(AppDefinition appDef, int minInstances, String correlationId) { + String appDefName = appDef.getSpec().getName(); + ISpan span = Tracing.childSpan("pool.ensure_capacity", "Ensure pool capacity"); + + span.setTag("app_definition", appDefName); + span.setData("min_instances", minInstances); + + LOGGER.info(formatLogMessage(correlationId, "Ensuring pool capacity: " + minInstances + " for " + appDefName)); + + try { + String ownerName = appDef.getMetadata().getName(); + String ownerUID = appDef.getMetadata().getUid(); + Map labels = new HashMap<>(); + + // Get existing resources + ISpan fetchSpan = span.startChild("pool.fetch_existing", "Fetch existing resources"); + List existingServices = K8sUtil.getExistingServices(client.kubernetes(), client.namespace(), + ownerName, ownerUID); + List existingDeployments = K8sUtil.getExistingDeployments(client.kubernetes(), + client.namespace(), ownerName, ownerUID); + List existingConfigMaps = K8sUtil.getExistingConfigMaps(client.kubernetes(), client.namespace(), + ownerName, ownerUID); + + fetchSpan.setData("existing_services", existingServices.size()); + fetchSpan.setData("existing_deployments", existingDeployments.size()); + fetchSpan.setData("existing_configmaps", existingConfigMaps.size()); + Tracing.finishSuccess(fetchSpan); + + // Compute missing IDs + Set missingServiceIds = TheiaCloudServiceUtil.computeIdsOfMissingServices(appDef, correlationId, + minInstances, existingServices); + Set missingDeploymentIds = TheiaCloudDeploymentUtil.computeIdsOfMissingDeployments(appDef, + correlationId, minInstances, existingDeployments); + + span.setData("missing_service_ids", missingServiceIds.size()); + span.setData("missing_deployment_ids", missingDeploymentIds.size()); + + boolean success = true; + + // Create missing services + if (!missingServiceIds.isEmpty()) { + ISpan serviceSpan = span.startChild("pool.create_services", "Create missing services"); + serviceSpan.setData("count", missingServiceIds.size()); + int created = 0; + int failed = 0; + for (int instance : missingServiceIds) { + ISpan svcSpan = serviceSpan.startChild("k8s.service.create", "Create service " + instance); + svcSpan.setData("instance_id", instance); + boolean extOk = resourceFactory + .createServiceForEagerInstance(appDef, instance, labels, correlationId).isPresent(); + boolean intOk = resourceFactory + .createInternalServiceForEagerInstance(appDef, instance, labels, correlationId).isPresent(); + if (extOk && intOk) { + created++; + Tracing.finishSuccess(svcSpan); + } else { + failed++; + success = false; + svcSpan.setTag("outcome", "failure"); Tracing.finish(svcSpan, SpanStatus.INTERNAL_ERROR); + } + } + serviceSpan.setData("created", created); + serviceSpan.setData("failed", failed); + serviceSpan.setTag("outcome", failed == 0 ? "success" : "failure"); + Tracing.finish(serviceSpan, failed == 0 ? SpanStatus.OK : SpanStatus.INTERNAL_ERROR); + } + + // Create missing configmaps (if using Keycloak) + if (arguments.isUseKeycloak()) { + List proxyConfigMaps = existingConfigMaps.stream().filter( + cm -> "proxy".equals(cm.getMetadata().getLabels().get("theia-cloud.io/template-purpose"))) + .collect(Collectors.toList()); + List emailConfigMaps = existingConfigMaps.stream().filter( + cm -> "emails".equals(cm.getMetadata().getLabels().get("theia-cloud.io/template-purpose"))) + .collect(Collectors.toList()); + + Set missingProxyIds = TheiaCloudConfigMapUtil.computeIdsOfMissingProxyConfigMaps(appDef, + correlationId, minInstances, proxyConfigMaps); + Set missingEmailIds = TheiaCloudConfigMapUtil.computeIdsOfMissingEmailConfigMaps(appDef, + correlationId, minInstances, emailConfigMaps); + + if (!missingProxyIds.isEmpty() || !missingEmailIds.isEmpty()) { + ISpan cmSpan = span.startChild("pool.create_configmaps", "Create missing configmaps"); + cmSpan.setData("missing_proxy", missingProxyIds.size()); + cmSpan.setData("missing_email", missingEmailIds.size()); + + for (int instance : missingProxyIds) { + success &= resourceFactory + .createProxyConfigMapForEagerInstance(appDef, instance, labels, correlationId) + .isPresent(); + } + for (int instance : missingEmailIds) { + success &= resourceFactory + .createEmailConfigMapForEagerInstance(appDef, instance, labels, correlationId) + .isPresent(); + } + cmSpan.setTag("outcome", success ? "success" : "failure"); Tracing.finish(cmSpan, success ? SpanStatus.OK : SpanStatus.INTERNAL_ERROR); + } + } + + // Ensure PVCs exist first for sidecars with mountWorkspace=true. + for (int instance = 1; instance <= minInstances; instance++) { + createInstancePvc(appDef, instance, correlationId); + } + + // Create sidecar resources BEFORE Theia deployments so DNS resolves at pod startup + success &= sidecarManager.ensurePrewarmedSidecarCapacity(appDef, minInstances, labels, correlationId); + + // Create missing deployments (with shared PVC) + if (!missingDeploymentIds.isEmpty()) { + ISpan deploySpan = span.startChild("pool.create_deployments", "Create missing deployments"); + deploySpan.setData("count", missingDeploymentIds.size()); + int created = 0; + int failed = 0; + for (int instance : missingDeploymentIds) { + ISpan depSpan = deploySpan.startChild("k8s.deployment.create", "Create deployment " + instance); + depSpan.setData("instance_id", instance); + Optional pvcName = createInstancePvc(appDef, instance, correlationId); + if (resourceFactory.createDeploymentForEagerInstance(appDef, instance, pvcName, labels, correlationId) + .isPresent()) { + created++; + Tracing.finishSuccess(depSpan); + } else { + failed++; + success = false; + depSpan.setTag("outcome", "failure"); Tracing.finish(depSpan, SpanStatus.INTERNAL_ERROR); + } + } + deploySpan.setData("created", created); + deploySpan.setData("failed", failed); + deploySpan.setTag("outcome", failed == 0 ? "success" : "failure"); + Tracing.finish(deploySpan, failed == 0 ? SpanStatus.OK : SpanStatus.INTERNAL_ERROR); + } + + span.setTag("outcome", success ? "success" : "failure"); Tracing.finish(span, success ? SpanStatus.OK : SpanStatus.INTERNAL_ERROR); + return success; + + } catch (Exception e) { + Tracing.finishError(span, e); + throw e; + } + } + + /** + * Reconciles the pool to match the target instance count. Creates missing instances, removes excess instances + * (respecting ownership). + */ + public boolean reconcile(AppDefinition appDef, int targetInstances, String correlationId) { + String appDefName = appDef.getSpec().getName(); + ISpan span = Tracing.childSpan("pool.reconcile", "Reconcile pool"); + + span.setTag("app_definition", appDefName); + span.setData("target_instances", targetInstances); + span.setData("generation", appDef.getMetadata().getGeneration()); + + LOGGER.info(formatLogMessage(correlationId, "Reconciling pool to " + targetInstances + " instances")); + + try { + String ownerName = appDef.getMetadata().getName(); + String ownerUID = appDef.getMetadata().getUid(); + OwnerContext owner = OwnerContext.of(ownerName, ownerUID, AppDefinition.API, AppDefinition.KIND); + long currentGeneration = appDef.getMetadata().getGeneration(); + Map labels = new HashMap<>(); + + boolean success = true; + + // Reconcile services + ISpan serviceSpan = span.startChild("pool.reconcile_services", "Reconcile services"); + List existingServices = K8sUtil.getExistingServices(client.kubernetes(), client.namespace(), + ownerName, ownerUID); + List externalServices = existingServices.stream() + .filter(s -> !s.getMetadata().getName().endsWith("-int")).collect(Collectors.toList()); + List internalServices = existingServices.stream() + .filter(s -> s.getMetadata().getName().endsWith("-int")).collect(Collectors.toList()); + Set missingServiceIds = TheiaCloudServiceUtil.computeIdsOfMissingServices(appDef, correlationId, + targetInstances, existingServices); + + serviceSpan.setData("existing_external", externalServices.size()); + serviceSpan.setData("existing_internal", internalServices.size()); + serviceSpan.setData("missing", missingServiceIds.size()); + + // Reconcile external services + ResourceLifecycleManager.ReconcileResult extResult = ResourceLifecycleManager + .reconcile(ResourceLifecycleManager.ReconcileContext. builder() + .correlationId(correlationId).existingResources(externalServices) + .missingIds(missingServiceIds).targetCount(targetInstances).owner(owner) + .resourceAccessor( + s -> client.kubernetes().services().inNamespace(client.namespace()).resource(s)) + .idExtractor(s -> TheiaCloudServiceUtil.getId(correlationId, appDef, s)) + .resourceTypeName("service").createResource(instance -> { + resourceFactory.createServiceForEagerInstance(appDef, instance, labels, correlationId); + }).shouldRecreate( + s -> OwnershipManager.isOwnedSolelyBy(s, owner) + && isOutdated(s, currentGeneration)) + .recreateResource(s -> { + Integer id = TheiaCloudServiceUtil.getId(correlationId, appDef, s); + if (id != null) { + resourceFactory.createServiceForEagerInstance(appDef, id, labels, correlationId); + } + }).build()); + success &= extResult.isSuccess(); + + // Reconcile internal services + ResourceLifecycleManager.ReconcileResult intResult = ResourceLifecycleManager + .reconcile(ResourceLifecycleManager.ReconcileContext. builder() + .correlationId(correlationId).existingResources(internalServices) + .missingIds(missingServiceIds).targetCount(targetInstances).owner(owner) + .resourceAccessor( + s -> client.kubernetes().services().inNamespace(client.namespace()).resource(s)) + .idExtractor(s -> TheiaCloudServiceUtil.getId(correlationId, appDef, s)) + .resourceTypeName("internal service").createResource(instance -> { + resourceFactory.createInternalServiceForEagerInstance(appDef, instance, labels, + correlationId); + }).shouldRecreate( + s -> OwnershipManager.isOwnedSolelyBy(s, owner) + && isOutdated(s, currentGeneration)) + .recreateResource(s -> { + Integer id = TheiaCloudServiceUtil.getId(correlationId, appDef, s); + if (id != null) { + resourceFactory.createInternalServiceForEagerInstance(appDef, id, labels, + correlationId); + } + }).build()); + success &= intResult.isSuccess(); + + int created = extResult.getCreated() + intResult.getCreated(); + int deleted = extResult.getDeleted() + intResult.getDeleted(); + int recreated = extResult.getRecreated() + intResult.getRecreated(); + int skipped = extResult.getSkipped() + intResult.getSkipped(); + success = extResult.isSuccess() && intResult.isSuccess(); + serviceSpan.setData("created_count", created); + serviceSpan.setData("deleted_count", deleted); + serviceSpan.setData("recreated_count", recreated); + serviceSpan.setData("skipped_count", skipped); + serviceSpan.setTag("outcome", success ? "success" : "failure"); + serviceSpan.setTag("had_changes", (created + deleted + recreated) > 0 ? "true" : "false"); + serviceSpan.finish(); + + // Reconcile configmaps (if using Keycloak) + if (arguments.isUseKeycloak()) { + ISpan cmSpan = span.startChild("pool.reconcile_configmaps", "Reconcile configmaps"); + List existingConfigMaps = K8sUtil.getExistingConfigMaps(client.kubernetes(), + client.namespace(), ownerName, ownerUID); + List proxyConfigMaps = existingConfigMaps.stream().filter( + cm -> "proxy".equals(cm.getMetadata().getLabels().get("theia-cloud.io/template-purpose"))) + .collect(Collectors.toList()); + List emailConfigMaps = existingConfigMaps.stream().filter( + cm -> "emails".equals(cm.getMetadata().getLabels().get("theia-cloud.io/template-purpose"))) + .collect(Collectors.toList()); + + Set missingProxyIds = TheiaCloudConfigMapUtil.computeIdsOfMissingProxyConfigMaps(appDef, + correlationId, targetInstances, proxyConfigMaps); + Set missingEmailIds = TheiaCloudConfigMapUtil.computeIdsOfMissingEmailConfigMaps(appDef, + correlationId, targetInstances, emailConfigMaps); + + ResourceLifecycleManager.ReconcileResult proxyResult = ResourceLifecycleManager + .reconcile(ResourceLifecycleManager.ReconcileContext. builder() + .correlationId(correlationId).existingResources(proxyConfigMaps) + .missingIds(missingProxyIds).targetCount(targetInstances).owner(owner) + .resourceAccessor(cm -> client.kubernetes().configMaps().inNamespace(client.namespace()) + .resource(cm)) + .idExtractor(cm -> TheiaCloudConfigMapUtil.getProxyId(correlationId, appDef, cm)) + .resourceTypeName("proxy configmap") + .createResource(instance -> resourceFactory.createProxyConfigMapForEagerInstance(appDef, + instance, labels, correlationId)) + .shouldRecreate(cm -> OwnershipManager.isOwnedSolelyBy(cm, owner) + && isOutdated(cm, currentGeneration)) + .recreateResource(cm -> { + Integer id = TheiaCloudConfigMapUtil.getProxyId(correlationId, appDef, cm); + if (id != null) { + resourceFactory.createProxyConfigMapForEagerInstance(appDef, id, labels, + correlationId); + } + }).build()); + success &= proxyResult.isSuccess(); + + ResourceLifecycleManager.ReconcileResult emailResult = ResourceLifecycleManager + .reconcile(ResourceLifecycleManager.ReconcileContext. builder() + .correlationId(correlationId).existingResources(emailConfigMaps) + .missingIds(missingEmailIds).targetCount(targetInstances).owner(owner) + .resourceAccessor(cm -> client.kubernetes().configMaps().inNamespace(client.namespace()) + .resource(cm)) + .idExtractor(cm -> TheiaCloudConfigMapUtil.getEmailId(correlationId, appDef, cm)) + .resourceTypeName("email configmap") + .createResource(instance -> resourceFactory.createEmailConfigMapForEagerInstance(appDef, + instance, labels, correlationId)) + .shouldRecreate(cm -> OwnershipManager.isOwnedSolelyBy(cm, owner) + && isOutdated(cm, currentGeneration)) + .recreateResource(cm -> { + Integer id = TheiaCloudConfigMapUtil.getEmailId(correlationId, appDef, cm); + if (id != null) { + resourceFactory.createEmailConfigMapForEagerInstance(appDef, id, labels, + correlationId); + } + }).build()); + success &= emailResult.isSuccess(); + + int cmCreated = proxyResult.getCreated() + emailResult.getCreated(); + int cmDeleted = proxyResult.getDeleted() + emailResult.getDeleted(); + int cmRecreated = proxyResult.getRecreated() + emailResult.getRecreated(); + int cmSkipped = proxyResult.getSkipped() + emailResult.getSkipped(); + boolean cmSuccess = proxyResult.isSuccess() && emailResult.isSuccess(); + cmSpan.setData("created_count", cmCreated); + cmSpan.setData("deleted_count", cmDeleted); + cmSpan.setData("recreated_count", cmRecreated); + cmSpan.setData("skipped_count", cmSkipped); + cmSpan.setTag("outcome", cmSuccess ? "success" : "failure"); + cmSpan.setTag("had_changes", (cmCreated + cmDeleted + cmRecreated) > 0 ? "true" : "false"); + cmSpan.finish(); + } + + // Ensure PVCs exist first for sidecars with mountWorkspace=true. + for (int instance = 1; instance <= targetInstances; instance++) { + createInstancePvc(appDef, instance, correlationId); + } + + // Reconcile sidecar resources BEFORE Theia deployments so DNS resolves at pod startup + success &= sidecarManager.reconcilePrewarmedSidecars(appDef, targetInstances, labels, correlationId); + + // Reconcile deployments + ISpan deploySpan = span.startChild("pool.reconcile_deployments", "Reconcile deployments"); + List existingDeployments = K8sUtil.getExistingDeployments(client.kubernetes(), + client.namespace(), ownerName, ownerUID); + Set missingDeploymentIds = TheiaCloudDeploymentUtil.computeIdsOfMissingDeployments(appDef, + correlationId, targetInstances, existingDeployments); + + deploySpan.setData("existing", existingDeployments.size()); + deploySpan.setData("missing", missingDeploymentIds.size()); + + ResourceLifecycleManager.ReconcileResult deployResult = ResourceLifecycleManager + .reconcile(ResourceLifecycleManager.ReconcileContext. builder() + .correlationId(correlationId).existingResources(existingDeployments) + .missingIds(missingDeploymentIds).targetCount(targetInstances).owner(owner) + .resourceAccessor(d -> client.kubernetes().apps().deployments() + .inNamespace(client.namespace()).resource(d)) + .idExtractor(d -> TheiaCloudDeploymentUtil.getId(correlationId, appDef, d)) + .resourceTypeName("deployment") + .createResource(instance -> { + Optional pvcName = createInstancePvc(appDef, instance, correlationId); + resourceFactory.createDeploymentForEagerInstance(appDef, instance, pvcName, labels, correlationId); + }) + .shouldRecreate( + d -> OwnershipManager.isOwnedSolelyBy(d, owner) + && isOutdated(d, currentGeneration)) + .recreateResource(d -> { + Integer id = TheiaCloudDeploymentUtil.getId(correlationId, appDef, d); + if (id != null) { + Optional pvcName = createInstancePvc(appDef, id, correlationId); + resourceFactory.createDeploymentForEagerInstance(appDef, id, pvcName, labels, correlationId); + } + }).build()); + success &= deployResult.isSuccess(); + + deploySpan.setData("created_count", deployResult.getCreated()); + deploySpan.setData("deleted_count", deployResult.getDeleted()); + deploySpan.setData("recreated_count", deployResult.getRecreated()); + deploySpan.setData("skipped_count", deployResult.getSkipped()); + deploySpan.setTag("outcome", deployResult.isSuccess() ? "success" : "failure"); + deploySpan.setTag("had_changes", (deployResult.getCreated() + deployResult.getDeleted() + deployResult.getRecreated()) > 0 ? "true" : "false"); + deploySpan.finish(); + + span.setTag("outcome", success ? "success" : "failure"); Tracing.finish(span, success ? SpanStatus.OK : SpanStatus.INTERNAL_ERROR); + return success; + + } catch (Exception e) { + Tracing.finishError(span, e); + throw e; + } + } + + /** + * Reconciles a single instance after session release. - If instanceId > minInstances → delete all resources for + * this instance - If resource generation != current AppDefinition generation → recreate - Otherwise → do nothing + */ + public void reconcileInstance(AppDefinition appDef, int instanceId, String correlationId) { + String appDefName = appDef.getSpec().getName(); + int minInstances = appDef.getSpec().getMinInstances(); + long currentGeneration = appDef.getMetadata().getGeneration(); + + ISpan span = Tracing.childSpan("pool.reconcile_instance", "Reconcile instance " + instanceId); + + span.setTag("app_definition", appDefName); + span.setData("instance_id", instanceId); + span.setData("min_instances", minInstances); + span.setData("generation", currentGeneration); + + try { + String ownerName = appDef.getMetadata().getName(); + String ownerUID = appDef.getMetadata().getUid(); + + LOGGER.info(formatLogMessage(correlationId, "Reconciling instance " + instanceId + " (minInstances=" + + minInstances + ", generation=" + currentGeneration + ")")); + + // Find resources for this instance + List allServices = K8sUtil.getExistingServices(client.kubernetes(), client.namespace(), ownerName, + ownerUID); + List allDeployments = K8sUtil.getExistingDeployments(client.kubernetes(), client.namespace(), + ownerName, ownerUID); + List allConfigMaps = K8sUtil.getExistingConfigMaps(client.kubernetes(), client.namespace(), + ownerName, ownerUID); + + // Filter to just this instance + List instanceServices = allServices.stream() + .filter(s -> instanceId == parseInstanceIdOrDefault(s, -1)).collect(Collectors.toList()); + List instanceDeployments = allDeployments.stream() + .filter(d -> instanceId == parseDeploymentInstanceIdOrDefault(appDef, d, -1)) + .collect(Collectors.toList()); + List instanceConfigMaps = allConfigMaps.stream() + .filter(cm -> instanceId == parseConfigMapInstanceIdOrDefault(appDef, cm, -1)) + .collect(Collectors.toList()); + + span.setData("found_services", instanceServices.size()); + span.setData("found_deployments", instanceDeployments.size()); + span.setData("found_configmaps", instanceConfigMaps.size()); + + if (instanceId > minInstances) { + span.setTag("action", "delete_excess"); + LOGGER.info(formatLogMessage(correlationId, + "Instance " + instanceId + " exceeds minInstances (" + minInstances + "), deleting")); + + ISpan deleteSpan = span.startChild("pool.delete_excess_instance", "Delete excess instance"); + deleteInstanceResources(instanceServices, instanceDeployments, instanceConfigMaps, correlationId); + sidecarManager.deletePrewarmedSidecars(appDef, instanceId, correlationId); + deleteInstancePvc(appDef, instanceId, correlationId); + Tracing.finishSuccess(deleteSpan); + Tracing.finishSuccess(span); + return; + } + + boolean outdated = isOutdated(instanceServices, currentGeneration) + || isOutdated(instanceDeployments, currentGeneration) + || isOutdated(instanceConfigMaps, currentGeneration); + + if (outdated) { + span.setTag("action", "recreate_outdated"); + LOGGER.info(formatLogMessage(correlationId, + "Instance " + instanceId + " has outdated resources, recreating")); + + ISpan recreateSpan = span.startChild("pool.recreate_outdated_instance", "Recreate outdated instance"); + deleteInstanceResources(instanceServices, instanceDeployments, instanceConfigMaps, correlationId); + sidecarManager.deletePrewarmedSidecars(appDef, instanceId, correlationId); + deleteInstancePvc(appDef, instanceId, correlationId); + createInstanceResources(appDef, instanceId, correlationId); + Tracing.finishSuccess(recreateSpan); + Tracing.finishSuccess(span); + return; + } + + span.setTag("action", "no_action"); + LOGGER.info(formatLogMessage(correlationId, "Instance " + instanceId + " is up-to-date, no action needed")); + Tracing.finishSuccess(span); + + } catch (Exception e) { + Tracing.finishError(span, e); + throw e; + } + } + + private boolean isOutdated(List resources, long currentGeneration) { + for (var resource : resources) { + if (isOutdated(resource, currentGeneration)) { + return true; + } + } + return false; + } + + private boolean isOutdated(HasMetadata resource, long currentGeneration) { + Map labels = resource.getMetadata().getLabels(); + if (labels == null) { + return true; + } + String genLabel = labels.get(APPDEFINITION_GENERATION_LABEL); + if (genLabel == null) { + return true; + } + try { + long resourceGen = Long.parseLong(genLabel); + return resourceGen != currentGeneration; + } catch (NumberFormatException e) { + return true; + } + } + + private void deleteInstanceResources(List services, List deployments, + List configMaps, String correlationId) { + for (Service s : services) { + try { + client.kubernetes().services().inNamespace(client.namespace()).resource(s).delete(); + LOGGER.trace(formatLogMessage(correlationId, "Deleted service " + s.getMetadata().getName())); + } catch (KubernetesClientException e) { + LOGGER.warn(formatLogMessage(correlationId, "Failed to delete service " + s.getMetadata().getName()), + e); + } + } + for (Deployment d : deployments) { + try { + client.kubernetes().apps().deployments().inNamespace(client.namespace()).resource(d).delete(); + LOGGER.trace(formatLogMessage(correlationId, "Deleted deployment " + d.getMetadata().getName())); + } catch (KubernetesClientException e) { + LOGGER.warn(formatLogMessage(correlationId, "Failed to delete deployment " + d.getMetadata().getName()), + e); + } + } + for (ConfigMap cm : configMaps) { + try { + client.kubernetes().configMaps().inNamespace(client.namespace()).resource(cm).delete(); + LOGGER.trace(formatLogMessage(correlationId, "Deleted configmap " + cm.getMetadata().getName())); + } catch (KubernetesClientException e) { + LOGGER.warn(formatLogMessage(correlationId, "Failed to delete configmap " + cm.getMetadata().getName()), + e); + } + } + } + + private void createInstanceResources(AppDefinition appDef, int instanceId, String correlationId) { + Map labels = new HashMap<>(); + + resourceFactory.createServiceForEagerInstance(appDef, instanceId, labels, correlationId); + resourceFactory.createInternalServiceForEagerInstance(appDef, instanceId, labels, correlationId); + + if (arguments.isUseKeycloak()) { + resourceFactory.createProxyConfigMapForEagerInstance(appDef, instanceId, labels, correlationId); + resourceFactory.createEmailConfigMapForEagerInstance(appDef, instanceId, labels, correlationId); + } + + Optional pvcName = createInstancePvc(appDef, instanceId, correlationId); + + // Create sidecar Deployment+Service BEFORE Theia Deployment so DNS resolves at pod startup + sidecarManager.createPrewarmedSidecars(appDef, instanceId, pvcName, labels, correlationId); + + resourceFactory.createDeploymentForEagerInstance(appDef, instanceId, pvcName, labels, correlationId); + } + + private int parseInstanceIdOrDefault(Service service, int defaultValue) { + Integer id = parseInstanceId(service); + return id != null ? id : defaultValue; + } + + private int parseDeploymentInstanceIdOrDefault(AppDefinition appDef, Deployment deployment, int defaultValue) { + Integer id = TheiaCloudDeploymentUtil.getId(null, appDef, deployment); + return id != null ? id : defaultValue; + } + + private int parseConfigMapInstanceIdOrDefault(AppDefinition appDef, ConfigMap configMap, int defaultValue) { + Integer id = TheiaCloudConfigMapUtil.getProxyId(null, appDef, configMap); + if (id != null) { + return id; + } + id = TheiaCloudConfigMapUtil.getEmailId(null, appDef, configMap); + return id != null ? id : defaultValue; + } + + /** + * Releases all pool resources for an app definition (used during deletion). + */ + public boolean releaseAll(AppDefinition appDef, String correlationId) { + String appDefName = appDef.getSpec().getName(); + ISpan span = Tracing.childSpan("pool.release_all", "Release all pool resources"); + + span.setTag("app_definition", appDefName); + + LOGGER.info(formatLogMessage(correlationId, "Releasing all pool resources for " + appDefName)); + + try { + String ownerName = appDef.getMetadata().getName(); + String ownerUID = appDef.getMetadata().getUid(); + OwnerContext owner = OwnerContext.of(ownerName, ownerUID); + + boolean success = true; + + ISpan svcSpan = span.startChild("pool.release_services", "Release services"); + List services = K8sUtil.getExistingServices(client.kubernetes(), client.namespace(), ownerName, + ownerUID); + svcSpan.setData("count", services.size()); + success &= ResourceLifecycleManager.releaseOwnership(services, owner, + s -> client.kubernetes().services().inNamespace(client.namespace()).resource(s), "service", + correlationId); + svcSpan.setTag("outcome", success ? "success" : "failure"); Tracing.finish(svcSpan, success ? SpanStatus.OK : SpanStatus.INTERNAL_ERROR); + + ISpan cmSpan = span.startChild("pool.release_configmaps", "Release configmaps"); + List configMaps = K8sUtil.getExistingConfigMaps(client.kubernetes(), client.namespace(), + ownerName, ownerUID); + cmSpan.setData("count", configMaps.size()); + boolean cmSuccess = ResourceLifecycleManager.releaseOwnership(configMaps, owner, + cm -> client.kubernetes().configMaps().inNamespace(client.namespace()).resource(cm), "configmap", + correlationId); + success &= cmSuccess; + cmSpan.setTag("outcome", cmSuccess ? "success" : "failure"); Tracing.finish(cmSpan, cmSuccess ? SpanStatus.OK : SpanStatus.INTERNAL_ERROR); + + ISpan deploySpan = span.startChild("pool.release_deployments", "Release deployments"); + List deployments = K8sUtil.getExistingDeployments(client.kubernetes(), client.namespace(), + ownerName, ownerUID); + deploySpan.setData("count", deployments.size()); + boolean deploySuccess = ResourceLifecycleManager.releaseOwnership(deployments, owner, + d -> client.kubernetes().apps().deployments().inNamespace(client.namespace()).resource(d), + "deployment", correlationId); + success &= deploySuccess; + deploySpan.setTag("outcome", deploySuccess ? "success" : "failure"); Tracing.finish(deploySpan, deploySuccess ? SpanStatus.OK : SpanStatus.INTERNAL_ERROR); + + span.setTag("outcome", success ? "success" : "failure"); Tracing.finish(span, success ? SpanStatus.OK : SpanStatus.INTERNAL_ERROR); + return success; + + } catch (Exception e) { + Tracing.finishError(span, e); + throw e; + } + } + + // ========== Instance Reservation ========== + + /** + * Reserves a prewarmed instance for a session. Adds the session as an owner of the instance's resources. + */ + public synchronized ReservationResult reserveInstance(Session session, AppDefinition appDef, String correlationId) { + String sessionName = session.getSpec().getName(); + String appDefName = appDef.getSpec().getName(); + + ISpan span = Tracing.childSpan("pool.reserve", "Reserve pool instance"); + + span.setTag("app_definition", appDefName); + span.setData("session_name", sessionName); + + LOGGER.info(formatLogMessage(correlationId, "Attempting to reserve instance for session " + sessionName)); + + try { + String appDefOwnerName = appDef.getMetadata().getName(); + String appDefOwnerUID = appDef.getMetadata().getUid(); + String sessionUID = session.getMetadata().getUid(); + + // Get all services for this app definition + List existingServices = K8sUtil.getExistingServices(client.kubernetes(), client.namespace(), + appDefOwnerName, appDefOwnerUID); + + // Separate external and internal services + List externalServices = existingServices.stream() + .filter(s -> !s.getMetadata().getName().endsWith("-int")).collect(Collectors.toList()); + List internalServices = existingServices.stream() + .filter(s -> s.getMetadata().getName().endsWith("-int")).collect(Collectors.toList()); + + // Track pool capacity + int totalCapacity = externalServices.size(); + int availableCount = (int) externalServices.stream().filter(s -> TheiaCloudServiceUtil.isUnusedService(s)) + .count(); + span.setData("pool.total_capacity", totalCapacity); + span.setData("pool.available", availableCount); + + // Check if session already has a reservation + Optional alreadyReservedExternal = TheiaCloudServiceUtil.getServiceOwnedBySession(sessionName, + sessionUID, externalServices); + Optional alreadyReservedInternal = TheiaCloudServiceUtil.getServiceOwnedBySession(sessionName, + sessionUID, internalServices); + + if (alreadyReservedExternal.isPresent() && alreadyReservedInternal.isPresent()) { + Integer extId = parseInstanceId(alreadyReservedExternal.get()); + Integer intId = parseInstanceId(alreadyReservedInternal.get()); + if (extId == null || intId == null || !extId.equals(intId)) { + LOGGER.error(formatLogMessage(correlationId, "Reservation mismatch for session " + sessionName)); + span.setTag("pool.outcome", "error");; + span.setTag("outcome", "error"); Tracing.finish(span, SpanStatus.INTERNAL_ERROR); + return ReservationResult.error(); + } + String deploymentName = TheiaCloudDeploymentUtil.getDeploymentName(appDef, extId); + span.setTag("reservation.type", "already_reserved"); + span.setTag("pool.outcome", "success"); + span.setData("instance_id", extId);; + Tracing.finishSuccess(span); + return ReservationResult.success(new PoolInstance(extId, alreadyReservedExternal.get(), + alreadyReservedInternal.get(), deploymentName)); + } + + // Build instance maps + Map externalByInstance = buildInstanceMap(externalServices); + Map internalByInstance = buildInstanceMap(internalServices); + + // Handle partial reservation + if (alreadyReservedExternal.isPresent() ^ alreadyReservedInternal.isPresent()) { + span.setTag("reservation.type", "partial_recovery"); + ReservationResult result = handlePartialReservation(session, appDef, alreadyReservedExternal, + alreadyReservedInternal, externalByInstance, internalByInstance, correlationId); + span.setTag("pool.outcome", result.getOutcome().name().toLowerCase()); + Integer instanceId = result.getInstance().map(PoolInstance::getInstanceId).orElse(null); + if (instanceId != null) { + span.setData("instance_id", instanceId); + } + String outcome = result.getOutcome() == ReservationOutcome.SUCCESS ? "success" + : result.getOutcome().name().toLowerCase(); + SpanStatus status = result.getOutcome() == ReservationOutcome.SUCCESS ? SpanStatus.OK + : result.getOutcome() == ReservationOutcome.NO_CAPACITY ? SpanStatus.RESOURCE_EXHAUSTED + : SpanStatus.INTERNAL_ERROR; + span.setTag("outcome", outcome); + Tracing.finish(span, status); + return result; + } + + // Find available instance + List availableIds = externalByInstance.entrySet().stream() + .filter(e -> TheiaCloudServiceUtil.isUnusedService(e.getValue())).filter(e -> { + Service internal = internalByInstance.get(e.getKey()); + return internal != null && TheiaCloudServiceUtil.isUnusedService(internal); + }).map(Map.Entry::getKey).sorted(Comparator.naturalOrder()).collect(Collectors.toList()); + + if (availableIds.isEmpty()) { + LOGGER.info(formatLogMessage(correlationId, "No prewarmed instances available")); + span.setTag("reservation.type", "no_capacity"); + span.setTag("pool.outcome", "no_capacity");; + span.setTag("outcome", "no_capacity"); Tracing.finish(span, SpanStatus.RESOURCE_EXHAUSTED); + return ReservationResult.noCapacity(); + } + + int chosenInstance = availableIds.get(0); + Service chosenExternal = externalByInstance.get(chosenInstance); + Service chosenInternal = internalByInstance.get(chosenInstance); + + span.setTag("reservation.type", "new"); + span.setData("chosen_instance", chosenInstance); + + // Reserve both services + ISpan reserveExtSpan = span.startChild("pool.reserve_external_service", "Reserve external service"); + try { + reserveService(chosenExternal, sessionName, sessionUID, correlationId); + Tracing.finishSuccess(reserveExtSpan); + } catch (KubernetesClientException e) { + LOGGER.error(formatLogMessage(correlationId, "Failed to reserve external service"), e); + Tracing.finishError(reserveExtSpan, e); + span.setTag("pool.outcome", "error");; + span.setTag("outcome", "error"); Tracing.finish(span, SpanStatus.INTERNAL_ERROR); + return ReservationResult.error(); + } + + ISpan reserveIntSpan = span.startChild("pool.reserve_internal_service", "Reserve internal service"); + try { + reserveService(chosenInternal, sessionName, sessionUID, correlationId); + Tracing.finishSuccess(reserveIntSpan); + } catch (KubernetesClientException e) { + LOGGER.error(formatLogMessage(correlationId, "Failed to reserve internal service"), e); + Tracing.finishError(reserveIntSpan, e); + rollbackReservation(chosenExternal, sessionName, sessionUID, correlationId); + span.setTag("pool.outcome", "error");; + span.setTag("outcome", "error"); Tracing.finish(span, SpanStatus.INTERNAL_ERROR); + return ReservationResult.error(); + } + + String deploymentName = TheiaCloudDeploymentUtil.getDeploymentName(appDef, chosenInstance); + span.setTag("pool.outcome", "success"); + span.setData("instance_id", chosenInstance);; + Tracing.finishSuccess(span); + return ReservationResult + .success(new PoolInstance(chosenInstance, chosenExternal, chosenInternal, deploymentName)); + + } catch (Exception e) { + Tracing.finishError(span, e); + throw e; + } + } + + /** + * Completes the session setup after reservation. Adds session labels, reserves deployment, configures email config. + */ + public boolean completeSessionSetup(Session session, AppDefinition appDef, PoolInstance instance, + String correlationId) { + + ISpan span = Tracing.childSpan("pool.complete_setup", "Complete session setup"); + + span.setData("session_name", session.getSpec().getName()); + span.setData("instance_id", instance.getInstanceId()); + + try { + String sessionName = session.getMetadata().getName(); + String sessionUID = session.getMetadata().getUid(); + + Map sessionLabels = LabelsUtil.createSessionLabels(session, appDef); + + // Add labels to services + ISpan labelSpan = span.startChild("pool.add_session_labels", "Add session labels to services"); + try { + addSessionLabelsToService(instance.getExternalService(), sessionLabels, correlationId); + addSessionLabelsToService(instance.getInternalService(), sessionLabels, correlationId); + Tracing.finishSuccess(labelSpan); + } catch (KubernetesClientException e) { + LOGGER.error(formatLogMessage(correlationId, "Failed to add session labels to services"), e); + Tracing.finishError(labelSpan, e); + span.setTag("outcome", "failure"); Tracing.finish(span, SpanStatus.INTERNAL_ERROR); + return false; + } + + // Reserve deployment + ISpan deploySpan = span.startChild("pool.reserve_deployment", "Reserve deployment"); + try { + client.kubernetes().inNamespace(session.getMetadata().getNamespace()).apps().deployments().withName(instance.getDeploymentName()).edit( + d -> TheiaCloudHandlerUtil.addOwnerReferenceToItem(correlationId, sessionName, sessionUID, d)); + Tracing.finishSuccess(deploySpan); + } catch (KubernetesClientException e) { + LOGGER.error(formatLogMessage(correlationId, "Failed to reserve deployment"), e); + Tracing.finishError(deploySpan, e); + span.setTag("outcome", "failure"); Tracing.finish(span, SpanStatus.INTERNAL_ERROR); + return false; + } + + // Configure email config (if using Keycloak) + if (arguments.isUseKeycloak()) { + ISpan emailSpan = span.startChild("pool.configure_email", "Configure email config"); + String emailConfigName = TheiaCloudConfigMapUtil.getEmailConfigName(appDef, instance.getInstanceId()); + try { + client.kubernetes().inNamespace(session.getMetadata().getNamespace()).configMaps().withName(emailConfigName).edit(cm -> { + cm.setData(Collections.singletonMap(AddedHandlerUtil.FILENAME_AUTHENTICATED_EMAILS_LIST, + session.getSpec().getUser())); + return cm; + }); + Tracing.finishSuccess(emailSpan); + } catch (KubernetesClientException e) { + LOGGER.error(formatLogMessage(correlationId, "Failed to configure email config"), e); + Tracing.finishError(emailSpan, e); + span.setTag("outcome", "failure"); Tracing.finish(span, SpanStatus.INTERNAL_ERROR); + return false; + } + + // Trigger pod refresh + ISpan refreshSpan = span.startChild("pool.refresh_pods", "Trigger pod refresh"); + refreshPods(instance.getDeploymentName(), correlationId); + Tracing.finishSuccess(refreshSpan); + } + + Tracing.finishSuccess(span); + return true; + + } catch (Exception e) { + Tracing.finishError(span, e); + throw e; + } + } + + /** + * Releases an instance back to the pool (used when session ends). Removes session ownership and clears + * session-specific data. + */ + public boolean releaseInstance(Session session, AppDefinition appDef, String correlationId) { + String sessionName = session.getSpec().getName(); + String appDefName = appDef.getSpec().getName(); + + ISpan span = Tracing.childSpan("pool.release_instance", "Release pool instance"); + + span.setTag("app_definition", appDefName); + span.setData("session_name", sessionName); + + LOGGER.info(formatLogMessage(correlationId, "Releasing instance for session " + sessionName)); + + try { + String sessionUID = session.getMetadata().getUid(); + + // Find services owned by this session + Map sessionLabels = LabelsUtil.createSessionLabels(session, appDef); + List services = findServicesByLabels(sessionLabels, correlationId); + + if (services.isEmpty()) { + LOGGER.error(formatLogMessage(correlationId, + "No services found for session " + session.getSpec().getName())); + span.setTag("error.reason", "no_services_found"); + span.setTag("outcome", "failure"); Tracing.finish(span, SpanStatus.NOT_FOUND); + return false; + } + + List externalServices = services.stream().filter(s -> !s.getMetadata().getName().endsWith("-int")) + .collect(Collectors.toList()); + List internalServices = services.stream().filter(s -> s.getMetadata().getName().endsWith("-int")) + .collect(Collectors.toList()); + + if (externalServices.size() != 1 || internalServices.size() != 1) { + LOGGER.error(formatLogMessage(correlationId, "Expected 1 external and 1 internal service, found " + + externalServices.size() + " and " + internalServices.size())); + span.setTag("error.reason", "service_count_mismatch"); + span.setData("external_count", externalServices.size()); + span.setData("internal_count", internalServices.size()); + span.setTag("outcome", "failure"); Tracing.finish(span, SpanStatus.INTERNAL_ERROR); + return false; + } + + Service externalService = externalServices.get(0); + Service internalService = internalServices.get(0); + + Integer instanceId = parseInstanceId(externalService); + if (instanceId == null) { + LOGGER.error(formatLogMessage(correlationId, "Cannot determine instance ID from service")); + span.setTag("error.reason", "cannot_parse_instance_id"); + span.setTag("outcome", "failure"); Tracing.finish(span, SpanStatus.INTERNAL_ERROR); + return false; + } + span.setData("instance_id", instanceId); + + boolean success = true; + + // Clean up services + ISpan svcSpan = span.startChild("pool.cleanup_services", "Clean up services"); + success &= cleanupService(externalService, sessionName, sessionUID, correlationId); + success &= cleanupService(internalService, sessionName, sessionUID, correlationId); + svcSpan.setTag("outcome", success ? "success" : "failure"); Tracing.finish(svcSpan, success ? SpanStatus.OK : SpanStatus.INTERNAL_ERROR); + + // Clean up deployment + ISpan deploySpan = span.startChild("pool.cleanup_deployment", "Clean up deployment"); + String deploymentName = TheiaCloudDeploymentUtil.getDeploymentName(appDef, instanceId); + try { + client.kubernetes().apps().deployments().withName(deploymentName).edit(d -> TheiaCloudHandlerUtil + .removeOwnerReferenceFromItem(correlationId, sessionName, sessionUID, d)); + Tracing.finishSuccess(deploySpan); + } catch (KubernetesClientException e) { + LOGGER.error(formatLogMessage(correlationId, "Failed to clean up deployment"), e); + Tracing.finishError(deploySpan, e); + success = false; + } + + // Clear email config + if (arguments.isUseKeycloak()) { + ISpan emailSpan = span.startChild("pool.clear_email_config", "Clear email config"); + String emailConfigName = TheiaCloudConfigMapUtil.getEmailConfigName(appDef, instanceId); + try { + client.kubernetes().configMaps().withName(emailConfigName).edit(cm -> { + cm.setData(Collections.singletonMap(AddedHandlerUtil.FILENAME_AUTHENTICATED_EMAILS_LIST, null)); + return cm; + }); + Tracing.finishSuccess(emailSpan); + } catch (KubernetesClientException e) { + LOGGER.error(formatLogMessage(correlationId, "Failed to clear email config"), e); + Tracing.finishError(emailSpan, e); + success = false; + } + } + + // Delete pod to reset state + ISpan podSpan = span.startChild("pool.delete_pod", "Delete pod to reset state"); + deletePod(deploymentName, correlationId); + Tracing.finishSuccess(podSpan); + + span.setTag("outcome", success ? "success" : "failure"); Tracing.finish(span, success ? SpanStatus.OK : SpanStatus.INTERNAL_ERROR); + return success; + + } catch (Exception e) { + Tracing.finishError(span, e); + throw e; + } + } + + // ========== Helper Methods ========== + + private Map buildInstanceMap(List services) { + Map map = new HashMap<>(); + for (Service s : services) { + Integer id = parseInstanceId(s); + if (id != null) { + map.putIfAbsent(id, s); + } + } + return map; + } + + private Integer parseInstanceId(Service service) { + String id = TheiaCloudK8sUtil.extractIdFromName(service.getMetadata()); + if (id == null || id.isBlank()) { + return null; + } + try { + return Integer.valueOf(id); + } catch (NumberFormatException e) { + return null; + } + } + + private ReservationResult handlePartialReservation(Session session, AppDefinition appDef, + Optional reservedExternal, Optional reservedInternal, Map externalMap, + Map internalMap, String correlationId) { + + String sessionName = session.getMetadata().getName(); + String sessionUID = session.getMetadata().getUid(); + + Service reserved = reservedExternal.orElseGet(reservedInternal::get); + Integer instance = parseInstanceId(reserved); + if (instance == null) { + LOGGER.error(formatLogMessage(correlationId, "Cannot parse instance from partially reserved service")); + rollbackReservation(reserved, sessionName, sessionUID, correlationId); + return ReservationResult.error(); + } + + Service counterpart = reservedExternal.isPresent() ? internalMap.get(instance) : externalMap.get(instance); + if (counterpart == null || !TheiaCloudServiceUtil.isUnusedService(counterpart)) { + LOGGER.warn(formatLogMessage(correlationId, "Partial reservation cannot be completed, rolling back")); + rollbackReservation(reserved, sessionName, sessionUID, correlationId); + return ReservationResult.noCapacity(); + } + + try { + reserveService(counterpart, sessionName, sessionUID, correlationId); + } catch (KubernetesClientException e) { + LOGGER.error(formatLogMessage(correlationId, "Failed to complete partial reservation"), e); + rollbackReservation(reserved, sessionName, sessionUID, correlationId); + return ReservationResult.error(); + } + + Service ext = reservedExternal.orElse(counterpart); + Service in = reservedInternal.orElse(counterpart); + String deploymentName = TheiaCloudDeploymentUtil.getDeploymentName(appDef, instance); + return ReservationResult.success(new PoolInstance(instance, ext, in, deploymentName)); + } + + private void reserveService(Service service, String sessionName, String sessionUID, String correlationId) { + client.services().inNamespace(client.namespace()).withName(service.getMetadata().getName()) + .edit(s -> TheiaCloudHandlerUtil.addOwnerReferenceToItem(correlationId, sessionName, sessionUID, s)); + } + + private void rollbackReservation(Service service, String sessionName, String sessionUID, String correlationId) { + try { + client.services().inNamespace(client.namespace()).withName(service.getMetadata().getName()).edit(s -> { + TheiaCloudHandlerUtil.removeOwnerReferenceFromItem(correlationId, sessionName, sessionUID, s); + return s; + }); + } catch (KubernetesClientException e) { + LOGGER.warn(formatLogMessage(correlationId, "Failed to rollback reservation"), e); + } + } + + private void addSessionLabelsToService(Service service, Map labels, String correlationId) { + client.services().inNamespace(client.namespace()).withName(service.getMetadata().getName()).edit(s -> { + Map existing = s.getMetadata().getLabels(); + if (existing == null) { + existing = new HashMap<>(); + s.getMetadata().setLabels(existing); + } + existing.putAll(labels); + return s; + }); + } + + private boolean cleanupService(Service service, String sessionName, String sessionUID, String correlationId) { + int attempts = 0; + while (attempts < 3) { + try { + client.services().inNamespace(service.getMetadata().getNamespace()).withName(service.getMetadata().getName()).edit(s -> { + TheiaCloudHandlerUtil.removeOwnerReferenceFromItem(correlationId, sessionName, sessionUID, s); + if (s.getMetadata().getLabels() != null) { + s.getMetadata().getLabels().keySet().removeAll(LabelsUtil.getSessionSpecificLabelKeys()); + } + return s; + }); + return true; + } catch (KubernetesClientException e) { + attempts++; + if (attempts >= 3) { + LOGGER.error(formatLogMessage(correlationId, "Failed to cleanup service after 3 attempts"), e); + return false; + } + } + } + return false; + } + + private List findServicesByLabels(Map labels, String correlationId) { + String labelSelector = labels.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()) + .collect(Collectors.joining(",")); + return client.kubernetes().services().inNamespace(client.namespace()).withLabelSelector(labelSelector).list() + .getItems(); + } + + private void refreshPods(String deploymentName, String correlationId) { + try { + // Get deployment to extract selector labels + Deployment deployment = client.kubernetes().apps().deployments().inNamespace(client.namespace()) + .withName(deploymentName).get(); + if (deployment == null || deployment.getSpec() == null || deployment.getSpec().getSelector() == null + || deployment.getSpec().getSelector().getMatchLabels() == null) { + return; + } + + // Build label selector from deployment's matchLabels + Map selectorLabels = deployment.getSpec().getSelector().getMatchLabels(); + String labelSelector = selectorLabels.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()) + .collect(Collectors.joining(",")); + + // Query pods using label selector + client.kubernetes().pods().inNamespace(client.namespace()).withLabelSelector(labelSelector).list() + .getItems().forEach(pod -> { + // Null-check metadata and ownerReferences before streaming + if (pod.getMetadata() == null || pod.getMetadata().getOwnerReferences() == null) { + return; + } + // Check if pod belongs to this deployment via owner references + if (pod.getMetadata().getOwnerReferences().stream() + .anyMatch(or -> or.getName().startsWith(deploymentName))) { + // Null-check and initialize annotations before mutating + if (pod.getMetadata().getAnnotations() == null) { + pod.getMetadata().setAnnotations(new HashMap<>()); + } + pod.getMetadata().getAnnotations().put(EAGER_START_REFRESH_ANNOTATION, + Instant.now().toString()); + PodResource podResource = client.pods().inNamespace(client.namespace()) + .withName(pod.getMetadata().getName()); + podResource.edit(p -> pod); + } + }); + } catch (KubernetesClientException e) { + LOGGER.warn(formatLogMessage(correlationId, "Failed to refresh pods"), e); + } + } + + private void deletePod(String deploymentName, String correlationId) { + try { + Optional pod = client.kubernetes().pods().list().getItems().stream() + .filter(p -> p.getMetadata().getName().startsWith(deploymentName)).findAny(); + if (pod.isPresent()) { + LOGGER.info(formatLogMessage(correlationId, "Deleting pod " + pod.get().getMetadata().getName())); + client.pods().withName(pod.get().getMetadata().getName()).delete(); + } + } catch (KubernetesClientException e) { + LOGGER.error(formatLogMessage(correlationId, "Failed to delete pod"), e); + } + } + + // ========== PVC Management ========== + + private static final String TEMPLATE_PERSISTENTVOLUMECLAIM_YAML = "/templatePersistentVolumeClaim.yaml"; + private static final int PVC_DELETION_WAIT_ATTEMPTS = 30; + private static final long PVC_DELETION_WAIT_MILLIS = 1000; + + static String getInstancePvcName(AppDefinition appDef, int instanceId) { + return NamingUtil.createNameWithSuffix(appDef, instanceId, "pvc"); + } + + /** + * Creates a PersistentVolumeClaim for an eager-start pool instance. The PVC is owned by the AppDefinition so it + * survives session lifecycle and is cleaned up when the AppDefinition is deleted or the instance is scaled down. + * + * @return the PVC name if created successfully, or empty if creation fails + */ + private Optional createInstancePvc(AppDefinition appDef, int instanceId, String correlationId) { + if (!SidecarConfig.requiresSharedWorkspace(appDef)) { + return Optional.empty(); + } + + String pvcName = getInstancePvcName(appDef, instanceId); + + Optional existingPvc = client + .persistentVolumeClaimsClient().get(pvcName); + if (existingPvc.isPresent() + && existingPvc.get().getMetadata() != null + && existingPvc.get().getMetadata().getDeletionTimestamp() == null) { + LOGGER.info(formatLogMessage(correlationId, + "PVC " + pvcName + " already exists for instance " + instanceId)); + return Optional.of(pvcName); + } + + if (existingPvc.isPresent()) { + LOGGER.warn(formatLogMessage(correlationId, + "PVC " + pvcName + " is terminating for instance " + instanceId + + ". Waiting for deletion before recreation.")); + if (!waitUntilPvcDeleted(pvcName, correlationId)) { + LOGGER.error(formatLogMessage(correlationId, + "PVC " + pvcName + " is still present/terminating after waiting. Skipping recreation.")); + return Optional.empty(); + } + } + + ISpan span = Tracing.childSpan("pool.create_pvc", "Create instance PVC"); + span.setData("pvc_name", pvcName); + span.setData("instance_id", instanceId); + + LOGGER.info(formatLogMessage(correlationId, + "Creating PVC " + pvcName + " for pool instance " + instanceId)); + + try { + Map replacements = new LinkedHashMap<>(); + replacements.put(DefaultPersistentVolumeTemplateReplacements.PLACEHOLDER_PERSISTENTVOLUMENAME, pvcName); + replacements.put(TheiaCloudHandlerUtil.PLACEHOLDER_NAMESPACE, client.namespace()); + replacements.put(DefaultPersistentVolumeTemplateReplacements.PLACEHOLDER_LABEL_WORKSPACE_NAME, + appDef.getSpec().getName() + "-instance-" + instanceId); + replacements.put(DefaultPersistentVolumeTemplateReplacements.PLACEHOLDER_STORAGE_CLASS_NAME, + arguments.getStorageClassName() != null ? arguments.getStorageClassName() : ""); + replacements.put(DefaultPersistentVolumeTemplateReplacements.PLACEHOLDER_REQUESTED_STORAGE, + arguments.getRequestedStorage() != null ? arguments.getRequestedStorage() + : DefaultPersistentVolumeTemplateReplacements.DEFAULT_REQUESTED_STORAGE); + replacements.put(DefaultPersistentVolumeTemplateReplacements.PLACEHOLDER_ACCESS_MODE, + DefaultPersistentVolumeTemplateReplacements.READ_WRITE_MANY); + + String pvcYaml = JavaResourceUtil.readResourceAndReplacePlaceholders( + TEMPLATE_PERSISTENTVOLUMECLAIM_YAML, replacements, correlationId); + + Optional result = + client.persistentVolumeClaimsClient().loadAndCreate(correlationId, pvcYaml, + claim -> claim.addOwnerReference(appDef)); + + if (result.isPresent()) { + Tracing.finishSuccess(span); + LOGGER.info(formatLogMessage(correlationId, + "Successfully created PVC " + pvcName)); + return Optional.of(pvcName); + } else { + span.setTag("outcome", "failure"); + Tracing.finish(span, SpanStatus.INTERNAL_ERROR); + LOGGER.error(formatLogMessage(correlationId, + "Failed to create PVC " + pvcName)); + return Optional.empty(); + } + + } catch (IOException | URISyntaxException e) { + LOGGER.error(formatLogMessage(correlationId, + "Error loading PVC template for instance " + instanceId), e); + Tracing.finishError(span, e); + return Optional.empty(); + } catch (RuntimeException e) { + LOGGER.error(formatLogMessage(correlationId, + "Unexpected error creating PVC for instance " + instanceId), e); + Tracing.finishError(span, e); + return Optional.empty(); + } + } + + private void deleteInstancePvc(AppDefinition appDef, int instanceId, String correlationId) { + String pvcName = getInstancePvcName(appDef, instanceId); + + LOGGER.info(formatLogMessage(correlationId, + "Deleting PVC " + pvcName + " for pool instance " + instanceId)); + + try { + client.kubernetes().persistentVolumeClaims() + .inNamespace(client.namespace()) + .withName(pvcName) + .delete(); + LOGGER.info(formatLogMessage(correlationId, "Deleted PVC " + pvcName)); + } catch (KubernetesClientException e) { + LOGGER.warn(formatLogMessage(correlationId, "Failed to delete PVC " + pvcName), e); + } + } + + private boolean waitUntilPvcDeleted(String pvcName, String correlationId) { + for (int attempt = 1; attempt <= PVC_DELETION_WAIT_ATTEMPTS; attempt++) { + if (client.persistentVolumeClaimsClient().get(pvcName).isEmpty()) { + return true; + } + + try { + Thread.sleep(PVC_DELETION_WAIT_MILLIS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOGGER.warn(formatLogMessage(correlationId, + "Interrupted while waiting for PVC deletion: " + pvcName), e); + return false; + } + } + + return client.persistentVolumeClaimsClient().get(pvcName).isEmpty(); + } +} diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/pv/MinikubePersistentVolumeCreator.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/pv/MinikubePersistentVolumeCreator.java index b8bc52711..2fac66b33 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/pv/MinikubePersistentVolumeCreator.java +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/pv/MinikubePersistentVolumeCreator.java @@ -64,8 +64,7 @@ public Optional createAndApplyPersistentVolume(String correlat e); return Optional.empty(); } - return client.persistentVolumesClient().loadAndCreate(correlationId, persistentVolumeYaml, - volume -> volume.addOwnerReference(workspace)); + return client.persistentVolumesClient().loadAndCreate(correlationId, persistentVolumeYaml); } @Override diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/replacements/DefaultDeploymentTemplateReplacements.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/replacements/DefaultDeploymentTemplateReplacements.java index 78a3508b3..aba4a6b98 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/replacements/DefaultDeploymentTemplateReplacements.java +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/replacements/DefaultDeploymentTemplateReplacements.java @@ -27,6 +27,7 @@ import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinition; import org.eclipse.theia.cloud.common.k8s.resource.session.Session; +import org.eclipse.theia.cloud.common.util.DataBridgeUtil; import org.eclipse.theia.cloud.operator.TheiaCloudOperatorArguments; import org.eclipse.theia.cloud.operator.ingress.IngressPathProvider; import org.eclipse.theia.cloud.operator.util.TheiaCloudConfigMapUtil; @@ -49,6 +50,8 @@ public class DefaultDeploymentTemplateReplacements implements DeploymentTemplate public static final String PLACEHOLDER_MEMORY_REQUESTS = "placeholder-memory-requests"; public static final String PLACEHOLDER_UID = "placeholder-uid"; + public static final String PLACEHOLDER_ENV_SERVICE_AUTH_TOKEN = "placeholder-env-service-auth-token"; + /** @deprecated Use PLACEHOLDER_ENV_SERVICE_AUTH_TOKEN instead */ public static final String PLACEHOLDER_ENV_APP_ID = "placeholder-env-app-id"; public static final String PLACEHOLDER_ENV_SERVICE_URL = "placeholder-env-service-url"; public static final String PLACEHOLDER_ENV_SESSION_UID = "placeholder-env-session-uid"; @@ -65,6 +68,14 @@ public class DefaultDeploymentTemplateReplacements implements DeploymentTemplate public static final String PLACEHOLDER_ENABLE_ACTIVITY_TRACKER = "placeholder-enable-activity-tracker"; public static final String PLACEHOLDER_OAUTH2_PROXY_VERSION = "placeholder-oauth2-proxy-version"; + public static final String PLACEHOLDER_ENV_BUILD_CACHE_ENABLED = "placeholder-build-cache-enabled"; + public static final String PLACEHOLDER_ENV_BUILD_CACHE_URL = "placeholder-build-cache-url"; + public static final String PLACEHOLDER_ENV_BAZEL_BUILD_CACHE_URL = "placeholder-bazel-build-cache-url"; + public static final String PLACEHOLDER_ENV_BUILD_CACHE_PUSH = "placeholder-build-cache-push"; + + public static final String PLACEHOLDER_ENV_DEPENDENCY_CACHE_ENABLED = "placeholder-dependency-cache-enabled"; + public static final String PLACEHOLDER_ENV_DEPENDENCY_CACHE_URL = "placeholder-dependency-cache-url"; + protected static final String DEFAULT_UID = "1000"; @Inject @@ -126,7 +137,9 @@ protected Map getEnvironmentVariables(AppDefinition appDefinitio protected Map getEnvironmentVariables(AppDefinition appDefinition, Optional session) { Map environmentVariables = new LinkedHashMap(); - environmentVariables.put(PLACEHOLDER_ENV_APP_ID, arguments.getAppId()); + // Set both new and old placeholders for backwards compatibility + environmentVariables.put(PLACEHOLDER_ENV_SERVICE_AUTH_TOKEN, arguments.getServiceAuthToken()); + environmentVariables.put(PLACEHOLDER_ENV_APP_ID, arguments.getServiceAuthToken()); environmentVariables.put(PLACEHOLDER_ENV_SERVICE_URL, arguments.getServiceUrl()); environmentVariables.put(PLACEHOLDER_ENV_SESSION_UID, session.map(s -> s.getMetadata().getUid()).orElse("")); environmentVariables.put(PLACEHOLDER_ENV_SESSION_NAME, session.map(s -> s.getSpec().getName()).orElse("")); @@ -145,6 +158,36 @@ protected Map getEnvironmentVariables(AppDefinition appDefinitio environmentVariables.put(PLACEHOLDER_ENV_SESSION_KEYCLOAK_CLIENT_ID, ""); } + boolean gradleCacheActive = arguments.isEnableBuildCaching() && arguments.getBuildCacheUrl() != null + && !arguments.getBuildCacheUrl().trim().isEmpty(); + boolean bazelCacheActive = arguments.isEnableBuildCaching() && arguments.getBazelBuildCacheUrl() != null + && !arguments.getBazelBuildCacheUrl().trim().isEmpty(); + + if (gradleCacheActive || bazelCacheActive) { + environmentVariables.put(PLACEHOLDER_ENV_BUILD_CACHE_ENABLED, "true"); + environmentVariables.put(PLACEHOLDER_ENV_BUILD_CACHE_URL, + gradleCacheActive ? arguments.getBuildCacheUrl().trim() : ""); + environmentVariables.put(PLACEHOLDER_ENV_BAZEL_BUILD_CACHE_URL, + bazelCacheActive ? arguments.getBazelBuildCacheUrl().trim() : ""); + environmentVariables.put(PLACEHOLDER_ENV_BUILD_CACHE_PUSH, + arguments.isEnableBuildCachePush() ? "true" : "false"); + } else { + environmentVariables.put(PLACEHOLDER_ENV_BUILD_CACHE_ENABLED, "false"); + environmentVariables.put(PLACEHOLDER_ENV_BUILD_CACHE_URL, ""); + environmentVariables.put(PLACEHOLDER_ENV_BAZEL_BUILD_CACHE_URL, ""); + environmentVariables.put(PLACEHOLDER_ENV_BUILD_CACHE_PUSH, "false"); + } + + if (arguments.isEnableDependencyCaching() && arguments.getDependencyCacheUrl() != null + && !arguments.getDependencyCacheUrl().trim().isEmpty()) { + environmentVariables.put(PLACEHOLDER_ENV_DEPENDENCY_CACHE_ENABLED, "true"); + environmentVariables.put(PLACEHOLDER_ENV_DEPENDENCY_CACHE_URL, + arguments.getDependencyCacheUrl().trim()); + } else { + environmentVariables.put(PLACEHOLDER_ENV_DEPENDENCY_CACHE_ENABLED, "false"); + environmentVariables.put(PLACEHOLDER_ENV_DEPENDENCY_CACHE_URL, ""); + } + if (arguments.isEnableMonitor()) { if (appDefinition.getSpec().getMonitor() != null && appDefinition.getSpec().getMonitor().getPort() > 0) { String port = String.valueOf(appDefinition.getSpec().getMonitor().getPort()); @@ -165,6 +208,35 @@ protected Map getEnvironmentVariables(AppDefinition appDefinitio } else { environmentVariables.put(TheiaCloudHandlerUtil.PLACEHOLDER_MONITOR_PORT, ""); } + + // Handle data bridge port and strategy + // Data bridge is only enabled for eager sessions (session.isEmpty()). + // For lazy sessions, environment variables are set directly on the session resource. + boolean dataBridgeEnabled = DataBridgeUtil.isDataBridgeEnabled(appDefinition.getSpec()) && session.isEmpty(); + if (!dataBridgeEnabled) { + environmentVariables.put(TheiaCloudHandlerUtil.PLACEHOLDER_DATA_BRIDGE_CONTAINER_PORT, ""); + environmentVariables.put(TheiaCloudHandlerUtil.PLACEHOLDER_DATA_BRIDGE_ENV_PORT, ""); + environmentVariables.put(TheiaCloudHandlerUtil.PLACEHOLDER_DATA_BRIDGE_ENABLED, "0"); + environmentVariables.put(TheiaCloudHandlerUtil.PLACEHOLDER_DATA_BRIDGE_STRATEGY, ""); + } else { + int portNumber = DataBridgeUtil.getDataBridgePort(appDefinition.getSpec()); + if (portNumber == appDefinition.getSpec().getPort() || (appDefinition.getSpec().getMonitor() != null + && portNumber == appDefinition.getSpec().getMonitor().getPort())) { + // Just remove the placeholder, otherwise the port would be duplicate + environmentVariables.put(TheiaCloudHandlerUtil.PLACEHOLDER_DATA_BRIDGE_CONTAINER_PORT, ""); + environmentVariables.put(TheiaCloudHandlerUtil.PLACEHOLDER_DATA_BRIDGE_ENV_PORT, ""); + } else { + // Replace the placeholder with the port information + environmentVariables.put(TheiaCloudHandlerUtil.PLACEHOLDER_DATA_BRIDGE_CONTAINER_PORT, + "- containerPort: " + portNumber + "\n" + " name: data-bridge"); + // Set the environment variable value + environmentVariables.put(TheiaCloudHandlerUtil.PLACEHOLDER_DATA_BRIDGE_ENV_PORT, + String.valueOf(portNumber)); + } + environmentVariables.put(TheiaCloudHandlerUtil.PLACEHOLDER_DATA_BRIDGE_ENABLED, "1"); + environmentVariables.put(TheiaCloudHandlerUtil.PLACEHOLDER_DATA_BRIDGE_STRATEGY, "data-bridge"); + } + return environmentVariables; } diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/replacements/DefaultPersistentVolumeTemplateReplacements.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/replacements/DefaultPersistentVolumeTemplateReplacements.java index 8bc019a78..74022f53f 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/replacements/DefaultPersistentVolumeTemplateReplacements.java +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/replacements/DefaultPersistentVolumeTemplateReplacements.java @@ -18,6 +18,7 @@ import java.util.LinkedHashMap; import java.util.Map; +import org.eclipse.theia.cloud.common.k8s.client.TheiaCloudClient; import org.eclipse.theia.cloud.common.k8s.resource.workspace.Workspace; import org.eclipse.theia.cloud.common.util.WorkspaceUtil; import org.eclipse.theia.cloud.operator.TheiaCloudOperatorArguments; @@ -31,12 +32,18 @@ public class DefaultPersistentVolumeTemplateReplacements implements PersistentVo public static final String PLACEHOLDER_LABEL_WORKSPACE_NAME = "placeholder-label-workspace-name"; public static final String PLACEHOLDER_STORAGE_CLASS_NAME = "placeholder-storage-class-name"; public static final String PLACEHOLDER_REQUESTED_STORAGE = "placeholder-requested-storage"; + public static final String PLACEHOLDER_ACCESS_MODE = "placeholder-access-mode"; public static final String DEFAULT_REQUESTED_STORAGE = "250Mi"; + public static final String READ_WRITE_ONCE = "ReadWriteOnce"; + public static final String READ_WRITE_MANY = "ReadWriteMany"; @Inject TheiaCloudOperatorArguments arguments; + @Inject + TheiaCloudClient client; + @Override public Map getPersistentVolumeReplacements(String namespace, Workspace workspace) { Map replacements = new LinkedHashMap(); @@ -57,7 +64,20 @@ public Map getPersistentVolumeClaimReplacements(String namespace replacements.put(PLACEHOLDER_STORAGE_CLASS_NAME, orEmpty(arguments.getStorageClassName())); replacements.put(PLACEHOLDER_REQUESTED_STORAGE, orDefault(arguments.getRequestedStorage(), DEFAULT_REQUESTED_STORAGE)); + replacements.put(PLACEHOLDER_ACCESS_MODE, getAccessMode(workspace)); return replacements; } + protected String getAccessMode(Workspace workspace) { + String appDefinitionName = workspace.getSpec().getAppDefinition(); + if (appDefinitionName == null || appDefinitionName.isBlank()) { + return READ_WRITE_ONCE; + } + + return client.appDefinitions().get(appDefinitionName) + .filter(appDef -> appDef.getSpec() != null && appDef.getSpec().requiresSharedWorkspace()) + .map(appDef -> READ_WRITE_MANY) + .orElse(READ_WRITE_ONCE); + } + } diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/sidecar/SidecarConfig.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/sidecar/SidecarConfig.java new file mode 100644 index 000000000..ae0e474ca --- /dev/null +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/sidecar/SidecarConfig.java @@ -0,0 +1,108 @@ +/******************************************************************************** + * Copyright (C) 2025 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.operator.sidecar; + +import java.util.List; + +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinition; +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.SidecarSpec; + +/** + * Immutable configuration for a single sidecar container. + * Derived from {@link SidecarSpec} (v1beta11 CRD). + */ +public record SidecarConfig( + String name, + String image, + int containerPort, + List languages, + String cpuLimit, + String memoryLimit, + String cpuRequest, + String memoryRequest, + boolean mountWorkspace +) { + + /** Default port all sidecar containers should use internally. */ + public static final int DEFAULT_PORT = 5000; + + /** + * Creates a SidecarConfig from a v1beta11 {@link SidecarSpec}, applying defaults for missing fields. + */ + public static SidecarConfig fromSpec(SidecarSpec spec) { + return new SidecarConfig( + spec.getName(), + spec.getImage(), + spec.getPort() > 0 ? spec.getPort() : DEFAULT_PORT, + spec.getLanguages() != null ? spec.getLanguages() : List.of(), + spec.getCpuLimit() != null && !spec.getCpuLimit().isBlank() ? spec.getCpuLimit() : "500m", + spec.getMemoryLimit() != null && !spec.getMemoryLimit().isBlank() ? spec.getMemoryLimit() : "1Gi", + spec.getCpuRequest() != null && !spec.getCpuRequest().isBlank() ? spec.getCpuRequest() : "100m", + spec.getMemoryRequest() != null && !spec.getMemoryRequest().isBlank() ? spec.getMemoryRequest() : "256Mi", + spec.isMountWorkspace() + ); + } + + /** + * Returns the environment variable name for this sidecar's host. + * Example: sidecar name "lang-java" → "SIDECAR_LANG_JAVA_HOST" + */ + public String hostEnvVar() { + return "SIDECAR_" + name.toUpperCase().replace("-", "_") + "_HOST"; + } + + /** + * Returns the environment variable name for this sidecar's port. + * Example: sidecar name "lang-java" → "SIDECAR_LANG_JAVA_PORT" + */ + public String portEnvVar() { + return "SIDECAR_" + name.toUpperCase().replace("-", "_") + "_PORT"; + } + + /** + * Checks if the given AppDefinition has any sidecar configuration + * (v1beta11 sidecars list). + */ + public static boolean hasSidecars(AppDefinition appDef) { + if (appDef == null || appDef.getSpec() == null) { + return false; + } + + return appDef.getSpec().hasSidecars(); + } + + /** + * Checks if the given AppDefinition requires a shared workspace PVC. + */ + public static boolean requiresSharedWorkspace(AppDefinition appDef) { + if (appDef == null || appDef.getSpec() == null) { + return false; + } + + return appDef.getSpec().requiresSharedWorkspace(); + } + + /** + * Extracts the list of SidecarConfigs from an AppDefinition. + * Uses spec.sidecars only. + */ + public static List getSidecarConfigs(AppDefinition appDef) { + if (appDef == null || appDef.getSpec() == null) { + return List.of(); + } + + List sidecars = appDef.getSpec().getSidecars(); + if (sidecars != null && !sidecars.isEmpty()) { + return sidecars.stream().map(SidecarConfig::fromSpec).toList(); + } + + return List.of(); + } +} diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/sidecar/SidecarManager.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/sidecar/SidecarManager.java new file mode 100644 index 000000000..553a89297 --- /dev/null +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/sidecar/SidecarManager.java @@ -0,0 +1,472 @@ +/******************************************************************************** + * Copyright (C) 2025 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.operator.sidecar; + +import static org.eclipse.theia.cloud.common.util.LogMessageUtil.formatLogMessage; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinition; +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinitionSpec; +import org.eclipse.theia.cloud.common.k8s.resource.session.Session; +import org.eclipse.theia.cloud.common.tracing.Tracing; +import org.eclipse.theia.cloud.operator.util.TheiaCloudPersistentVolumeUtil; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import io.fabric8.kubernetes.api.model.Container; +import io.fabric8.kubernetes.api.model.EnvVar; +import io.fabric8.kubernetes.api.model.EnvVarBuilder; +import io.fabric8.kubernetes.api.model.PodSpec; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.sentry.ISpan; +import io.sentry.SpanStatus; + +@Singleton +public class SidecarManager { + + private static final Logger LOGGER = LogManager.getLogger(SidecarManager.class); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + @Inject + private SidecarResourceFactory factory; + + /** + * Returns true if the given AppDefinition has sidecar configuration. + */ + public boolean hasSidecars(AppDefinition appDef) { + return SidecarConfig.hasSidecars(appDef); + } + + /** + * Returns the list of sidecar configurations for the given AppDefinition. + */ + public List getSidecarConfigs(AppDefinition appDef) { + return SidecarConfig.getSidecarConfigs(appDef); + } + + /** + * Validates that all sidecar names are unique within the AppDefinition. + * + * @return true if valid or no sidecars configured, false when duplicates are detected + */ + public boolean validateUniqueSidecarNames(AppDefinition appDef, String correlationId) { + List configs = getSidecarConfigs(appDef); + if (configs.isEmpty()) { + return true; + } + + Set seen = new HashSet<>(); + Set duplicates = new LinkedHashSet<>(); + for (SidecarConfig config : configs) { + if (!seen.add(config.name())) { + duplicates.add(config.name()); + } + } + + if (!duplicates.isEmpty()) { + LOGGER.error(formatLogMessage(correlationId, + "[Sidecar] Duplicate sidecar names in AppDefinition '" + appDef.getSpec().getName() + + "': " + duplicates + ". Sidecar names must be unique.")); + return false; + } + + return true; + } + + /** + * Creates all sidecar resources (Service + Deployment per sidecar) for a lazy session. + * On failure of any sidecar, rolls back all previously created sidecars for this session. + * + * @return true if all sidecars were created (or none were needed), false on any failure + */ + public boolean createSidecars( + Session session, + AppDefinition appDef, + Optional pvcName, + String correlationId) { + + Optional> configsOpt = getValidatedSidecarConfigs(appDef, correlationId); + if (configsOpt.isEmpty()) { + return false; + } + List configs = configsOpt.get(); + if (configs.isEmpty()) { + LOGGER.debug(formatLogMessage(correlationId, "[Sidecar] No sidecars configured for " + appDef.getSpec().getName())); + return true; + } + + ISpan span = Tracing.childSpan("sidecar.create_all", "Create all sidecars"); + span.setData("session", session.getMetadata().getName()); + span.setData("sidecar_count", configs.size()); + + LOGGER.info(formatLogMessage(correlationId, + "[Sidecar] Creating " + configs.size() + " sidecar(s) for session " + session.getMetadata().getName())); + + List created = new ArrayList<>(); + try { + for (SidecarConfig config : configs) { + ISpan sidecarSpan = Tracing.childSpan(span, "sidecar.create", "Create sidecar " + config.name()); + sidecarSpan.setData("sidecar_name", config.name()); + sidecarSpan.setData("image", config.image()); + + boolean serviceOk = factory.createService(session, config, correlationId).isPresent(); + if (!serviceOk) { + sidecarSpan.setTag("outcome", "service_failed"); + Tracing.finish(sidecarSpan, SpanStatus.INTERNAL_ERROR); + throw new RuntimeException("Failed to create sidecar service for " + config.name()); + } + + boolean deploymentOk = factory.createDeployment(session, config, pvcName, appDef.getSpec(), correlationId).isPresent(); + if (!deploymentOk) { + sidecarSpan.setTag("outcome", "deployment_failed"); + Tracing.finish(sidecarSpan, SpanStatus.INTERNAL_ERROR); + throw new RuntimeException("Failed to create sidecar deployment for " + config.name()); + } + + Tracing.finishSuccess(sidecarSpan); + created.add(config); + } + + Tracing.finishSuccess(span); + LOGGER.info(formatLogMessage(correlationId, "[Sidecar] Successfully created all sidecars")); + return true; + + } catch (Exception e) { + LOGGER.error(formatLogMessage(correlationId, + "[Sidecar] Error creating sidecars, rolling back " + created.size() + " created sidecar(s)"), e); + + // Rollback: delete all resources created so far + try { + factory.deleteResources(session, correlationId); + } catch (Exception rollbackEx) { + LOGGER.error(formatLogMessage(correlationId, "[Sidecar] Rollback failed"), rollbackEx); + } + + Tracing.finishError(span, e); + return false; + } + } + + /** + * Injects sidecar environment variables into a Theia deployment that is being created. + * Adds per-sidecar HOST/PORT env vars plus a SIDECAR_CONFIG JSON array. + * + * Called at Theia deployment creation time (before the deployment is submitted to K8s), + * so the Theia pod starts with env vars already set — no restart needed. + */ + public void injectSidecarEnvVars( + Session session, + Deployment theiaDeployment, + AppDefinition appDef, + String correlationId) { + + Optional> configsOpt = getValidatedSidecarConfigs(appDef, correlationId); + if (configsOpt.isEmpty()) { + return; + } + List configs = configsOpt.get(); + if (configs.isEmpty()) { + LOGGER.debug(formatLogMessage(correlationId, "[Sidecar] No sidecars configured, skipping env var injection")); + return; + } + + ISpan span = Tracing.childSpan("sidecar.inject_env", "Inject sidecar env vars"); + span.setData("sidecar_count", configs.size()); + + try { + AppDefinitionSpec appDefSpec = appDef.getSpec(); + PodSpec podSpec = theiaDeployment.getSpec().getTemplate().getSpec(); + Container container = TheiaCloudPersistentVolumeUtil.getTheiaContainer(podSpec, appDefSpec); + + if (container == null) { + LOGGER.error(formatLogMessage(correlationId, "[Sidecar] Could not find Theia container in deployment")); + span.setTag("outcome", "container_not_found"); + Tracing.finish(span, SpanStatus.NOT_FOUND); + return; + } + + List envVars = container.getEnv(); + if (envVars == null) { + envVars = new ArrayList<>(); + container.setEnv(envVars); + } + + for (SidecarConfig config : configs) { + String serviceName = SidecarResourceFactory.getLazyResourceName(session, config); + envVars.add(new EnvVarBuilder() + .withName(config.hostEnvVar()) + .withValue(serviceName) + .build()); + envVars.add(new EnvVarBuilder() + .withName(config.portEnvVar()) + .withValue(String.valueOf(config.containerPort())) + .build()); + } + + String configJson = serializeSidecarConfigJson(configs, + config -> SidecarResourceFactory.getLazyResourceName(session, config)); + + envVars.add(new EnvVarBuilder() + .withName("SIDECAR_CONFIG") + .withValue(configJson) + .build()); + + Tracing.finishSuccess(span); + LOGGER.info(formatLogMessage(correlationId, + "[Sidecar] Injected env vars for " + configs.size() + " sidecar(s)")); + + } catch (Exception e) { + LOGGER.error(formatLogMessage(correlationId, "[Sidecar] Error injecting env vars"), e); + Tracing.finishError(span, e); + } + } + + /** + * Injects sidecar env vars for a prewarmed (eager) deployment. + * Service names are deterministic from appDef + instanceId + sidecar name. + */ + public void injectPrewarmedSidecarEnvVars( + Deployment theiaDeployment, + AppDefinition appDef, + int instanceId, + String correlationId) { + + Optional> configsOpt = getValidatedSidecarConfigs(appDef, correlationId); + if (configsOpt.isEmpty()) { + return; + } + List configs = configsOpt.get(); + if (configs.isEmpty()) { + return; + } + + ISpan span = Tracing.childSpan("sidecar.inject_prewarmed_env", "Inject prewarmed sidecar env vars"); + + try { + AppDefinitionSpec appDefSpec = appDef.getSpec(); + PodSpec podSpec = theiaDeployment.getSpec().getTemplate().getSpec(); + Container container = TheiaCloudPersistentVolumeUtil.getTheiaContainer(podSpec, appDefSpec); + + if (container == null) { + LOGGER.error(formatLogMessage(correlationId, "[Sidecar] Could not find Theia container in prewarmed deployment")); + Tracing.finish(span, SpanStatus.NOT_FOUND); + return; + } + + List envVars = container.getEnv(); + if (envVars == null) { + envVars = new ArrayList<>(); + container.setEnv(envVars); + } + + for (SidecarConfig config : configs) { + String serviceName = SidecarResourceFactory.getPrewarmedResourceName(appDef, instanceId, config); + + envVars.add(new EnvVarBuilder() + .withName(config.hostEnvVar()) + .withValue(serviceName) + .build()); + envVars.add(new EnvVarBuilder() + .withName(config.portEnvVar()) + .withValue(String.valueOf(config.containerPort())) + .build()); + } + + String configJson = serializeSidecarConfigJson(configs, + config -> SidecarResourceFactory.getPrewarmedResourceName(appDef, instanceId, config)); + + envVars.add(new EnvVarBuilder() + .withName("SIDECAR_CONFIG") + .withValue(configJson) + .build()); + + Tracing.finishSuccess(span); + + } catch (Exception e) { + LOGGER.error(formatLogMessage(correlationId, "[Sidecar] Error injecting prewarmed env vars"), e); + Tracing.finishError(span, e); + } + } + + /** + * Deletes all sidecar resources for a lazy session. + */ + public void deleteSidecars(Session session, String correlationId) { + factory.deleteResources(session, correlationId); + } + + /** + * Deletes all prewarmed sidecar resources for a specific instance of an AppDefinition. + */ + public void deletePrewarmedSidecars(AppDefinition appDef, int instanceId, String correlationId) { + Optional> configsOpt = getValidatedSidecarConfigs(appDef, correlationId); + if (configsOpt.isEmpty()) { + return; + } + List configs = configsOpt.get(); + for (SidecarConfig config : configs) { + factory.deletePrewarmedResources(appDef, instanceId, config, correlationId); + } + } + + /** + * Restarts sidecar pods for a prewarmed instance WITHOUT deleting the Deployment or Service. + * Deletes the Pod(s) managed by each sidecar Deployment, allowing the Deployment controller + * to recreate them with a fresh state. Used during session release so the sidecar + * infrastructure stays in place for pool reuse. + */ + public void restartPrewarmedSidecarPods(AppDefinition appDef, int instanceId, String correlationId) { + List configs = getSidecarConfigs(appDef); + if (configs.isEmpty()) { + return; + } + + ISpan span = Tracing.childSpan("sidecar.restart_prewarmed_pods", "Restart prewarmed sidecar pods"); + span.setData("instance_id", instanceId); + span.setData("sidecar_count", configs.size()); + + LOGGER.info(formatLogMessage(correlationId, + "[Sidecar] Restarting " + configs.size() + " sidecar pod(s) for instance " + instanceId)); + + for (SidecarConfig config : configs) { + String resourceName = SidecarResourceFactory.getPrewarmedResourceName(appDef, instanceId, config); + factory.deletePodsForDeployment(resourceName, correlationId); + } + + Tracing.finishSuccess(span); + } + + /** + * Creates prewarmed sidecar resources (Service + Deployment) for a single pool instance. + * Called during pool creation / reconciliation. + */ + public boolean createPrewarmedSidecars( + AppDefinition appDef, + int instanceId, + Optional pvcName, + Map additionalLabels, + String correlationId) { + + Optional> configsOpt = getValidatedSidecarConfigs(appDef, correlationId); + if (configsOpt.isEmpty()) { + return false; + } + List configs = configsOpt.get(); + if (configs.isEmpty()) { + return true; + } + + ISpan span = Tracing.childSpan("sidecar.create_prewarmed", "Create prewarmed sidecars"); + span.setData("instance_id", instanceId); + span.setData("sidecar_count", configs.size()); + + boolean success = true; + for (SidecarConfig config : configs) { + success &= factory.createPrewarmedService(appDef, instanceId, config, additionalLabels, correlationId).isPresent(); + success &= factory.createPrewarmedDeployment(appDef, instanceId, config, pvcName, additionalLabels, correlationId).isPresent(); + } + + span.setTag("outcome", success ? "success" : "failure"); + Tracing.finish(span, success ? SpanStatus.OK : SpanStatus.INTERNAL_ERROR); + return success; + } + + /** + * Ensures prewarmed sidecar capacity exists for the given number of instances. + */ + public boolean ensurePrewarmedSidecarCapacity( + AppDefinition appDef, + int minInstances, + Map labels, + String correlationId) { + + Optional> configsOpt = getValidatedSidecarConfigs(appDef, correlationId); + if (configsOpt.isEmpty()) { + return false; + } + List configs = configsOpt.get(); + if (configs.isEmpty()) { + return true; + } + + boolean success = true; + for (int instance = 1; instance <= minInstances; instance++) { + for (SidecarConfig config : configs) { + String resourceName = SidecarResourceFactory.getPrewarmedResourceName(appDef, instance, config); + + var kube = factory.getClient().kubernetes(); + String namespace = factory.getClient().namespace(); + + boolean serviceExists = kube.services().inNamespace(namespace).withName(resourceName).get() != null; + boolean deploymentExists = kube.apps().deployments().inNamespace(namespace).withName(resourceName) + .get() != null; + + Optional pvcName = factory.lookupExistingPvc(appDef, instance); + + if (!serviceExists) { + success &= factory.createPrewarmedService(appDef, instance, config, labels, correlationId) + .isPresent(); + } + if (!deploymentExists) { + success &= factory.createPrewarmedDeployment(appDef, instance, config, pvcName, labels, + correlationId).isPresent(); + } + } + } + return success; + } + + /** + * Reconciles prewarmed sidecar resources for the given target count. + */ + public boolean reconcilePrewarmedSidecars( + AppDefinition appDef, + int targetInstances, + Map labels, + String correlationId) { + // Same logic as ensureCapacity — create any missing + return ensurePrewarmedSidecarCapacity(appDef, targetInstances, labels, correlationId); + } + + private Optional> getValidatedSidecarConfigs(AppDefinition appDef, String correlationId) { + if (!validateUniqueSidecarNames(appDef, correlationId)) { + return Optional.empty(); + } + return Optional.of(getSidecarConfigs(appDef)); + } + + private String serializeSidecarConfigJson(List configs, + Function serviceNameResolver) throws JsonProcessingException { + List> entries = new ArrayList<>(); + for (SidecarConfig config : configs) { + Map entry = new LinkedHashMap<>(); + entry.put("name", config.name()); + entry.put("host", serviceNameResolver.apply(config)); + entry.put("port", config.containerPort()); + entry.put("languages", config.languages() != null ? config.languages() : List.of()); + entries.add(entry); + } + return OBJECT_MAPPER.writeValueAsString(entries); + } +} diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/sidecar/SidecarResourceFactory.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/sidecar/SidecarResourceFactory.java new file mode 100644 index 000000000..0afd4da3b --- /dev/null +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/sidecar/SidecarResourceFactory.java @@ -0,0 +1,481 @@ +/******************************************************************************** + * Copyright (C) 2025 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.operator.sidecar; + +import static org.eclipse.theia.cloud.common.util.LogMessageUtil.formatLogMessage; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.theia.cloud.common.k8s.client.TheiaCloudClient; +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinition; +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinitionSpec; +import org.eclipse.theia.cloud.common.k8s.resource.session.Session; +import org.eclipse.theia.cloud.common.tracing.Tracing; +import org.eclipse.theia.cloud.common.util.NamingUtil; +import org.eclipse.theia.cloud.operator.util.TheiaCloudPersistentVolumeUtil; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import io.fabric8.kubernetes.api.model.Container; +import io.fabric8.kubernetes.api.model.EnvVar; +import io.fabric8.kubernetes.api.model.EnvVarBuilder; +import io.fabric8.kubernetes.api.model.IntOrString; +import io.fabric8.kubernetes.api.model.OwnerReference; +import io.fabric8.kubernetes.api.model.OwnerReferenceBuilder; +import io.fabric8.kubernetes.api.model.PersistentVolumeClaimVolumeSource; +import io.fabric8.kubernetes.api.model.Quantity; +import io.fabric8.kubernetes.api.model.LocalObjectReference; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServiceBuilder; +import io.fabric8.kubernetes.api.model.Volume; +import io.fabric8.kubernetes.api.model.VolumeMount; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; +import io.sentry.ISpan; +import io.sentry.SpanStatus; + +/** + * Creates Kubernetes Service and Deployment resources for sidecar containers + * using Fabric8 builders (no YAML templates). + */ +@Singleton +public class SidecarResourceFactory { + + private static final Logger LOGGER = LogManager.getLogger(SidecarResourceFactory.class); + + @Inject + private TheiaCloudClient client; + + /** Exposed for SidecarManager to check existing resources / lookup PVCs. */ + public TheiaCloudClient getClient() { + return client; + } + + // ========== Naming ========== + + /** + * Lazy session naming: {@code -} + */ + public static String getLazyResourceName(Session session, SidecarConfig config) { + return NamingUtil.createNameWithSuffix(session, config.name()); + } + + /** + * Overload accepting a deployment name prefix (used during env var injection + * when the Session object is not available, but the deployment name is). + * The Theia deployment name for lazy sessions equals the session resource name, + * so this returns the same result as the Session-based overload. + */ + public static String getLazyResourceName(String sessionOrDeploymentName, SidecarConfig config) { + return NamingUtil.asValidName(sessionOrDeploymentName + "-" + config.name(), 63); + } + + /** + * Prewarmed naming: {@code NamingUtil.createName(appDef, instance, sidecar-name)} + */ + public static String getPrewarmedResourceName(AppDefinition appDef, int instance, SidecarConfig config) { + return NamingUtil.createName(appDef, instance, config.name()); + } + + // ========== Lazy Session Resources ========== + + public Optional createService(Session session, SidecarConfig config, String correlationId) { + String name = getLazyResourceName(session, config); + String sessionName = session.getMetadata().getName(); + String sessionUID = session.getMetadata().getUid(); + + ISpan span = Tracing.childSpan("sidecar.service.create", "Create sidecar service " + config.name()); + span.setData("service_name", name); + + LOGGER.info(formatLogMessage(correlationId, "[Sidecar] Creating service " + name)); + + try { + Service service = new ServiceBuilder() + .withNewMetadata() + .withName(name) + .withNamespace(client.namespace()) + .addToLabels("app", name) + .addToLabels("theia-cloud.io/sidecar", config.name()) + .withOwnerReferences(createOwnerReference(Session.API, Session.KIND, sessionName, sessionUID)) + .endMetadata() + .withNewSpec() + .addToSelector("app", name) + .addNewPort() + .withPort(config.containerPort()) + .withTargetPort(new IntOrString(config.containerPort())) + .withProtocol("TCP") + .endPort() + .endSpec() + .build(); + + Service created = client.kubernetes().services().inNamespace(client.namespace()).resource(service).create(); + Tracing.finishSuccess(span); + LOGGER.info(formatLogMessage(correlationId, "[Sidecar] Created service " + name)); + return Optional.of(created); + + } catch (Exception e) { + LOGGER.error(formatLogMessage(correlationId, "[Sidecar] Error creating service " + name), e); + Tracing.finishError(span, e); + return Optional.empty(); + } + } + + public Optional createDeployment( + Session session, + SidecarConfig config, + Optional pvcName, + AppDefinitionSpec appDefSpec, + String correlationId) { + + String name = getLazyResourceName(session, config); + String sessionName = session.getMetadata().getName(); + String sessionUID = session.getMetadata().getUid(); + + ISpan span = Tracing.childSpan("sidecar.deployment.create", "Create sidecar deployment " + config.name()); + span.setData("deployment_name", name); + span.setData("image", config.image()); + + LOGGER.info(formatLogMessage(correlationId, "[Sidecar] Creating deployment " + name)); + + try { + Deployment deployment = buildDeployment(name, config, + createOwnerReference(Session.API, Session.KIND, sessionName, sessionUID)); + + addImagePullSecret(deployment, appDefSpec); + + if (config.mountWorkspace() && pvcName.isPresent()) { + addVolumeClaimToDeployment(deployment, pvcName.get(), appDefSpec); + LOGGER.info(formatLogMessage(correlationId, "[Sidecar] Mounted PVC " + pvcName.get() + " to " + name)); + } + + Deployment created = client.kubernetes().apps().deployments().inNamespace(client.namespace()).resource(deployment).create(); + Tracing.finishSuccess(span); + LOGGER.info(formatLogMessage(correlationId, "[Sidecar] Created deployment " + name)); + return Optional.of(created); + + } catch (Exception e) { + LOGGER.error(formatLogMessage(correlationId, "[Sidecar] Error creating deployment " + name), e); + Tracing.finishError(span, e); + return Optional.empty(); + } + } + + // ========== Prewarmed Resources ========== + + public Optional createPrewarmedService( + AppDefinition appDef, + int instance, + SidecarConfig config, + Map additionalLabels, + String correlationId) { + + String name = getPrewarmedResourceName(appDef, instance, config); + String appDefName = appDef.getMetadata().getName(); + String appDefUID = appDef.getMetadata().getUid(); + + LOGGER.info(formatLogMessage(correlationId, "[Sidecar] Creating prewarmed service " + name)); + + try { + Service service = new ServiceBuilder() + .withNewMetadata() + .withName(name) + .withNamespace(client.namespace()) + .addToLabels("app", name) + .addToLabels("theia-cloud.io/sidecar", config.name()) + .addToLabels(additionalLabels) + .withOwnerReferences(createOwnerReference(AppDefinition.API, AppDefinition.KIND, appDefName, appDefUID)) + .endMetadata() + .withNewSpec() + .addToSelector("app", name) + .addNewPort() + .withPort(config.containerPort()) + .withTargetPort(new IntOrString(config.containerPort())) + .withProtocol("TCP") + .endPort() + .endSpec() + .build(); + + Service created = client.kubernetes().services().inNamespace(client.namespace()).resource(service).create(); + LOGGER.info(formatLogMessage(correlationId, "[Sidecar] Created prewarmed service " + name)); + return Optional.of(created); + + } catch (Exception e) { + LOGGER.error(formatLogMessage(correlationId, "[Sidecar] Error creating prewarmed service " + name), e); + return Optional.empty(); + } + } + + public Optional createPrewarmedDeployment( + AppDefinition appDef, + int instance, + SidecarConfig config, + Optional pvcName, + Map additionalLabels, + String correlationId) { + + String name = getPrewarmedResourceName(appDef, instance, config); + String appDefName = appDef.getMetadata().getName(); + String appDefUID = appDef.getMetadata().getUid(); + + LOGGER.info(formatLogMessage(correlationId, "[Sidecar] Creating prewarmed deployment " + name)); + + try { + Deployment deployment = buildDeployment(name, config, + createOwnerReference(AppDefinition.API, AppDefinition.KIND, appDefName, appDefUID)); + + addImagePullSecret(deployment, appDef.getSpec()); + + // Add additional labels to deployment + pod template + if (additionalLabels != null && !additionalLabels.isEmpty()) { + deployment.getMetadata().getLabels().putAll(additionalLabels); + deployment.getSpec().getTemplate().getMetadata().getLabels().putAll(additionalLabels); + } + + if (config.mountWorkspace() && pvcName.isPresent()) { + addVolumeClaimToDeployment(deployment, pvcName.get(), appDef.getSpec()); + LOGGER.info(formatLogMessage(correlationId, "[Sidecar] Mounted PVC " + pvcName.get() + " to prewarmed " + name)); + } + + Deployment created = client.kubernetes().apps().deployments().inNamespace(client.namespace()).resource(deployment).create(); + LOGGER.info(formatLogMessage(correlationId, "[Sidecar] Created prewarmed deployment " + name)); + return Optional.of(created); + + } catch (Exception e) { + LOGGER.error(formatLogMessage(correlationId, "[Sidecar] Error creating prewarmed deployment " + name), e); + return Optional.empty(); + } + } + + // ========== Deletion ========== + + /** + * Deletes ALL sidecar resources (deployments + services) for a lazy session. + * Matches by label {@code theia-cloud.io/sidecar}. + */ + public void deleteResources(Session session, String correlationId) { + String sessionName = session.getMetadata().getName(); + + ISpan span = Tracing.childSpan("sidecar.delete", "Delete sidecar resources"); + span.setData("session", sessionName); + + LOGGER.info(formatLogMessage(correlationId, "[Sidecar] Deleting resources for session " + sessionName)); + + try { + // Delete deployments and services whose name starts with session name + matches sidecar label + client.kubernetes().apps().deployments().inNamespace(client.namespace()) + .withLabel("theia-cloud.io/sidecar") + .list().getItems().stream() + .filter(d -> d.getMetadata().getName().startsWith(sessionName + "-")) + .forEach(d -> { + try { + client.kubernetes().apps().deployments().inNamespace(client.namespace()).resource(d).delete(); + } catch (Exception e) { + LOGGER.warn(formatLogMessage(correlationId, "[Sidecar] Failed to delete deployment " + d.getMetadata().getName()), e); + } + }); + + client.kubernetes().services().inNamespace(client.namespace()) + .withLabel("theia-cloud.io/sidecar") + .list().getItems().stream() + .filter(s -> s.getMetadata().getName().startsWith(sessionName + "-")) + .forEach(s -> { + try { + client.kubernetes().services().inNamespace(client.namespace()).resource(s).delete(); + } catch (Exception e) { + LOGGER.warn(formatLogMessage(correlationId, "[Sidecar] Failed to delete service " + s.getMetadata().getName()), e); + } + }); + + Tracing.finishSuccess(span); + } catch (Exception e) { + LOGGER.error(formatLogMessage(correlationId, "[Sidecar] Error deleting resources for " + sessionName), e); + Tracing.finishError(span, e); + } + } + + /** + * Deletes prewarmed sidecar resources for a specific sidecar of an instance. + */ + public void deletePrewarmedResources(AppDefinition appDef, int instance, SidecarConfig config, String correlationId) { + String name = getPrewarmedResourceName(appDef, instance, config); + + LOGGER.info(formatLogMessage(correlationId, "[Sidecar] Deleting prewarmed resources: " + name)); + + try { + client.kubernetes().apps().deployments().inNamespace(client.namespace()).withName(name).delete(); + client.kubernetes().services().inNamespace(client.namespace()).withName(name).delete(); + } catch (Exception e) { + LOGGER.error(formatLogMessage(correlationId, "[Sidecar] Error deleting prewarmed resources: " + name), e); + } + } + + /** + * Deletes all pods matching a sidecar deployment's label selector, causing + * the Deployment controller to recreate them. The Deployment and Service are kept. + */ + public void deletePodsForDeployment(String deploymentName, String correlationId) { + LOGGER.info(formatLogMessage(correlationId, "[Sidecar] Deleting pods for deployment " + deploymentName)); + + try { + client.kubernetes().pods().inNamespace(client.namespace()) + .withLabel("app", deploymentName) + .delete(); + } catch (Exception e) { + LOGGER.warn(formatLogMessage(correlationId, + "[Sidecar] Failed to delete pods for deployment " + deploymentName), e); + } + } + + // ========== PVC Lookup (for SidecarManager) ========== + + /** + * Looks up an existing PVC for a pool instance (delegates to client). + */ + public Optional lookupExistingPvc(AppDefinition appDef, int instance) { + String pvcName = NamingUtil.createNameWithSuffix(appDef, instance, "pvc"); + if (client.persistentVolumeClaimsClient().has(pvcName)) { + return Optional.of(pvcName); + } + return Optional.empty(); + } + + // ========== Private Helpers ========== + + private Deployment buildDeployment(String name, SidecarConfig config, OwnerReference ownerRef) { + return new DeploymentBuilder() + .withNewMetadata() + .withName(name) + .withNamespace(client.namespace()) + .addToLabels("app", name) + .addToLabels("theia-cloud.io/sidecar", config.name()) + .withOwnerReferences(ownerRef) + .endMetadata() + .withNewSpec() + .withReplicas(1) + .withNewSelector() + .addToMatchLabels("app", name) + .endSelector() + .withNewTemplate() + .withNewMetadata() + .addToLabels("app", name) + .addToLabels("theia-cloud.io/sidecar", config.name()) + .endMetadata() + .withNewSpec() + .addNewContainer() + .withName(config.name()) + .withImage(config.image()) + .addNewPort() + .withContainerPort(config.containerPort()) + .endPort() + .withNewResources() + .addToLimits("cpu", new Quantity(config.cpuLimit())) + .addToLimits("memory", new Quantity(config.memoryLimit())) + .addToRequests("cpu", new Quantity(config.cpuRequest())) + .addToRequests("memory", new Quantity(config.memoryRequest())) + .endResources() + .endContainer() + .endSpec() + .endTemplate() + .endSpec() + .build(); + } + + private OwnerReference createOwnerReference(String apiVersion, String kind, String name, String uid) { + return new OwnerReferenceBuilder() + .withApiVersion(apiVersion) + .withKind(kind) + .withName(name) + .withUid(uid) + .withBlockOwnerDeletion(true) + .withController(false) + .build(); + } + + private void addVolumeClaimToDeployment(Deployment deployment, String pvcName, AppDefinitionSpec appDefSpec) { + io.fabric8.kubernetes.api.model.PodSpec podSpec = deployment.getSpec().getTemplate().getSpec(); + + List volumes = podSpec.getVolumes(); + if (volumes == null) { + volumes = new ArrayList<>(); + podSpec.setVolumes(volumes); + } + + final String volumeName = "workspace-data"; + boolean hasWorkspaceVolume = volumes.stream().anyMatch(v -> volumeName.equals(v.getName())); + if (!hasWorkspaceVolume) { + Volume volume = new Volume(); + volume.setName(volumeName); + PersistentVolumeClaimVolumeSource pvcSource = new PersistentVolumeClaimVolumeSource(); + pvcSource.setClaimName(pvcName); + volume.setPersistentVolumeClaim(pvcSource); + volumes.add(volume); + } + + Container sidecarContainer = podSpec.getContainers().get(0); + + String mountPath = TheiaCloudPersistentVolumeUtil.getMountPath(appDefSpec); + + List volumeMounts = sidecarContainer.getVolumeMounts(); + if (volumeMounts == null) { + volumeMounts = new ArrayList<>(); + sidecarContainer.setVolumeMounts(volumeMounts); + } + + boolean hasWorkspaceMount = volumeMounts.stream() + .anyMatch(vm -> volumeName.equals(vm.getName()) || mountPath.equals(vm.getMountPath())); + if (!hasWorkspaceMount) { + VolumeMount volumeMount = new VolumeMount(); + volumeMount.setName(volumeName); + volumeMount.setMountPath(mountPath); + volumeMounts.add(volumeMount); + } + + List envVars = sidecarContainer.getEnv(); + if (envVars == null) { + envVars = new ArrayList<>(); + sidecarContainer.setEnv(envVars); + } + + boolean hasWorkspaceEnv = envVars.stream().anyMatch(env -> "WORKSPACE_PATH".equals(env.getName())); + if (!hasWorkspaceEnv) { + envVars.add(new EnvVarBuilder() + .withName("WORKSPACE_PATH") + .withValue(mountPath) + .build()); + } + } + + private void addImagePullSecret(Deployment deployment, AppDefinitionSpec appDefSpec) { + String pullSecret = appDefSpec.getPullSecret(); + if (pullSecret == null || pullSecret.isBlank()) { + return; + } + + io.fabric8.kubernetes.api.model.PodSpec podSpec = deployment.getSpec().getTemplate().getSpec(); + if (podSpec.getImagePullSecrets() == null) { + podSpec.setImagePullSecrets(new ArrayList<>()); + } + + boolean alreadyPresent = podSpec.getImagePullSecrets().stream() + .map(LocalObjectReference::getName) + .anyMatch(pullSecret::equals); + if (!alreadyPresent) { + LocalObjectReference ref = new LocalObjectReference(); + ref.setName(pullSecret); + podSpec.getImagePullSecrets().add(ref); + } + } +} diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/util/K8sResourceFactory.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/util/K8sResourceFactory.java new file mode 100644 index 000000000..d405214d0 --- /dev/null +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/util/K8sResourceFactory.java @@ -0,0 +1,390 @@ +/******************************************************************************** + * Copyright (C) 2025 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.operator.util; + +import static org.eclipse.theia.cloud.common.util.LogMessageUtil.formatLogMessage; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.theia.cloud.common.k8s.client.TheiaCloudClient; +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinition; +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinitionSpec; +import org.eclipse.theia.cloud.common.k8s.resource.session.Session; +import org.eclipse.theia.cloud.operator.TheiaCloudOperatorArguments; +import org.eclipse.theia.cloud.operator.bandwidth.BandwidthLimiter; +import org.eclipse.theia.cloud.operator.handler.AddedHandlerUtil; +import org.eclipse.theia.cloud.operator.ingress.IngressPathProvider; +import org.eclipse.theia.cloud.operator.replacements.DeploymentTemplateReplacements; +import org.eclipse.theia.cloud.operator.sidecar.SidecarManager; +import org.eclipse.theia.cloud.operator.util.OwnershipManager.OwnerContext; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.Container; +import io.fabric8.kubernetes.api.model.PersistentVolumeClaimVolumeSource; +import io.fabric8.kubernetes.api.model.PodSpec; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.Volume; +import io.fabric8.kubernetes.api.model.VolumeMount; +import io.fabric8.kubernetes.api.model.apps.Deployment; + +import static org.eclipse.theia.cloud.operator.pool.PrewarmedResourcePool.APPDEFINITION_GENERATION_LABEL; + +/** + * Factory for creating Kubernetes resources from templates. Centralizes the pattern: load template → replace + * placeholders → add owner → apply. + */ +@Singleton +public class K8sResourceFactory { + + private static final Logger LOGGER = LogManager.getLogger(K8sResourceFactory.class); + + @Inject + private TheiaCloudClient client; + + @Inject + private TheiaCloudOperatorArguments arguments; + + @Inject + private IngressPathProvider pathProvider; + + @Inject + private BandwidthLimiter bandwidthLimiter; + + @Inject + private DeploymentTemplateReplacements deploymentReplacements; + + @Inject + private SidecarManager sidecarManager; + + // ========== Service Creation ========== + + /** + * Creates a service for a prewarmed (eager) instance. + */ + public Optional createServiceForEagerInstance(AppDefinition appDef, int instance, + Map labels, String correlationId) { + + Map replacements = TheiaCloudServiceUtil.getServiceReplacements(client.namespace(), appDef, + instance); + + String template = arguments.isUseKeycloak() ? AddedHandlerUtil.TEMPLATE_SERVICE_YAML + : AddedHandlerUtil.TEMPLATE_SERVICE_WITHOUT_AOUTH2_PROXY_YAML; + + Map labelsWithGeneration = addGenerationLabel(labels, appDef); + + return createService(template, replacements, OwnerContext.of(appDef.getMetadata().getName(), + appDef.getMetadata().getUid(), AppDefinition.API, AppDefinition.KIND), labelsWithGeneration, + correlationId); + } + + /** + * Creates an internal service for a prewarmed (eager) instance. + */ + public Optional createInternalServiceForEagerInstance(AppDefinition appDef, int instance, + Map labels, String correlationId) { + + Map replacements = TheiaCloudServiceUtil.getInternalServiceReplacements(client.namespace(), + appDef, instance); + + Map labelsWithGeneration = addGenerationLabel(labels, appDef); + + return createService(AddedHandlerUtil.TEMPLATE_INTERNAL_SERVICE_YAML, replacements, + OwnerContext.of(appDef.getMetadata().getName(), appDef.getMetadata().getUid(), AppDefinition.API, + AppDefinition.KIND), + labelsWithGeneration, correlationId); + } + + /** + * Creates a service for a lazy session. + */ + public Optional createServiceForLazySession(Session session, AppDefinition appDef, + Map labels, String correlationId) { + + Map replacements = TheiaCloudServiceUtil.getServiceReplacements(client.namespace(), session, + appDef.getSpec()); + + String template = arguments.isUseKeycloak() ? AddedHandlerUtil.TEMPLATE_SERVICE_YAML + : AddedHandlerUtil.TEMPLATE_SERVICE_WITHOUT_AOUTH2_PROXY_YAML; + + return createService(template, replacements, OwnerContext.of(session.getMetadata().getName(), + session.getMetadata().getUid(), Session.API, Session.KIND), labels, correlationId); + } + + /** + * Creates an internal service for a lazy session. + */ + public Optional createInternalServiceForLazySession(Session session, AppDefinition appDef, + Map labels, String correlationId) { + + Map replacements = TheiaCloudServiceUtil.getInternalServiceReplacements(client.namespace(), + session, appDef.getSpec()); + + return createService( + AddedHandlerUtil.TEMPLATE_INTERNAL_SERVICE_YAML, replacements, OwnerContext + .of(session.getMetadata().getName(), session.getMetadata().getUid(), Session.API, Session.KIND), + labels, correlationId); + } + + // ========== Deployment Creation ========== + + /** + * Creates a deployment for a prewarmed (eager) instance. + */ + public Optional createDeploymentForEagerInstance(AppDefinition appDef, int instance, + Map labels, String correlationId) { + return createDeploymentForEagerInstance(appDef, instance, Optional.empty(), labels, correlationId); + } + + public Optional createDeploymentForEagerInstance(AppDefinition appDef, int instance, + Optional pvcName, Map labels, String correlationId) { + + Map replacements = deploymentReplacements.getReplacements(client.namespace(), appDef, instance); + + String template = arguments.isUseKeycloak() ? AddedHandlerUtil.TEMPLATE_DEPLOYMENT_YAML + : AddedHandlerUtil.TEMPLATE_DEPLOYMENT_WITHOUT_AOUTH2_PROXY_YAML; + + Map labelsWithGeneration = addGenerationLabel(labels, appDef); + + return createDeployment(template, replacements, OwnerContext.of(appDef.getMetadata().getName(), + appDef.getMetadata().getUid(), AppDefinition.API, AppDefinition.KIND), labelsWithGeneration, + deployment -> { + if (pvcName.isPresent()) { + addVolumeClaim(deployment, pvcName.get(), appDef.getSpec()); + } + bandwidthLimiter.limit(deployment, appDef.getSpec().getDownlinkLimit(), + appDef.getSpec().getUplinkLimit(), correlationId); + AddedHandlerUtil.removeEmptyResources(deployment); + if (appDef.getSpec().getPullSecret() != null && !appDef.getSpec().getPullSecret().isEmpty()) { + AddedHandlerUtil.addImagePullSecret(deployment, appDef.getSpec().getPullSecret()); + } + sidecarManager.injectPrewarmedSidecarEnvVars(deployment, appDef, instance, correlationId); + }, correlationId); + } + public Optional createDeploymentForLazySession(Session session, AppDefinition appDef, + Optional pvName, Map labels, Consumer volumeHandler, + String correlationId) { + + Map replacements = deploymentReplacements.getReplacements(client.namespace(), appDef, session); + + String template = arguments.isUseKeycloak() ? AddedHandlerUtil.TEMPLATE_DEPLOYMENT_YAML + : AddedHandlerUtil.TEMPLATE_DEPLOYMENT_WITHOUT_AOUTH2_PROXY_YAML; + + return createDeployment(template, replacements, OwnerContext.of(session.getMetadata().getName(), + session.getMetadata().getUid(), Session.API, Session.KIND), labels, deployment -> { + // Apply session-specific labels to pod template + Map podLabels = deployment.getSpec().getTemplate().getMetadata().getLabels(); + if (podLabels == null) { + podLabels = new HashMap<>(); + deployment.getSpec().getTemplate().getMetadata().setLabels(podLabels); + } + podLabels.putAll(labels); + + // Execute deployment handlers (volume, sidecars, etc.) + if (volumeHandler != null) { + volumeHandler.accept(deployment); + } + + bandwidthLimiter.limit(deployment, appDef.getSpec().getDownlinkLimit(), + appDef.getSpec().getUplinkLimit(), correlationId); + AddedHandlerUtil.removeEmptyResources(deployment); + AddedHandlerUtil.addCustomEnvVarsToDeploymentFromSession(correlationId, deployment, session, + appDef); + + if (appDef.getSpec().getPullSecret() != null && !appDef.getSpec().getPullSecret().isEmpty()) { + AddedHandlerUtil.addImagePullSecret(deployment, appDef.getSpec().getPullSecret()); + } + }, correlationId); + } + + // ========== ConfigMap Creation ========== + + /** + * Creates a proxy config map for a prewarmed (eager) instance. + */ + public Optional createProxyConfigMapForEagerInstance(AppDefinition appDef, int instance, + Map labels, String correlationId) { + + Map replacements = TheiaCloudConfigMapUtil.getProxyConfigMapReplacements(client.namespace(), + appDef, instance); + + String host = arguments.getInstancesHost() + pathProvider.getPath(appDef, instance); + int port = appDef.getSpec().getPort(); + + Map labelsWithGeneration = addGenerationLabel(labels, appDef); + + return createConfigMap(AddedHandlerUtil.TEMPLATE_CONFIGMAP_YAML, replacements, + OwnerContext.of(appDef.getMetadata().getName(), appDef.getMetadata().getUid(), AppDefinition.API, + AppDefinition.KIND), + labelsWithGeneration, configMap -> AddedHandlerUtil.updateProxyConfigMap(client.kubernetes(), + client.namespace(), configMap, host, port), + correlationId); + } + + /** + * Creates an email config map for a prewarmed (eager) instance. + */ + public Optional createEmailConfigMapForEagerInstance(AppDefinition appDef, int instance, + Map labels, String correlationId) { + + Map replacements = TheiaCloudConfigMapUtil.getEmailConfigMapReplacements(client.namespace(), + appDef, instance); + + Map labelsWithGeneration = addGenerationLabel(labels, appDef); + + return createConfigMap(AddedHandlerUtil.TEMPLATE_CONFIGMAP_EMAILS_YAML, replacements, + OwnerContext.of(appDef.getMetadata().getName(), appDef.getMetadata().getUid(), AppDefinition.API, + AppDefinition.KIND), + labelsWithGeneration, null, correlationId); + } + + /** + * Creates a proxy config map for a lazy session. + */ + public Optional createProxyConfigMapForLazySession(Session session, AppDefinition appDef, + Map labels, String correlationId) { + + Map replacements = TheiaCloudConfigMapUtil.getProxyConfigMapReplacements(client.namespace(), + session); + + String host = arguments.getInstancesHost() + pathProvider.getPath(appDef, session); + int port = appDef.getSpec().getPort(); + + return createConfigMap(AddedHandlerUtil.TEMPLATE_CONFIGMAP_YAML, replacements, + OwnerContext.of(session.getMetadata().getName(), session.getMetadata().getUid(), Session.API, + Session.KIND), + labels, configMap -> AddedHandlerUtil.updateProxyConfigMap(client.kubernetes(), client.namespace(), + configMap, host, port), + correlationId); + } + + /** + * Creates an email config map for a lazy session. + */ + public Optional createEmailConfigMapForLazySession(Session session, Map labels, + String correlationId) { + + Map replacements = TheiaCloudConfigMapUtil.getEmailConfigMapReplacements(client.namespace(), + session); + + return createConfigMap(AddedHandlerUtil.TEMPLATE_CONFIGMAP_EMAILS_YAML, replacements, + OwnerContext + .of(session.getMetadata().getName(), session.getMetadata().getUid(), Session.API, Session.KIND), + labels, + configMap -> configMap.setData(java.util.Collections.singletonMap( + AddedHandlerUtil.FILENAME_AUTHENTICATED_EMAILS_LIST, session.getSpec().getUser())), + correlationId); + } + + // ========== Volume Claim ========== + + private void addVolumeClaim(Deployment deployment, String pvcName, AppDefinitionSpec appDefSpec) { + PodSpec podSpec = deployment.getSpec().getTemplate().getSpec(); + + Volume volume = new Volume(); + volume.setName("user-data"); + PersistentVolumeClaimVolumeSource pvcSource = new PersistentVolumeClaimVolumeSource(); + pvcSource.setClaimName(pvcName); + volume.setPersistentVolumeClaim(pvcSource); + + java.util.List volumes = podSpec.getVolumes(); + if (volumes == null) { + volumes = new ArrayList<>(); + podSpec.setVolumes(volumes); + } + volumes.add(volume); + + Container theiaContainer = TheiaCloudPersistentVolumeUtil.getTheiaContainer(podSpec, appDefSpec); + + String mountPath = TheiaCloudPersistentVolumeUtil.getMountPath(appDefSpec); + + VolumeMount volumeMount = new VolumeMount(); + volumeMount.setName("user-data"); + volumeMount.setMountPath(mountPath); + + java.util.List volumeMounts = theiaContainer.getVolumeMounts(); + if (volumeMounts == null) { + volumeMounts = new ArrayList<>(); + theiaContainer.setVolumeMounts(volumeMounts); + } + volumeMounts.add(volumeMount); + } + + // ========== Generic Creation Methods ========== + + private Optional createService(String templatePath, Map replacements, OwnerContext owner, + Map labels, String correlationId) { + + String yaml = loadTemplate(templatePath, replacements, correlationId); + if (yaml == null) { + return Optional.empty(); + } + + return K8sUtil.loadAndCreateServiceWithOwnerReference(client.kubernetes(), client.namespace(), correlationId, + yaml, owner.getApiVersion(), owner.getKind(), owner.getName(), owner.getUid(), 0, + labels != null ? labels : new HashMap<>()); + } + + private Optional createDeployment(String templatePath, Map replacements, + OwnerContext owner, Map labels, Consumer postProcessor, String correlationId) { + + String yaml = loadTemplate(templatePath, replacements, correlationId); + if (yaml == null) { + return Optional.empty(); + } + + return K8sUtil.loadAndCreateDeploymentWithOwnerReference(client.kubernetes(), client.namespace(), correlationId, + yaml, owner.getApiVersion(), owner.getKind(), owner.getName(), owner.getUid(), 0, + labels != null ? labels : new HashMap<>(), postProcessor != null ? postProcessor : d -> { + }); + } + + private Optional createConfigMap(String templatePath, Map replacements, + OwnerContext owner, Map labels, Consumer postProcessor, String correlationId) { + + String yaml = loadTemplate(templatePath, replacements, correlationId); + if (yaml == null) { + return Optional.empty(); + } + + return K8sUtil.loadAndCreateConfigMapWithOwnerReference(client.kubernetes(), client.namespace(), correlationId, + yaml, owner.getApiVersion(), owner.getKind(), owner.getName(), owner.getUid(), 0, + labels != null ? labels : new HashMap<>(), postProcessor != null ? postProcessor : cm -> { + }); + } + + private String loadTemplate(String templatePath, Map replacements, String correlationId) { + try { + return JavaResourceUtil.readResourceAndReplacePlaceholders(templatePath, replacements, correlationId); + } catch (IOException | URISyntaxException e) { + LOGGER.error(formatLogMessage(correlationId, "Error loading template: " + templatePath), e); + return null; + } + } + + /** + * Adds the AppDefinition generation label to track resource version. + */ + private Map addGenerationLabel(Map labels, AppDefinition appDef) { + Map result = labels != null ? new HashMap<>(labels) : new HashMap<>(); + result.put(APPDEFINITION_GENERATION_LABEL, String.valueOf(appDef.getMetadata().getGeneration())); + return result; + } +} diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/util/K8sUtil.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/util/K8sUtil.java index 0c2d54b5d..842468510 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/util/K8sUtil.java +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/util/K8sUtil.java @@ -21,6 +21,8 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.List; +import java.util.Map; +import java.util.HashMap; import java.util.Optional; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -35,6 +37,8 @@ import io.fabric8.kubernetes.api.model.OwnerReference; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.api.model.gatewayapi.v1.HTTPRoute; +import io.fabric8.kubernetes.api.model.gatewayapi.v1.HTTPRouteList; import io.fabric8.kubernetes.api.model.networking.v1.Ingress; import io.fabric8.kubernetes.client.NamespacedKubernetesClient; import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; @@ -49,6 +53,7 @@ public final class K8sUtil { private static final String CONFIG_MAP = "ConfigMap"; private static final String SERVICE = "Service"; private static final String INGRESS = "Ingress"; + private static final String HTTP_ROUTE = "HTTPRoute"; private K8sUtil() { } @@ -67,6 +72,20 @@ public static Optional getExistingIngress(NamespacedKubernetesClient cl .findAny(); } + public static Optional getExistingHttpRoute(NamespacedKubernetesClient client, + String namespace, String routeName) { + return client.resources(HTTPRoute.class, HTTPRouteList.class).inNamespace(namespace).list().getItems().stream()// + .filter(route -> routeName.equals(route.getMetadata().getName()))// + .findAny(); + } + + public static Optional getExistingHttpRoute(NamespacedKubernetesClient client, + String namespace, String ownerName, String ownerUid) { + return getExistingTypesStream(client, namespace, ownerName, ownerUid, + client.resources(HTTPRoute.class, HTTPRouteList.class).inNamespace(namespace).list().getItems())// + .findAny(); + } + public static List getExistingServices(NamespacedKubernetesClient client, String namespace, String ownerName, String ownerUid) { return getExistingTypes(client, namespace, ownerName, ownerUid, @@ -110,50 +129,54 @@ private static boolean hasThisTemplateOwnerReference(List ownerR public static Optional loadAndCreateIngressWithOwnerReference(NamespacedKubernetesClient client, String namespace, String correlationId, String yaml, String ownerAPIVersion, String ownerKind, - String ownerName, String ownerUid, int ownerReferenceIndex) { + String ownerName, String ownerUid, int ownerReferenceIndex, Map labelsToAdd) { return loadAndCreateTypeWithOwnerReference(client, namespace, correlationId, yaml, ownerAPIVersion, ownerKind, ownerName, ownerUid, ownerReferenceIndex, INGRESS, - client.network().v1().ingresses().inNamespace(namespace), item -> { + client.network().v1().ingresses().inNamespace(namespace), labelsToAdd, item -> { }); } public static Optional loadAndCreateServiceWithOwnerReference(NamespacedKubernetesClient client, String namespace, String correlationId, String yaml, String ownerAPIVersion, String ownerKind, - String ownerName, String ownerUid, int ownerReferenceIndex) { + String ownerName, String ownerUid, int ownerReferenceIndex, Map labelsToAdd) { return loadAndCreateTypeWithOwnerReference(client, namespace, correlationId, yaml, ownerAPIVersion, ownerKind, - ownerName, ownerUid, ownerReferenceIndex, SERVICE, client.services().inNamespace(namespace), item -> { + ownerName, ownerUid, ownerReferenceIndex, SERVICE, client.services().inNamespace(namespace), + labelsToAdd, item -> { }); } public static Optional loadAndCreateConfigMapWithOwnerReference(NamespacedKubernetesClient client, String namespace, String correlationId, String yaml, String ownerAPIVersion, String ownerKind, - String ownerName, String ownerUid, int ownerReferenceIndex) { + String ownerName, String ownerUid, int ownerReferenceIndex, Map labelsToAdd) { return loadAndCreateTypeWithOwnerReference(client, namespace, correlationId, yaml, ownerAPIVersion, ownerKind, ownerName, ownerUid, ownerReferenceIndex, CONFIG_MAP, client.configMaps().inNamespace(namespace), - item -> { + labelsToAdd, item -> { }); } public static Optional loadAndCreateDeploymentWithOwnerReference(NamespacedKubernetesClient client, String namespace, String correlationId, String yaml, String ownerAPIVersion, String ownerKind, - String ownerName, String ownerUid, int ownerReferenceIndex, Consumer additionalModification) { + String ownerName, String ownerUid, int ownerReferenceIndex, Map labelsToAdd, + Consumer additionalModification) { return loadAndCreateTypeWithOwnerReference(client, namespace, correlationId, yaml, ownerAPIVersion, ownerKind, ownerName, ownerUid, ownerReferenceIndex, DEPLOYMENT, - client.apps().deployments().inNamespace(namespace), additionalModification); + client.apps().deployments().inNamespace(namespace), labelsToAdd, additionalModification); } public static Optional loadAndCreateConfigMapWithOwnerReference(NamespacedKubernetesClient client, String namespace, String correlationId, String yaml, String ownerAPIVersion, String ownerKind, - String ownerName, String ownerUid, int ownerReferenceIndex, Consumer additionalModification) { + String ownerName, String ownerUid, int ownerReferenceIndex, Map labelsToAdd, + Consumer additionalModification) { return loadAndCreateTypeWithOwnerReference(client, namespace, correlationId, yaml, ownerAPIVersion, ownerKind, ownerName, ownerUid, ownerReferenceIndex, CONFIG_MAP, client.configMaps().inNamespace(namespace), - additionalModification); + labelsToAdd, additionalModification); } private static > Optional loadAndCreateTypeWithOwnerReference( NamespacedKubernetesClient client, String namespace, String correlationId, String yaml, String ownerAPIVersion, String ownerKind, String ownerName, String ownerUid, int ownerReferenceIndex, - String typeName, NonNamespaceOperation items, Consumer additionalModification) { + String typeName, NonNamespaceOperation items, Map labelsToAdd, + Consumer additionalModification) { try (ByteArrayInputStream inputStream = new ByteArrayInputStream(yaml.getBytes())) { @@ -164,6 +187,20 @@ private static > Optional loa return Optional.empty(); } + // Apply labels to the resource metadata + if (newItem.getMetadata().getLabels() == null) { + newItem.getMetadata().setLabels(new HashMap<>()); + } + newItem.getMetadata().getLabels().putAll(labelsToAdd); + + // If the resource is a Deployment, also apply labels to the pod template metadata + if (newItem instanceof Deployment deployment) { + if (deployment.getSpec().getTemplate().getMetadata().getLabels() == null) { + deployment.getSpec().getTemplate().getMetadata().setLabels(new HashMap<>()); + } + deployment.getSpec().getTemplate().getMetadata().getLabels().putAll(labelsToAdd); + } + ResourceEdit. updateOwnerReference(ownerReferenceIndex, ownerAPIVersion, ownerKind, ownerName, ownerUid, correlationId).andThen(additionalModification).accept(newItem); diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/util/OwnershipManager.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/util/OwnershipManager.java new file mode 100644 index 000000000..fac71bdcc --- /dev/null +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/util/OwnershipManager.java @@ -0,0 +1,269 @@ +/******************************************************************************** + * Copyright (C) 2025 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.operator.util; + +import static org.eclipse.theia.cloud.common.util.LogMessageUtil.formatLogMessage; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.OwnerReference; +import io.fabric8.kubernetes.client.KubernetesClientException; +import io.fabric8.kubernetes.client.dsl.Resource; + +/** + * Centralized manager for Kubernetes owner reference operations. Consolidates ownership logic from + * OwnerReferenceChecker and TheiaCloudHandlerUtil. + */ +public final class OwnershipManager { + + private static final Logger LOGGER = LogManager.getLogger(OwnershipManager.class); + + private OwnershipManager() { + } + + /** + * Context representing an owner entity. + */ + public static class OwnerContext { + private final String name; + private final String uid; + private final String apiVersion; + private final String kind; + + public OwnerContext(String name, String uid, String apiVersion, String kind) { + this.name = name; + this.uid = uid; + this.apiVersion = apiVersion; + this.kind = kind; + } + + public String getName() { + return name; + } + + public String getUid() { + return uid; + } + + public String getApiVersion() { + return apiVersion; + } + + public String getKind() { + return kind; + } + + public OwnerReference toOwnerReference() { + OwnerReference ref = new OwnerReference(); + ref.setApiVersion(apiVersion); + ref.setKind(kind); + ref.setName(name); + ref.setUid(uid); + return ref; + } + + public static OwnerContext of(String name, String uid) { + return new OwnerContext(name, uid, null, null); + } + + public static OwnerContext of(String name, String uid, String apiVersion, String kind) { + return new OwnerContext(name, uid, apiVersion, kind); + } + } + + // ========== Ownership Queries ========== + + /** + * Checks if a resource is owned solely by the specified owner (no other owners). + */ + public static boolean isOwnedSolelyBy(HasMetadata resource, String ownerName, String ownerUID) { + if (resource.getMetadata() == null) { + return false; + } + List refs = resource.getMetadata().getOwnerReferences(); + if (refs == null || refs.isEmpty()) { + return false; + } + if (refs.size() != 1) { + return false; + } + OwnerReference ref = refs.get(0); + return ownerUID.equals(ref.getUid()) && ownerName.equals(ref.getName()); + } + + /** + * Checks if a resource is owned solely by the specified owner. + */ + public static boolean isOwnedSolelyBy(HasMetadata resource, OwnerContext owner) { + return isOwnedSolelyBy(resource, owner.getName(), owner.getUid()); + } + + /** + * Checks if a resource has the specified owner as one of its owners. + */ + public static boolean hasOwner(HasMetadata resource, String ownerName, String ownerUID) { + if (resource.getMetadata() == null) { + return false; + } + List refs = resource.getMetadata().getOwnerReferences(); + if (refs == null || refs.isEmpty()) { + return false; + } + return refs.stream().anyMatch(ref -> ownerUID.equals(ref.getUid()) && ownerName.equals(ref.getName())); + } + + /** + * Checks if a resource has the specified owner as one of its owners. + */ + public static boolean hasOwner(HasMetadata resource, OwnerContext owner) { + return hasOwner(resource, owner.getName(), owner.getUid()); + } + + /** + * Checks if a resource has the specified owner AND at least one other owner. + */ + public static boolean hasAdditionalOwners(HasMetadata resource, String ownerName, String ownerUID) { + if (resource.getMetadata() == null) { + return false; + } + List refs = resource.getMetadata().getOwnerReferences(); + if (refs == null || refs.size() < 2) { + return false; + } + boolean hasThisOwner = refs.stream() + .anyMatch(ref -> ownerUID.equals(ref.getUid()) && ownerName.equals(ref.getName())); + boolean hasOtherOwner = refs.stream() + .anyMatch(ref -> !(ownerUID.equals(ref.getUid()) && ownerName.equals(ref.getName()))); + return hasThisOwner && hasOtherOwner; + } + + /** + * Checks if a resource has the specified owner AND at least one other owner. + */ + public static boolean hasAdditionalOwners(HasMetadata resource, OwnerContext owner) { + return hasAdditionalOwners(resource, owner.getName(), owner.getUid()); + } + + /** + * Checks if a resource has no owners. + */ + public static boolean isOrphan(HasMetadata resource) { + if (resource.getMetadata() == null) { + return true; + } + List refs = resource.getMetadata().getOwnerReferences(); + return refs == null || refs.isEmpty(); + } + + // ========== Ownership Mutations (In-Memory) ========== + + /** + * Adds an owner reference to a resource (in-memory mutation). + * + * @return the same resource for chaining + */ + public static T addOwner(T resource, OwnerContext owner, String correlationId) { + if (resource.getMetadata() == null) { + LOGGER.warn(formatLogMessage(correlationId, + "Cannot add owner " + owner.getName() + ": resource metadata is null")); + return resource; + } + LOGGER.info(formatLogMessage(correlationId, + "Adding owner " + owner.getName() + " to " + resource.getMetadata().getName())); + if (resource.getMetadata().getOwnerReferences() == null) { + resource.getMetadata().setOwnerReferences(new ArrayList<>()); + } + resource.getMetadata().getOwnerReferences().add(owner.toOwnerReference()); + return resource; + } + + /** + * Removes an owner reference from a resource (in-memory mutation). Does nothing if the owner is not present. + * + * @return the same resource for chaining + */ + public static T removeOwner(T resource, String ownerName, String ownerUID, + String correlationId) { + if (resource.getMetadata() == null) { + LOGGER.warn(formatLogMessage(correlationId, + "Cannot remove owner " + ownerName + ": resource metadata is null")); + return resource; + } + LOGGER.info(formatLogMessage(correlationId, + "Removing owner " + ownerName + " from " + resource.getMetadata().getName())); + if (resource.getMetadata().getOwnerReferences() == null) { + return resource; + } + resource.getMetadata().getOwnerReferences() + .removeIf(ref -> ownerName.equals(ref.getName()) && ownerUID.equals(ref.getUid())); + return resource; + } + + /** + * Removes an owner reference from a resource (in-memory mutation). + */ + public static T removeOwner(T resource, OwnerContext owner, String correlationId) { + return removeOwner(resource, owner.getName(), owner.getUid(), correlationId); + } + + // ========== Ownership Mutations (With K8s Client) ========== + + /** + * Result of an ownership operation. + */ + public enum OwnershipResult { + SUCCESS, + RESOURCE_NOT_FOUND, + CONFLICT, + ERROR + } + + /** + * Adds an owner reference to a resource via the K8s API. + */ + public static OwnershipResult addOwnerViaApi( + Function> resourceAccessor, + T resource, + OwnerContext owner, + String correlationId) { + try { + resourceAccessor.apply(resource).edit(r -> addOwner(r, owner, correlationId)); + return OwnershipResult.SUCCESS; + } catch (KubernetesClientException e) { + LOGGER.error(formatLogMessage(correlationId, + "Failed to add owner " + owner.getName() + " to " + resource.getMetadata().getName()), e); + return OwnershipResult.ERROR; + } + } + + /** + * Removes an owner reference from a resource via the K8s API. + */ + public static OwnershipResult removeOwnerViaApi( + Function> resourceAccessor, + T resource, + OwnerContext owner, + String correlationId) { + try { + resourceAccessor.apply(resource).edit(r -> removeOwner(r, owner, correlationId)); + return OwnershipResult.SUCCESS; + } catch (KubernetesClientException e) { + LOGGER.error(formatLogMessage(correlationId, + "Failed to remove owner " + owner.getName() + " from " + resource.getMetadata().getName()), e); + return OwnershipResult.ERROR; + } + } +} diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/util/ResourceLifecycleManager.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/util/ResourceLifecycleManager.java new file mode 100644 index 000000000..c01c4c916 --- /dev/null +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/util/ResourceLifecycleManager.java @@ -0,0 +1,538 @@ +/******************************************************************************** + * Copyright (C) 2025 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.operator.util; + +import static org.eclipse.theia.cloud.common.util.LogMessageUtil.formatLogMessage; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.theia.cloud.operator.util.OwnershipManager.OwnerContext; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.dsl.Resource; +import org.eclipse.theia.cloud.common.tracing.Tracing; + +import io.sentry.ISpan; +import io.sentry.Sentry; +import io.sentry.SpanStatus; + +/** + * Unified manager for Kubernetes resource lifecycle operations. + * Handles reconciliation (create/delete/update) and ownership release. + * + * Replaces the separate ResourceReconciler and ResourceDeletionManager classes. + */ +public final class ResourceLifecycleManager { + + private static final Logger LOGGER = LogManager.getLogger(ResourceLifecycleManager.class); + + private ResourceLifecycleManager() { + } + + /** + * Result of a reconciliation operation. + */ + public static class ReconcileResult { + private final boolean success; + private final int created; + private final int deleted; + private final int recreated; + private final int skipped; + + private ReconcileResult(boolean success, int created, int deleted, int recreated, int skipped) { + this.success = success; + this.created = created; + this.deleted = deleted; + this.recreated = recreated; + this.skipped = skipped; + } + + public boolean isSuccess() { + return success; + } + + public int getCreated() { + return created; + } + + public int getDeleted() { + return deleted; + } + + public int getRecreated() { + return recreated; + } + + public int getSkipped() { + return skipped; + } + + public static ReconcileResult success(int created, int deleted, int recreated, int skipped) { + return new ReconcileResult(true, created, deleted, recreated, skipped); + } + + public static ReconcileResult failure(int created, int deleted, int recreated, int skipped) { + return new ReconcileResult(false, created, deleted, recreated, skipped); + } + } + + /** + * Context for reconciliation operations. + * + * @param the resource type + */ + public static class ReconcileContext { + private final String correlationId; + private final List existingResources; + private final Set missingIds; + private final int targetCount; + private final OwnerContext owner; + private final Function> resourceAccessor; + private final Function idExtractor; + private final String resourceTypeName; + private final Consumer createResource; + private final Consumer recreateResource; + private final Predicate shouldRecreate; + + private ReconcileContext(Builder builder) { + this.correlationId = builder.correlationId; + this.existingResources = builder.existingResources; + this.missingIds = builder.missingIds; + this.targetCount = builder.targetCount; + this.owner = builder.owner; + this.resourceAccessor = builder.resourceAccessor; + this.idExtractor = builder.idExtractor; + this.resourceTypeName = builder.resourceTypeName; + this.createResource = builder.createResource; + this.recreateResource = builder.recreateResource; + this.shouldRecreate = builder.shouldRecreate; + } + + public static Builder builder() { + return new Builder<>(); + } + + public static class Builder { + private String correlationId; + private List existingResources = new ArrayList<>(); + private Set missingIds = Set.of(); + private int targetCount; + private OwnerContext owner; + private Function> resourceAccessor; + private Function idExtractor; + private String resourceTypeName = "resource"; + private Consumer createResource = id -> { + }; + private Consumer recreateResource; + private Predicate shouldRecreate = r -> false; + + public Builder correlationId(String correlationId) { + this.correlationId = correlationId; + return this; + } + + public Builder existingResources(List existingResources) { + this.existingResources = existingResources != null ? existingResources : new ArrayList<>(); + return this; + } + + public Builder missingIds(Set missingIds) { + this.missingIds = missingIds != null ? missingIds : Set.of(); + return this; + } + + public Builder targetCount(int targetCount) { + this.targetCount = targetCount; + return this; + } + + public Builder owner(OwnerContext owner) { + this.owner = owner; + return this; + } + + public Builder owner(String name, String uid) { + this.owner = OwnerContext.of(name, uid); + return this; + } + + public Builder resourceAccessor(Function> resourceAccessor) { + this.resourceAccessor = resourceAccessor; + return this; + } + + public Builder idExtractor(Function idExtractor) { + this.idExtractor = idExtractor; + return this; + } + + public Builder resourceTypeName(String resourceTypeName) { + this.resourceTypeName = resourceTypeName; + return this; + } + + public Builder createResource(Consumer createResource) { + this.createResource = createResource; + return this; + } + + public Builder recreateResource(Consumer recreateResource) { + this.recreateResource = recreateResource; + return this; + } + + public Builder shouldRecreate(Predicate shouldRecreate) { + this.shouldRecreate = shouldRecreate; + return this; + } + + public ReconcileContext build() { + return new ReconcileContext<>(this); + } + } + } + + /** + * Reconciles resources to match the target state. + * + * Operations performed: + * 1. Creates resources for missing IDs (1 to targetCount) + * 2. Deletes resources with IDs > targetCount (if solely owned) + * 3. Recreates resources that need updating (if solely owned) + * + * Resources with multiple owners are handled specially: + * - If we're one of multiple owners and resource should be deleted, we remove our owner ref + * - If we're one of multiple owners and resource needs recreation, we skip it + */ + public static ReconcileResult reconcile(ReconcileContext ctx) { + ISpan parentSpan = Sentry.getSpan(); + ISpan span = parentSpan != null + ? parentSpan.startChild("lifecycle.reconcile", "Reconcile " + ctx.resourceTypeName) + : null; + + if (span != null) { + span.setTag("resource.type", ctx.resourceTypeName); + span.setData("target_count", ctx.targetCount); + span.setData("existing_count", ctx.existingResources.size()); + span.setData("missing_count", ctx.missingIds.size()); + } + + boolean success = true; + int created = 0; + int deleted = 0; + int recreated = 0; + int skipped = 0; + + try { + // Step 1: Create missing resources + for (int instanceId : ctx.missingIds) { + ISpan createSpan = span != null + ? span.startChild("lifecycle.create", "Create " + ctx.resourceTypeName + " " + instanceId) + : null; + if (createSpan != null) { + createSpan.setTag("resource.type", ctx.resourceTypeName); + createSpan.setTag("resource.operation", "create"); + createSpan.setData("instance_id", instanceId); + } + + try { + ctx.createResource.accept(instanceId); + created++; + if (createSpan != null) { + Tracing.finishSuccess(createSpan); + } + } catch (Exception e) { + LOGGER.error(formatLogMessage(ctx.correlationId, + "Failed to create " + ctx.resourceTypeName + " for instance " + instanceId), e); + success = false; + if (createSpan != null) { + Tracing.finishError(createSpan, e); + } + } + } + + // Step 2: Process existing resources + for (T resource : ctx.existingResources) { + Integer id = ctx.idExtractor.apply(resource); + if (id == null) { + LOGGER.warn(formatLogMessage(ctx.correlationId, "Cannot extract ID from " + ctx.resourceTypeName + + " " + resource.getMetadata().getName())); + skipped++; + continue; + } + + String resourceName = resource.getMetadata().getName(); + + // Case A: Resource ID > target count → should be deleted + if (id > ctx.targetCount) { + if (OwnershipManager.isOwnedSolelyBy(resource, ctx.owner)) { + ISpan deleteSpan = span != null + ? span.startChild("lifecycle.delete", "Delete excess " + ctx.resourceTypeName) + : null; + if (deleteSpan != null) { + deleteSpan.setTag("resource.type", ctx.resourceTypeName); + deleteSpan.setTag("resource.operation", "delete"); + deleteSpan.setData("resource_name", resourceName); + deleteSpan.setData("instance_id", id); + deleteSpan.setTag("delete.reason", "excess"); + } + + try { + ctx.resourceAccessor.apply(resource).delete(); + deleted++; + LOGGER.info(formatLogMessage(ctx.correlationId, + "Deleted excess " + ctx.resourceTypeName + " " + resourceName)); + if (deleteSpan != null) { + Tracing.finishSuccess(deleteSpan); + } + } catch (Exception e) { + LOGGER.error(formatLogMessage(ctx.correlationId, + "Failed to delete " + ctx.resourceTypeName + " " + resourceName), e); + success = false; + if (deleteSpan != null) { + Tracing.finishError(deleteSpan, e); + } + } + } else if (OwnershipManager.hasAdditionalOwners(resource, ctx.owner)) { + ISpan removeOwnerSpan = span != null + ? span.startChild("lifecycle.remove_owner", "Remove owner from " + ctx.resourceTypeName) + : null; + if (removeOwnerSpan != null) { + removeOwnerSpan.setTag("resource.type", ctx.resourceTypeName); + removeOwnerSpan.setTag("resource.operation", "remove_owner"); + removeOwnerSpan.setData("resource_name", resourceName); + } + + try { + ctx.resourceAccessor.apply(resource).edit(r -> { + OwnershipManager.removeOwner(r, ctx.owner, ctx.correlationId); + return r; + }); + skipped++; + LOGGER.info(formatLogMessage(ctx.correlationId, "Removed owner from " + ctx.resourceTypeName + + " " + resourceName + " (has other owners)")); + if (removeOwnerSpan != null) { + Tracing.finishSuccess(removeOwnerSpan); + } + } catch (Exception e) { + LOGGER.error(formatLogMessage(ctx.correlationId, + "Failed to remove owner from " + ctx.resourceTypeName + " " + resourceName), e); + success = false; + if (removeOwnerSpan != null) { + Tracing.finishError(removeOwnerSpan, e); + } + } + } else { + skipped++; + } + } + // Case B: Resource needs recreation + else if (ctx.shouldRecreate.test(resource)) { + if (OwnershipManager.isOwnedSolelyBy(resource, ctx.owner)) { + ISpan recreateSpan = span != null + ? span.startChild("lifecycle.recreate", "Recreate " + ctx.resourceTypeName) + : null; + if (recreateSpan != null) { + recreateSpan.setTag("resource.type", ctx.resourceTypeName); + recreateSpan.setTag("resource.operation", "recreate"); + recreateSpan.setData("resource_name", resourceName); + recreateSpan.setData("instance_id", id); + } + + try { + ctx.resourceAccessor.apply(resource).delete(); + if (ctx.recreateResource != null) { + ctx.recreateResource.accept(resource); + } + recreated++; + LOGGER.info(formatLogMessage(ctx.correlationId, + "Recreated " + ctx.resourceTypeName + " " + resourceName)); + if (recreateSpan != null) { + Tracing.finishSuccess(recreateSpan); + } + } catch (Exception e) { + LOGGER.error(formatLogMessage(ctx.correlationId, + "Failed to recreate " + ctx.resourceTypeName + " " + resourceName), e); + success = false; + if (recreateSpan != null) { + Tracing.finishError(recreateSpan, e); + } + } + } else { + skipped++; + LOGGER.debug(formatLogMessage(ctx.correlationId, "Skipping recreation of " + + ctx.resourceTypeName + " " + resourceName + " (has other owners)")); + } + } + // Case C: Resource is fine, no action needed + } + + if (span != null) { + span.setData("created", created); + span.setData("deleted", deleted); + span.setData("recreated", recreated); + span.setData("skipped", skipped); + span.setTag("outcome", success ? "success" : "failure"); + span.setTag("had_changes", (created + deleted + recreated) > 0 ? "true" : "false"); + span.setStatus(success ? SpanStatus.OK : SpanStatus.INTERNAL_ERROR); + span.finish(); + } + + return success ? ReconcileResult.success(created, deleted, recreated, skipped) + : ReconcileResult.failure(created, deleted, recreated, skipped); + + } catch (Exception e) { + if (span != null) { + Tracing.finishError(span, e); + } + throw e; + } + } + + /** + * Releases ownership of resources without deleting them. + * Useful when an owner is being deleted but resources may have other owners. + * + * For each resource: + * - If solely owned by us → delete (K8s GC would do this anyway, but explicit is clearer) + * - If we're one of multiple owners → remove our owner reference + * - If not our resource → skip + */ + public static boolean releaseOwnership( + List resources, + OwnerContext owner, + Function> resourceAccessor, + String resourceTypeName, + String correlationId) { + + ISpan parentSpan = Sentry.getSpan(); + ISpan span = parentSpan != null + ? parentSpan.startChild("lifecycle.release_ownership", "Release " + resourceTypeName + " ownership") + : null; + + if (span != null) { + span.setTag("resource.type", resourceTypeName); + span.setData("resource_count", resources.size()); + } + + boolean success = true; + int deleted = 0; + int released = 0; + int skipped = 0; + + try { + for (T resource : resources) { + String resourceName = resource.getMetadata().getName(); + + if (OwnershipManager.isOwnedSolelyBy(resource, owner)) { + ISpan deleteSpan = span != null + ? span.startChild("lifecycle.delete_sole_owned", "Delete " + resourceTypeName) + : null; + if (deleteSpan != null) { + deleteSpan.setTag("resource.type", resourceTypeName); + deleteSpan.setTag("resource.operation", "delete"); + deleteSpan.setData("resource_name", resourceName); + deleteSpan.setTag("delete.reason", "sole_owner"); + } + + try { + resourceAccessor.apply(resource).delete(); + deleted++; + LOGGER.info(formatLogMessage(correlationId, + "Deleted " + resourceTypeName + " " + resourceName + " (sole owner)")); + if (deleteSpan != null) { + Tracing.finishSuccess(deleteSpan); + } + } catch (Exception e) { + LOGGER.error(formatLogMessage(correlationId, + "Failed to delete " + resourceTypeName + " " + resourceName), e); + success = false; + if (deleteSpan != null) { + Tracing.finishError(deleteSpan, e); + } + } + } else if (OwnershipManager.hasAdditionalOwners(resource, owner)) { + ISpan removeSpan = span != null + ? span.startChild("lifecycle.remove_owner_ref", "Remove owner from " + resourceTypeName) + : null; + if (removeSpan != null) { + removeSpan.setTag("resource.type", resourceTypeName); + removeSpan.setTag("resource.operation", "remove_owner"); + removeSpan.setData("resource_name", resourceName); + } + + try { + resourceAccessor.apply(resource).edit(r -> { + OwnershipManager.removeOwner(r, owner, correlationId); + return r; + }); + released++; + LOGGER.info(formatLogMessage(correlationId, + "Removed owner from " + resourceTypeName + " " + resourceName)); + if (removeSpan != null) { + Tracing.finishSuccess(removeSpan); + } + } catch (Exception e) { + LOGGER.error(formatLogMessage(correlationId, + "Failed to remove owner from " + resourceTypeName + " " + resourceName), e); + success = false; + if (removeSpan != null) { + Tracing.finishError(removeSpan, e); + } + } + } else { + skipped++; + } + } + + if (span != null) { + span.setData("deleted", deleted); + span.setData("released", released); + span.setData("skipped", skipped); + span.setTag("outcome", success ? "success" : "failure"); + span.setStatus(success ? SpanStatus.OK : SpanStatus.INTERNAL_ERROR); + span.finish(); + } + + return success; + + } catch (Exception e) { + if (span != null) { + Tracing.finishError(span, e); + } + throw e; + } + } + + /** + * Simplified release that skips certain resources. + */ + public static boolean releaseOwnership( + List resources, + OwnerContext owner, + Function> resourceAccessor, + String resourceTypeName, + Predicate skipPredicate, + String correlationId) { + + List filtered = resources.stream() + .filter(r -> skipPredicate == null || !skipPredicate.test(r)) + .toList(); + + return releaseOwnership(filtered, owner, resourceAccessor, resourceTypeName, correlationId); + } +} diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/util/SessionEnvCollector.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/util/SessionEnvCollector.java new file mode 100644 index 000000000..30573b21b --- /dev/null +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/util/SessionEnvCollector.java @@ -0,0 +1,126 @@ +package org.eclipse.theia.cloud.operator.util; + +import static org.eclipse.theia.cloud.common.util.LogMessageUtil.formatLogMessage; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.theia.cloud.common.k8s.client.TheiaCloudClient; +import org.eclipse.theia.cloud.common.k8s.resource.session.Session; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.Secret; +import io.sentry.Sentry; + +/** + * Collects environment variables for a session from various sources: direct env vars, ConfigMaps, and Secrets. + */ +@Singleton +public class SessionEnvCollector { + + private static final Logger LOGGER = LogManager.getLogger(SessionEnvCollector.class); + + @Inject + private TheiaCloudClient client; + + /** + * Collects all custom environment variables for a session. + * + * @param session The session to collect env vars for + * @param correlationId For logging/tracing + * @return Map of environment variable names to values + */ + public Map collect(Session session, String correlationId) { + Map result = new HashMap<>(); + + // Collect direct env vars + Map directEnvVars = session.getSpec().getEnvVars(); + if (directEnvVars != null && !directEnvVars.isEmpty()) { + result.putAll(directEnvVars); + LOGGER.debug(formatLogMessage(correlationId, "Collected " + directEnvVars.size() + " direct env vars")); + } + + // Resolve env vars from ConfigMaps + List configMapNames = session.getSpec().getEnvVarsFromConfigMaps(); + if (configMapNames != null) { + for (String configMapName : configMapNames) { + resolveConfigMap(configMapName, result, correlationId); + } + } + + // Resolve env vars from Secrets + List secretNames = session.getSpec().getEnvVarsFromSecrets(); + if (secretNames != null) { + for (String secretName : secretNames) { + resolveSecret(secretName, result, correlationId); + } + } + + LOGGER.info(formatLogMessage(correlationId, + "Collected " + result.size() + " total env vars for session " + session.getSpec().getName())); + + return result; + } + + private void resolveConfigMap(String configMapName, Map result, String correlationId) { + try { + ConfigMap configMap = client.kubernetes().configMaps().inNamespace(client.namespace()) + .withName(configMapName).get(); + + if (configMap == null) { + LOGGER.warn(formatLogMessage(correlationId, "ConfigMap not found: " + configMapName)); + return; + } + + Map data = configMap.getData(); + if (data != null && !data.isEmpty()) { + result.putAll(data); + LOGGER.debug(formatLogMessage(correlationId, + "Resolved " + data.size() + " env vars from ConfigMap: " + configMapName)); + } + } catch (Exception e) { + LOGGER.error(formatLogMessage(correlationId, "Failed to resolve ConfigMap: " + configMapName), e); + } + } + + private void resolveSecret(String secretName, Map result, String correlationId) { + try { + Secret secret = client.kubernetes().secrets().inNamespace(client.namespace()).withName(secretName).get(); + + if (secret == null) { + LOGGER.warn(formatLogMessage(correlationId, "Secret not found: " + secretName)); + return; + } + + Map data = secret.getData(); + if (data != null && !data.isEmpty()) { + // Secret data is base64 encoded + int successCount = 0; + for (Map.Entry entry : data.entrySet()) { + try { + String decodedValue = new String(Base64.getDecoder().decode(entry.getValue()), + StandardCharsets.UTF_8); + result.put(entry.getKey(), decodedValue); + successCount++; + } catch (IllegalArgumentException e) { + Sentry.captureException(e); + LOGGER.warn(formatLogMessage(correlationId, "Failed to decode Base64 value for key '" + + entry.getKey() + "' in Secret: " + secretName), e); + } + } + LOGGER.debug(formatLogMessage(correlationId, + "Resolved " + successCount + " env vars from Secret: " + secretName)); + } + } catch (Exception e) { + LOGGER.error(formatLogMessage(correlationId, "Failed to resolve Secret: " + secretName), e); + } + } +} diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/util/TheiaCloudHandlerUtil.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/util/TheiaCloudHandlerUtil.java index 974a4ec4d..d0e4a4357 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/util/TheiaCloudHandlerUtil.java +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/util/TheiaCloudHandlerUtil.java @@ -44,6 +44,11 @@ public final class TheiaCloudHandlerUtil { public static final String PLACEHOLDER_CONFIGNAME = "placeholder-configname"; public static final String PLACEHOLDER_MONITOR_PORT = "placeholder-monitor-port"; + public static final String PLACEHOLDER_DATA_BRIDGE_PORT = "placeholder-data-bridge-port"; + public static final String PLACEHOLDER_DATA_BRIDGE_CONTAINER_PORT = "placeholder-data-bridge-container-port"; + public static final String PLACEHOLDER_DATA_BRIDGE_ENV_PORT = "placeholder-data-bridge-env-port"; + public static final String PLACEHOLDER_DATA_BRIDGE_ENABLED = "placeholder-data-bridge-enabled"; + public static final String PLACEHOLDER_DATA_BRIDGE_STRATEGY = "placeholder-data-bridge-strategy"; private TheiaCloudHandlerUtil() { } @@ -73,6 +78,20 @@ public static T addOwnerReferenceToItem(String correlati return item; } + /** + * Removes the owner reference from the item if it is present. Does nothing otherwise. + */ + public static T removeOwnerReferenceFromItem(String correlationId, + String sessionResourceName, String sessionResourceUID, T item) { + LOGGER.info( + formatLogMessage(correlationId, "Removing the owner reference from " + item.getMetadata().getName())); + item.getMetadata().getOwnerReferences().removeIf(ownerReference -> { + return ownerReference.getName().equals(sessionResourceName) + && ownerReference.getUid().equals(sessionResourceUID); + }); + return item; + } + public static OwnerReference createOwnerReference(String sessionResourceName, String sessionResourceUID) { OwnerReference ownerReference = new OwnerReference(); ownerReference.setApiVersion(HasMetadata.getApiVersion(Session.class)); diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/util/TheiaCloudIngressUtil.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/util/TheiaCloudIngressUtil.java index fc4fbb465..9e5ddbe28 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/util/TheiaCloudIngressUtil.java +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/util/TheiaCloudIngressUtil.java @@ -16,88 +16,59 @@ ********************************************************************************/ package org.eclipse.theia.cloud.operator.util; -import static org.eclipse.theia.cloud.common.util.LogMessageUtil.formatLogMessage; - +import java.util.ArrayList; +import java.util.List; import java.util.Optional; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinition; -import org.eclipse.theia.cloud.common.util.JavaUtil; -import org.eclipse.theia.cloud.operator.handler.AddedHandlerUtil; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.OwnerReference; -import io.fabric8.kubernetes.api.model.networking.v1.HTTPIngressPath; -import io.fabric8.kubernetes.api.model.networking.v1.HTTPIngressRuleValue; -import io.fabric8.kubernetes.api.model.networking.v1.Ingress; -import io.fabric8.kubernetes.api.model.networking.v1.IngressRule; +import io.fabric8.kubernetes.api.model.gatewayapi.v1.HTTPRoute; +import io.fabric8.kubernetes.api.model.gatewayapi.v1.HTTPRouteList; import io.fabric8.kubernetes.client.NamespacedKubernetesClient; public final class TheiaCloudIngressUtil { - private static final Logger LOGGER = LogManager.getLogger(TheiaCloudIngressUtil.class); - private TheiaCloudIngressUtil() { } public static boolean checkForExistingIngressAndAddOwnerReferencesIfMissing(NamespacedKubernetesClient client, String namespace, AppDefinition appDefinition, String correlationId) { - Optional existingIngressWithParentAppDefinition = K8sUtil.getExistingIngress(client, namespace, - appDefinition.getMetadata().getName(), appDefinition.getMetadata().getUid()); - if (existingIngressWithParentAppDefinition.isPresent()) { + Optional existingRouteWithParentAppDefinition = K8sUtil.getExistingHttpRoute( + client, namespace, appDefinition.getMetadata().getName(), appDefinition.getMetadata().getUid()); + if (existingRouteWithParentAppDefinition.isPresent()) { return true; } - Optional ingress = K8sUtil.getExistingIngress(client, namespace, + Optional route = K8sUtil.getExistingHttpRoute(client, namespace, appDefinition.getSpec().getIngressname()); - if (ingress.isPresent()) { + if (route.isPresent()) { OwnerReference ownerReference = new OwnerReference(); ownerReference.setApiVersion(HasMetadata.getApiVersion(AppDefinition.class)); ownerReference.setKind(AppDefinition.KIND); ownerReference.setName(appDefinition.getMetadata().getName()); ownerReference.setUid(appDefinition.getMetadata().getUid()); - addOwnerReferenceToIngress(client, namespace, ingress.get(), ownerReference); + addOwnerReferenceToHttpRoute(client, namespace, route.get(), ownerReference); } - return ingress.isPresent(); + return route.isPresent(); } public static String getIngressName(AppDefinition appDefinition) { return appDefinition.getSpec().getIngressname(); } - public static void addOwnerReferenceToIngress(NamespacedKubernetesClient client, String namespace, Ingress ingress, - OwnerReference ownerReference) { - client.network().v1().ingresses().inNamespace(namespace).withName(ingress.getMetadata().getName()).edit(JavaUtil - .toUnary(ingressToEdit -> ingressToEdit.getMetadata().getOwnerReferences().add(ownerReference))); + public static void addOwnerReferenceToHttpRoute(NamespacedKubernetesClient client, String namespace, + HTTPRoute route, OwnerReference ownerReference) { + client.resources(HTTPRoute.class, HTTPRouteList.class).inNamespace(namespace) + .withName(route.getMetadata().getName()) + .edit(resource -> { + List ownerReferences = resource.getMetadata().getOwnerReferences(); + if (ownerReferences == null) { + ownerReferences = new ArrayList<>(); + resource.getMetadata().setOwnerReferences(ownerReferences); + } + ownerReferences.add(ownerReference); + return resource; + }); } - - public static void removeIngressRule(NamespacedKubernetesClient client, String namespace, Ingress ingress, - String path, String correlationId) { - client.network().v1().ingresses().inNamespace(namespace).withName(ingress.getMetadata().getName()) - .edit(JavaUtil.toUnary(ingressToEdit -> removeIngressRule(ingressToEdit, path, correlationId))); - } - - private static void removeIngressRule(Ingress ingressToEdit, String path, String correlationId) { - String ingressPath = path + AddedHandlerUtil.INGRESS_REWRITE_PATH; - IngressRule ruleToDelete = null; - for (IngressRule rule : ingressToEdit.getSpec().getRules()) { - HTTPIngressRuleValue ingressRuleValue = rule.getHttp(); - if (ingressRuleValue == null) { - continue; - } - for (HTTPIngressPath httpIngressPath : ingressRuleValue.getPaths()) { - if (ingressPath.equals(httpIngressPath.getPath())) { - ruleToDelete = rule; - break; - } else { - LOGGER.trace(formatLogMessage(correlationId, httpIngressPath.getPath() + " is NOT " + ingressPath)); - } - } - } - if (ruleToDelete != null) { - LOGGER.info(formatLogMessage(correlationId, "Removing ingress rule for path " + path)); - ingressToEdit.getSpec().getRules().remove(ruleToDelete); - } - } - } diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/util/TheiaCloudK8sUtil.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/util/TheiaCloudK8sUtil.java index 80d145f52..a8f16f989 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/util/TheiaCloudK8sUtil.java +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/util/TheiaCloudK8sUtil.java @@ -21,11 +21,11 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.eclipse.theia.cloud.common.k8s.resource.OperatorStatus; +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinition; import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinitionSpec; import org.eclipse.theia.cloud.common.k8s.resource.session.Session; import org.eclipse.theia.cloud.common.k8s.resource.session.SessionSpec; import org.eclipse.theia.cloud.common.k8s.resource.session.SessionSpecResourceList; -import org.eclipse.theia.cloud.common.util.NamingUtil; import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.client.NamespacedKubernetesClient; @@ -58,8 +58,10 @@ public static boolean checkIfMaxInstancesReached(NamespacedKubernetesClient clie .filter(w -> { String sessionAppDefinition = w.getSpec().getAppDefinition(); // Errored resources should not be counted - boolean result = appDefinitionName.equals(sessionAppDefinition) - && !OperatorStatus.ERROR.equals(w.getStatus().getOperatorStatus()); + // if status is null, treat as not ERROR + boolean isNotError = w.getStatus() == null + || !OperatorStatus.ERROR.equals(w.getStatus().getOperatorStatus()); + boolean result = appDefinitionName.equals(sessionAppDefinition) && isNotError; LOGGER.trace(formatLogMessage(correlationId, "Counting handled instances of app definition " + appDefinitionSpec.getName() + ": Is " + w.getSpec() + " of app definition and handled? " + result)); @@ -69,14 +71,20 @@ public static boolean checkIfMaxInstancesReached(NamespacedKubernetesClient clie return currentInstances > appDefinitionSpec.getMaxInstances(); } + /** + * Extracts the instance id from the name of a Kubernetes object whose name was generated based on an app definition + * and an id. + * + * @param metadata The object's metadata + * @return the extracted identifier + * @see org.eclipse.theia.cloud.common.util.NamingUtil#createName(AppDefinition, int) + * @see org.eclipse.theia.cloud.common.util.NamingUtil#createName(AppDefinition, int, String) + */ public static String extractIdFromName(ObjectMeta metadata) { + // Generated name is of the form "instance--" String name = metadata.getName(); String[] split = name.split("-"); - String instance = split.length == 0 ? "" : split[0]; - // kubernetes names must not start with letter, remove automatically added - // prefix - instance = instance.length() == 0 ? "" - : instance.charAt(0) == NamingUtil.VALID_NAME_PREFIX ? instance.substring(1) : instance; + String instance = split.length < 2 ? "" : split[1]; return instance; } diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/util/TheiaCloudServiceUtil.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/util/TheiaCloudServiceUtil.java index f23cdeade..9183e4bbb 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/util/TheiaCloudServiceUtil.java +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/util/TheiaCloudServiceUtil.java @@ -29,6 +29,7 @@ import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinition; import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinitionSpec; import org.eclipse.theia.cloud.common.k8s.resource.session.Session; +import org.eclipse.theia.cloud.common.util.DataBridgeUtil; import org.eclipse.theia.cloud.common.util.NamingUtil; import io.fabric8.kubernetes.api.model.OwnerReference; @@ -41,6 +42,7 @@ public final class TheiaCloudServiceUtil { public static final String SERVICE_NAME = "service"; public static final String PLACEHOLDER_SERVICENAME = "placeholder-servicename"; + public static final String PLACEHOLDER_SERVICENAME_INTERNAL = "placeholder-servicename-internal"; private TheiaCloudServiceUtil() { } @@ -53,6 +55,14 @@ public static String getServiceName(Session session) { return NamingUtil.createName(session); } + public static String getInternalServiceName(Session session) { + return NamingUtil.createNameWithSuffix(session, "int"); + } + + public static String getInternalServiceName(AppDefinition appDefinition, int instance) { + return NamingUtil.createNameWithSuffix(appDefinition, instance, "int"); + } + public static Integer getId(String correlationId, AppDefinition appDefinition, Service service) { String instance = TheiaCloudK8sUtil.extractIdFromName(service.getMetadata()); try { @@ -78,6 +88,7 @@ public static Map getServiceReplacements(String namespace, AppDe replacements.put(TheiaCloudHandlerUtil.PLACEHOLDER_NAMESPACE, namespace); replacements.put(TheiaCloudHandlerUtil.PLACEHOLDER_PORT, String.valueOf(appDefinition.getSpec().getPort())); putMonitorReplacements(appDefinition.getSpec(), replacements); + putDataBridgeReplacements(appDefinition.getSpec(), replacements); return replacements; } @@ -89,6 +100,28 @@ public static Map getServiceReplacements(String namespace, Sessi replacements.put(TheiaCloudHandlerUtil.PLACEHOLDER_NAMESPACE, namespace); replacements.put(TheiaCloudHandlerUtil.PLACEHOLDER_PORT, String.valueOf(appDefinitionSpec.getPort())); putMonitorReplacements(appDefinitionSpec, replacements); + putDataBridgeReplacements(appDefinitionSpec, replacements); + return replacements; + } + + public static Map getInternalServiceReplacements(String namespace, Session session, + AppDefinitionSpec appDefinitionSpec) { + Map replacements = new LinkedHashMap(); + replacements.put(PLACEHOLDER_SERVICENAME_INTERNAL, getInternalServiceName(session)); + replacements.put(TheiaCloudHandlerUtil.PLACEHOLDER_APP, TheiaCloudHandlerUtil.getAppSelector(session)); + replacements.put(TheiaCloudHandlerUtil.PLACEHOLDER_NAMESPACE, namespace); + replacements.put(TheiaCloudHandlerUtil.PLACEHOLDER_PORT, String.valueOf(appDefinitionSpec.getPort())); + return replacements; + } + + public static Map getInternalServiceReplacements(String namespace, AppDefinition appDefinition, + int instance) { + Map replacements = new LinkedHashMap(); + replacements.put(PLACEHOLDER_SERVICENAME_INTERNAL, getInternalServiceName(appDefinition, instance)); + replacements.put(TheiaCloudHandlerUtil.PLACEHOLDER_APP, + TheiaCloudHandlerUtil.getAppSelector(appDefinition, instance)); + replacements.put(TheiaCloudHandlerUtil.PLACEHOLDER_NAMESPACE, namespace); + replacements.put(TheiaCloudHandlerUtil.PLACEHOLDER_PORT, String.valueOf(appDefinition.getSpec().getPort())); return replacements; } @@ -109,6 +142,28 @@ private static void putMonitorReplacements(AppDefinitionSpec appDefinitionSpec, } } + private static void putDataBridgeReplacements(AppDefinitionSpec appDefinitionSpec, Map replacements) { + if (!DataBridgeUtil.isDataBridgeEnabled(appDefinitionSpec)) { + replacements.put(TheiaCloudHandlerUtil.PLACEHOLDER_DATA_BRIDGE_PORT, ""); + return; + } + + int portNumber = DataBridgeUtil.getDataBridgePort(appDefinitionSpec); + + if (portNumber == appDefinitionSpec.getPort() + || (appDefinitionSpec.getMonitor() != null && portNumber == appDefinitionSpec.getMonitor().getPort())) { + // Just remove the placeholder, otherwise the port would be duplicate + replacements.put(TheiaCloudHandlerUtil.PLACEHOLDER_DATA_BRIDGE_PORT, ""); + return; + } + + String port = String.valueOf(portNumber); + // Replace the placeholder with the port information + String replacement = "- name: data-bridge\n" + " port: " + port + "\n" + " targetPort: " + port + + "\n" + " protocol: TCP"; + replacements.put(TheiaCloudHandlerUtil.PLACEHOLDER_DATA_BRIDGE_PORT, replacement); + } + public static Optional getServiceOwnedBySession(String sessionResourceName, String sessionResourceUID, List existingServices) { Optional alreadyReservedService = existingServices.stream()// @@ -132,6 +187,15 @@ public static boolean isUnusedService(Service service) { return service.getMetadata().getOwnerReferences().size() == 1; } + /** + * Returns an unused service. + * + * @param existingServices + * @return + * @deprecated Use {@link #getUnusedService(List, String)} instead: Services should be owned by the corresponding + * app definition. + */ + @Deprecated public static Optional getUnusedService(List existingServices) { Optional serviceToUse = existingServices.stream()// .filter(TheiaCloudServiceUtil::isUnusedService)// @@ -139,4 +203,25 @@ public static Optional getUnusedService(List existingServices) return serviceToUse; } + /** + * Returns an unused service that is owned by the given app definition. + * + * @param existingServices The list of services to search in. + * @param appDefinitionResourceUID The UID of the app definition that should own the service. + * @return The unused service that is owned by the given app definition or nothing if none is available. + */ + public static Optional getUnusedService(List existingServices, String appDefinitionResourceUID) { + Optional serviceToUse = existingServices.stream()// + .filter(TheiaCloudServiceUtil::isUnusedService)// + .filter(service -> { + for (OwnerReference ownerReference : service.getMetadata().getOwnerReferences()) { + if (appDefinitionResourceUID.equals(ownerReference.getUid())) { + return true; + } + } + return false; + })// + .findAny(); + return serviceToUse; + }; } diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templateConfigmap.yaml b/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templateConfigmap.yaml index 8070abbab..4936c928a 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templateConfigmap.yaml +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templateConfigmap.yaml @@ -3,7 +3,7 @@ kind: ConfigMap metadata: name: placeholder-configname labels: - theiacloud: proxy + theia-cloud.io/template-purpose: proxy namespace: placeholder-namespace ownerReferences: - apiVersion: theia.cloud/v1beta9 diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templateConfigmapEmails.yaml b/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templateConfigmapEmails.yaml index 595dfd762..2d2e27ded 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templateConfigmapEmails.yaml +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templateConfigmapEmails.yaml @@ -3,7 +3,7 @@ kind: ConfigMap metadata: name: placeholder-emailsconfigname labels: - theiacloud: emails + theia-cloud.io/template-purpose: emails namespace: placeholder-namespace ownerReferences: - apiVersion: theia.cloud/v1beta9 diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templateDeployment.yaml b/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templateDeployment.yaml index 10f139afc..b40ad3b78 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templateDeployment.yaml +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templateDeployment.yaml @@ -19,10 +19,11 @@ spec: app: placeholder-app spec: automountServiceAccountToken: false + enableServiceLinks: false containers: - name: oauth2-proxy image: quay.io/oauth2-proxy/oauth2-proxy:placeholder-oauth2-proxy-version - imagePullPolicy: Always + imagePullPolicy: IfNotPresent args: - --config=/etc/oauth2-proxy.cfg - --authenticated-emails-file=/emails/authenticated-emails-list @@ -47,6 +48,7 @@ spec: - containerPort: placeholder-port name: application placeholder-monitor-port + placeholder-data-bridge-container-port resources: limits: cpu: placeholder-cpu-limits @@ -55,6 +57,8 @@ spec: cpu: placeholder-cpu-requests memory: placeholder-memory-requests env: + - name: THEIACLOUD_SERVICE_AUTH_TOKEN + value: placeholder-env-service-auth-token - name: THEIACLOUD_APP_ID value: placeholder-env-app-id - name: THEIACLOUD_SERVICE_URL @@ -79,6 +83,26 @@ spec: value: placeholder-keycloak-env-realm - name: THEIACLOUD_KEYCLOAK_CLIENT_ID value: placeholder-keycloak-env-clientid + - name: BUILD_CACHE_ENABLED + value: placeholder-build-cache-enabled + - name: BUILD_CACHE_URL + value: placeholder-build-cache-url + - name: BAZEL_BUILD_CACHE_URL + value: placeholder-bazel-build-cache-url + - name: BUILD_CACHE_PUSH + value: placeholder-build-cache-push + - name: DEPENDENCY_CACHE_ENABLED + value: placeholder-dependency-cache-enabled + - name: DEPENDENCY_CACHE_URL + value: placeholder-dependency-cache-url + - name: DATA_BRIDGE_PORT + value: placeholder-data-bridge-env-port + - name: DATA_BRIDGE_ENABLED + value: placeholder-data-bridge-enabled + - name: SCORPIO_THEIA_ENV_STRATEGY + value: placeholder-data-bridge-strategy + - name: THEIA + value: 1 securityContext: runAsUser: placeholder-uid runAsGroup: placeholder-uid diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templateDeploymentWithoutOAuthProxy.yaml b/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templateDeploymentWithoutOAuthProxy.yaml index 0d17697a9..c8428367e 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templateDeploymentWithoutOAuthProxy.yaml +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templateDeploymentWithoutOAuthProxy.yaml @@ -19,6 +19,7 @@ spec: app: placeholder-app spec: automountServiceAccountToken: false + enableServiceLinks: false # initContainers: # - name: wondershaper-init # image: gcr.io/kubernetes-238012/theia-cloud-wondershaper @@ -34,6 +35,7 @@ spec: - containerPort: placeholder-port name: application placeholder-monitor-port + placeholder-data-bridge-container-port resources: limits: cpu: placeholder-cpu-limits @@ -42,6 +44,8 @@ spec: cpu: placeholder-cpu-requests memory: placeholder-memory-requests env: + - name: THEIACLOUD_SERVICE_AUTH_TOKEN + value: placeholder-env-service-auth-token - name: THEIACLOUD_APP_ID value: placeholder-env-app-id - name: THEIACLOUD_SERVICE_URL @@ -66,6 +70,24 @@ spec: value: placeholder-keycloak-env-realm - name: THEIACLOUD_KEYCLOAK_CLIENT_ID value: placeholder-keycloak-env-clientid + - name: BUILD_CACHE_ENABLED + value: placeholder-build-cache-enabled + - name: BUILD_CACHE_URL + value: placeholder-build-cache-url + - name: BAZEL_BUILD_CACHE_URL + value: placeholder-bazel-build-cache-url + - name: BUILD_CACHE_PUSH + value: placeholder-build-cache-push + - name: DEPENDENCY_CACHE_ENABLED + value: placeholder-dependency-cache-enabled + - name: DEPENDENCY_CACHE_URL + value: placeholder-dependency-cache-url + - name: DATA_BRIDGE_PORT + value: placeholder-data-bridge-env-port + - name: DATA_BRIDGE_ENABLED + value: placeholder-data-bridge-enabled + - name: SCORPIO_THEIA_ENV_STRATEGY + value: placeholder-data-bridge-strategy securityContext: runAsUser: placeholder-uid runAsGroup: placeholder-uid diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templateInternalService.yaml b/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templateInternalService.yaml new file mode 100644 index 000000000..f00ff7d8f --- /dev/null +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templateInternalService.yaml @@ -0,0 +1,22 @@ +apiVersion: v1 +kind: Service +metadata: + name: placeholder-servicename-internal + labels: + app: placeholder-app + theia-cloud.io/service-type: internal + namespace: placeholder-namespace + ownerReferences: + - apiVersion: theia.cloud/v1beta9 + kind: AppDefinition + name: placeholder + uid: placeholder +spec: + ports: + - name: http-internal + port: 3001 + protocol: TCP + targetPort: placeholder-port + selector: + app: placeholder-app + type: ClusterIP \ No newline at end of file diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templatePersistentVolumeClaim.yaml b/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templatePersistentVolumeClaim.yaml index 94813cb07..d3f888cca 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templatePersistentVolumeClaim.yaml +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templatePersistentVolumeClaim.yaml @@ -4,11 +4,11 @@ metadata: name: placeholder-pv namespace: placeholder-namespace labels: - theia.cloud.workspace.name: placeholder-label-workspace-name -spec: - accessModes: - - ReadWriteOnce - storageClassName: placeholder-storage-class-name - resources: + theia-cloud.io/workspace-name: placeholder-label-workspace-name +spec: + accessModes: + - placeholder-access-mode + storageClassName: placeholder-storage-class-name + resources: requests: storage: placeholder-requested-storage diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templatePersistentVolumeClaimMinikube.yaml b/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templatePersistentVolumeClaimMinikube.yaml index 16afbf95a..4af790af6 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templatePersistentVolumeClaimMinikube.yaml +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templatePersistentVolumeClaimMinikube.yaml @@ -4,7 +4,7 @@ metadata: name: placeholder-pv namespace: placeholder-namespace labels: - theia.cloud.workspace.name: placeholder-label-workspace-name + theia-cloud.io/workspace-name: placeholder-label-workspace-name spec: accessModes: - ReadWriteOnce diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templateService.yaml b/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templateService.yaml index cb3c9c7ca..36873b751 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templateService.yaml +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templateService.yaml @@ -17,6 +17,7 @@ spec: protocol: TCP targetPort: web placeholder-monitor-port + placeholder-data-bridge-port selector: app: placeholder-app diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templateServiceWithoutOAuthProxy.yaml b/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templateServiceWithoutOAuthProxy.yaml index 0ae10460c..55bfaeb6e 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templateServiceWithoutOAuthProxy.yaml +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/resources/templateServiceWithoutOAuthProxy.yaml @@ -17,6 +17,7 @@ spec: protocol: TCP targetPort: placeholder-port placeholder-monitor-port + placeholder-data-bridge-port selector: app: placeholder-app type: ClusterIP diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/test/java/org/eclipse/theia/cloud/operator/sidecar/SidecarConfigTests.java b/java/operator/org.eclipse.theia.cloud.operator/src/test/java/org/eclipse/theia/cloud/operator/sidecar/SidecarConfigTests.java new file mode 100644 index 000000000..251656ef9 --- /dev/null +++ b/java/operator/org.eclipse.theia.cloud.operator/src/test/java/org/eclipse/theia/cloud/operator/sidecar/SidecarConfigTests.java @@ -0,0 +1,229 @@ +/******************************************************************************** + * Copyright (C) 2026 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.operator.sidecar; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinition; +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinitionSpec; +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.SidecarSpec; +import org.junit.jupiter.api.Test; + +class SidecarConfigTests { + + // ========== fromSpec ========== + + @Test + void fromSpec_allFieldsPresent() { + SidecarSpec spec = createSpec("langserver", "img:latest", 9090, List.of("java", "kotlin"), + "1", "2Gi", "200m", "512Mi", true); + + SidecarConfig config = SidecarConfig.fromSpec(spec); + + assertEquals("langserver", config.name()); + assertEquals("img:latest", config.image()); + assertEquals(9090, config.containerPort()); + assertEquals(List.of("java", "kotlin"), config.languages()); + assertEquals("1", config.cpuLimit()); + assertEquals("2Gi", config.memoryLimit()); + assertEquals("200m", config.cpuRequest()); + assertEquals("512Mi", config.memoryRequest()); + assertTrue(config.mountWorkspace()); + } + + @Test + void fromSpec_defaultPort() { + SidecarSpec spec = createSpec("langserver", "img:latest", 0, List.of(), null, null, null, null, false); + + SidecarConfig config = SidecarConfig.fromSpec(spec); + + assertEquals(SidecarConfig.DEFAULT_PORT, config.containerPort()); + } + + @Test + void fromSpec_nullLanguages() { + SidecarSpec spec = createSpec("langserver", "img:latest", 5000, null, null, null, null, null, false); + + SidecarConfig config = SidecarConfig.fromSpec(spec); + + assertEquals(List.of(), config.languages()); + } + + @Test + void fromSpec_defaultResourceLimits() { + SidecarSpec spec = createSpec("langserver", "img:latest", 5000, List.of(), null, null, null, null, false); + + SidecarConfig config = SidecarConfig.fromSpec(spec); + + assertEquals("500m", config.cpuLimit()); + assertEquals("1Gi", config.memoryLimit()); + assertEquals("100m", config.cpuRequest()); + assertEquals("256Mi", config.memoryRequest()); + } + + @Test + void fromSpec_blankResourceLimitsGetDefaults() { + SidecarSpec spec = createSpec("langserver", "img:latest", 5000, List.of(), " ", " ", " ", " ", false); + + SidecarConfig config = SidecarConfig.fromSpec(spec); + + assertEquals("500m", config.cpuLimit()); + assertEquals("1Gi", config.memoryLimit()); + assertEquals("100m", config.cpuRequest()); + assertEquals("256Mi", config.memoryRequest()); + } + + @Test + void fromSpec_mountWorkspaceDefaultsToFalse() { + SidecarSpec spec = createSpec("langserver", "img:latest", 5000, List.of(), null, null, null, null, null); + + SidecarConfig config = SidecarConfig.fromSpec(spec); + + assertFalse(config.mountWorkspace()); + } + + // ========== Env var naming ========== + + @Test + void hostEnvVar_simpleName() { + SidecarConfig config = createConfig("langserver"); + assertEquals("SIDECAR_LANGSERVER_HOST", config.hostEnvVar()); + } + + @Test + void hostEnvVar_hyphenatedName() { + SidecarConfig config = createConfig("rust-analyzer"); + assertEquals("SIDECAR_RUST_ANALYZER_HOST", config.hostEnvVar()); + } + + @Test + void portEnvVar_simpleName() { + SidecarConfig config = createConfig("langserver"); + assertEquals("SIDECAR_LANGSERVER_PORT", config.portEnvVar()); + } + + @Test + void portEnvVar_hyphenatedName() { + SidecarConfig config = createConfig("rust-analyzer"); + assertEquals("SIDECAR_RUST_ANALYZER_PORT", config.portEnvVar()); + } + + // ========== hasSidecars ========== + + @Test + void hasSidecars_nullAppDef() { + assertFalse(SidecarConfig.hasSidecars(null)); + } + + @Test + void hasSidecars_nullSpec() { + AppDefinition appDef = new AppDefinition(); + assertFalse(SidecarConfig.hasSidecars(appDef)); + } + + @Test + void hasSidecars_emptySidecarsList() { + AppDefinition appDef = createAppDefWithSidecars(List.of()); + assertFalse(SidecarConfig.hasSidecars(appDef)); + } + + @Test + void hasSidecars_withSidecars() { + SidecarSpec spec = createSpec("langserver", "img:latest", 5000, List.of("java"), null, null, null, null, true); + AppDefinition appDef = createAppDefWithSidecars(List.of(spec)); + assertTrue(SidecarConfig.hasSidecars(appDef)); + } + + @Test + void requiresSharedWorkspace_falseWhenNoSidecarMountsWorkspace() { + SidecarSpec spec = createSpec("langserver", "img:latest", 5000, List.of("java"), null, null, null, null, false); + AppDefinition appDef = createAppDefWithSidecars(List.of(spec)); + + assertFalse(SidecarConfig.requiresSharedWorkspace(appDef)); + } + + @Test + void requiresSharedWorkspace_trueWhenSidecarMountsWorkspace() { + SidecarSpec spec = createSpec("langserver", "img:latest", 5000, List.of("java"), null, null, null, null, true); + AppDefinition appDef = createAppDefWithSidecars(List.of(spec)); + + assertTrue(SidecarConfig.requiresSharedWorkspace(appDef)); + } + + // ========== getSidecarConfigs ========== + + @Test + void getSidecarConfigs_fromSpec() { + SidecarSpec spec1 = createSpec("java-ls", "img1:latest", 5000, List.of("java"), null, null, null, null, true); + SidecarSpec spec2 = createSpec("rust-ls", "img2:latest", 5001, List.of("rust"), null, null, null, null, false); + AppDefinition appDef = createAppDefWithSidecars(List.of(spec1, spec2)); + + List configs = SidecarConfig.getSidecarConfigs(appDef); + + assertEquals(2, configs.size()); + assertEquals("java-ls", configs.get(0).name()); + assertEquals("rust-ls", configs.get(1).name()); + } + + @Test + void getSidecarConfigs_nullAppDef() { + assertEquals(List.of(), SidecarConfig.getSidecarConfigs(null)); + } + + @Test + void getSidecarConfigs_noSidecarsNoLegacy() { + AppDefinition appDef = createAppDefWithSidecars(List.of()); + assertEquals(List.of(), SidecarConfig.getSidecarConfigs(appDef)); + } + + // ========== Helpers ========== + + private SidecarConfig createConfig(String name) { + return new SidecarConfig(name, "img:latest", 5000, List.of(), "500m", "1Gi", "100m", "256Mi", false); + } + + private SidecarSpec createSpec(String name, String image, int port, List languages, + String cpuLimit, String memoryLimit, String cpuRequest, String memoryRequest, Boolean mountWorkspace) { + SidecarSpec spec = new SidecarSpec(); + setFieldUnchecked(spec, "name", name); + setFieldUnchecked(spec, "image", image); + setFieldUnchecked(spec, "port", port); + setFieldUnchecked(spec, "languages", languages); + setFieldUnchecked(spec, "cpuLimit", cpuLimit); + setFieldUnchecked(spec, "memoryLimit", memoryLimit); + setFieldUnchecked(spec, "cpuRequest", cpuRequest); + setFieldUnchecked(spec, "memoryRequest", memoryRequest); + setFieldUnchecked(spec, "mountWorkspace", mountWorkspace); + return spec; + } + + private void setFieldUnchecked(Object obj, String fieldName, Object value) { + try { + var field = obj.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(obj, value); + } catch (Exception e) { + throw new RuntimeException("Failed to set field " + fieldName, e); + } + } + + private AppDefinition createAppDefWithSidecars(List sidecars) { + AppDefinition appDef = new AppDefinition(); + AppDefinitionSpec spec = new AppDefinitionSpec(); + setFieldUnchecked(spec, "sidecars", sidecars); + appDef.setSpec(spec); + return appDef; + } + +} diff --git a/java/service/org.eclipse.theia.cloud.service/README.md b/java/service/org.eclipse.theia.cloud.service/README.md index 0d53cd2d0..d879ea223 100644 --- a/java/service/org.eclipse.theia.cloud.service/README.md +++ b/java/service/org.eclipse.theia.cloud.service/README.md @@ -14,6 +14,15 @@ You can run your application in dev mode that enables live coding using: > **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at . +## Debug the service + +When you have the service running in dev mode you can use the `Attach to Service` debug config to debug the service. + +Check the [OpenAPI documentation](../../../documentation/OpenAPI.md) to see how you can send requests to the service. + +_Note_: The landing page will not communicate with this service, as it will be hosted on another address. +Checkout the [testing-page](../../../node/testing-page/README.md) to test requests against the server. + ## Packaging and running the application The application can be packaged using: @@ -49,7 +58,7 @@ Or, if you don't have GraalVM installed, you can run the native executable build ./mvnw package -Pnative -Dquarkus.native.container-build=true ``` -You can then execute your native executable with: `./target/service-1.0.0-SNAPSHOT-runner` +You can then execute your native executable with: `./target/service-1.2.0-SNAPSHOT-runner` If you want to learn more about building native executables, please consult . @@ -65,8 +74,6 @@ Easily start your RESTful Web Services Unit tests use the Quarkus test infrastructure. -All tests can be run in Eclipse via launch config [TheiaCloud Service Tests](./TheiaCloud Service Tests.launch). - When running tests from an IDE, the following VM argument might need to be set to enable logging during tests (this is pre-configured in the launch config): ```bash diff --git a/java/service/org.eclipse.theia.cloud.service/pom.xml b/java/service/org.eclipse.theia.cloud.service/pom.xml index 966d5e2f7..ff4d31ffe 100644 --- a/java/service/org.eclipse.theia.cloud.service/pom.xml +++ b/java/service/org.eclipse.theia.cloud.service/pom.xml @@ -9,7 +9,7 @@ org.eclipse.theia.cloud conf - 0.12.0-SNAPSHOT + 1.2.0-SNAPSHOT ../../common/maven-conf/ @@ -59,20 +59,24 @@ rest-assured test - + io.quarkus - quarkus-resteasy-reactive-jackson + quarkus-rest-jackson io.quarkus quarkus-smallrye-openapi + + io.quarkus + quarkus-smallrye-metrics + org.eclipse.theia.cloud common - 0.12.0-SNAPSHOT + 1.2.0-SNAPSHOT io.fabric8 @@ -111,8 +115,8 @@ maven-compiler-plugin ${maven-compiler-plugin.version} - 17 - 17 + 21 + 21 -parameters @@ -164,5 +168,38 @@ native + + sentry + + + sentry + true + + + + + + io.sentry + sentry-maven-plugin + 0.10.0 + true + + aet + theia + ${env.SENTRY_AUTH_TOKEN} + https://sentry.aet.cit.tum.de + + + + + uploadSourceBundle + validateSdkDependencyVersions + + + + + + + - \ No newline at end of file + diff --git a/java/service/org.eclipse.theia.cloud.service/sentry.properties b/java/service/org.eclipse.theia.cloud.service/sentry.properties new file mode 100644 index 000000000..4524a9fc1 --- /dev/null +++ b/java/service/org.eclipse.theia.cloud.service/sentry.properties @@ -0,0 +1,6 @@ +# Add your Sentry DSN here +dsn=https://a3851baf2ece0c6bec13e1c99fd3111f@sentry.aet.cit.tum.de/13 +# Add data like request headers and IP for users, +# see https://docs.sentry.io/platforms/java/data-management/data-collected/ for more info +send-default-pii=false +traces-sample-rate=1.0 \ No newline at end of file diff --git a/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/AdminApiTokenProtected.java b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/AdminApiTokenProtected.java new file mode 100644 index 000000000..f02874c0b --- /dev/null +++ b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/AdminApiTokenProtected.java @@ -0,0 +1,36 @@ +/******************************************************************************** + * Copyright (C) 2026 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.service; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import jakarta.ws.rs.NameBinding; + +/** + * Annotation to mark resources protected by an admin API token. + */ +@Inherited +@NameBinding +@Retention(RUNTIME) +@Target({ TYPE, METHOD }) +public @interface AdminApiTokenProtected { +} diff --git a/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/AdminOnly.java b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/AdminOnly.java new file mode 100644 index 000000000..cc81de349 --- /dev/null +++ b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/AdminOnly.java @@ -0,0 +1,43 @@ +/******************************************************************************** + * Copyright (C) 2025 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.service; + +import jakarta.ws.rs.NameBinding; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Inherited; + +/** + *

+ * Annotation to mark a resource or method as only accessible to admin users. + *

+ *

+ * Can be applied to a method or a resource class. In the latter case, the behavior applies to all its methods. + *

+ * + * @see AdminOnlyFilter + * @see TheiaCloudUserProducer + */ +@Inherited +@NameBinding +@Retention(RUNTIME) +@Target({ TYPE, METHOD }) +public @interface AdminOnly { +} diff --git a/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/AdminOnlyFilter.java b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/AdminOnlyFilter.java new file mode 100644 index 000000000..c7be2d3da --- /dev/null +++ b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/AdminOnlyFilter.java @@ -0,0 +1,51 @@ +/******************************************************************************** + * Copyright (C) 2025 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.service; + +import org.jboss.logging.Logger; + +import jakarta.annotation.Priority; +import jakarta.inject.Inject; +import jakarta.ws.rs.Priorities; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerRequestFilter; +import jakarta.ws.rs.ext.Provider; +import jakarta.ws.rs.core.Response; + +/** + * ContainerRequestFilter aborting all requests from non admin users to resources annotated with {@link AdminOnly}. + */ +@Provider +@AdminOnly +@Priority(Priorities.AUTHORIZATION) +public class AdminOnlyFilter implements ContainerRequestFilter { + + @Inject + Logger logger; + + @Inject + TheiaCloudUser theiaCloudUser; + + @Override + public void filter(ContainerRequestContext requestContext) { + if (!theiaCloudUser.isAdmin()) { + logger.infov("Blocked access to {0} {1} for non-admin user: {2}", requestContext.getMethod(), + requestContext.getUriInfo().getPath(), theiaCloudUser.getIdentifier()); + requestContext.abortWith(Response.status(Response.Status.FORBIDDEN) + .entity("Admin privileges required to access this resource.").build()); + } + } +} diff --git a/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/AppDefinitionAdminApiTokenFilter.java b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/AppDefinitionAdminApiTokenFilter.java new file mode 100644 index 000000000..fbe84b811 --- /dev/null +++ b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/AppDefinitionAdminApiTokenFilter.java @@ -0,0 +1,72 @@ +/******************************************************************************** + * Copyright (C) 2026 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.service; + +import org.jboss.logging.Logger; + +import jakarta.annotation.Priority; +import jakarta.inject.Inject; +import jakarta.ws.rs.Priorities; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerRequestFilter; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.Provider; + +/** + * ContainerRequestFilter protecting app definition admin endpoints with a dedicated admin token header. + */ +@Provider +@AdminApiTokenProtected +@Priority(Priorities.AUTHORIZATION) +public class AppDefinitionAdminApiTokenFilter implements ContainerRequestFilter { + + private static final String ADMIN_API_TOKEN_HEADER = "X-Admin-Api-Token"; + + @Inject + Logger logger; + + @Inject + ApplicationProperties applicationProperties; + + @Override + public void filter(ContainerRequestContext requestContext) { + String configuredToken = applicationProperties.getAdminApiToken(); + if (configuredToken.isEmpty()) { + logger.warnv("Blocked access to {0} {1}: no admin API token is configured.", + requestContext.getMethod(), requestContext.getUriInfo().getPath()); + abort(requestContext, Response.Status.FORBIDDEN, "Admin API token authentication is not configured."); + return; + } + + String presentedToken = requestContext.getHeaderString(ADMIN_API_TOKEN_HEADER); + if (presentedToken == null || presentedToken.isBlank()) { + logger.infov("Blocked access to {0} {1}: missing admin API token header.", requestContext.getMethod(), + requestContext.getUriInfo().getPath()); + abort(requestContext, Response.Status.UNAUTHORIZED, "Admin API token required in X-Admin-Api-Token header."); + return; + } + + if (!configuredToken.equals(presentedToken)) { + logger.infov("Blocked access to {0} {1}: invalid admin API token.", requestContext.getMethod(), + requestContext.getUriInfo().getPath()); + abort(requestContext, Response.Status.FORBIDDEN, "Valid admin API token required."); + } + } + + private void abort(ContainerRequestContext requestContext, Response.Status status, String entity) { + requestContext.abortWith(Response.status(status).entity(entity).build()); + } +} diff --git a/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/ApplicationLifecycleListener.java b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/ApplicationLifecycleListener.java index ac1d2df7a..5520e7f01 100644 --- a/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/ApplicationLifecycleListener.java +++ b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/ApplicationLifecycleListener.java @@ -31,12 +31,17 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.event.Observes; +import io.sentry.Sentry; + @ApplicationScoped public class ApplicationLifecycleListener { protected Logger logger; protected void onStart(@Observes StartupEvent event) { logger = Logger.getLogger(getClass()); + Sentry.configureScope(scope -> { + scope.setTag("component", "service"); + }); logConfiguration(); } diff --git a/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/ApplicationProperties.java b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/ApplicationProperties.java index e73ac402f..bad523cc0 100644 --- a/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/ApplicationProperties.java +++ b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/ApplicationProperties.java @@ -25,29 +25,48 @@ @ApplicationScoped public class ApplicationProperties { - private static final String THEIA_CLOUD_APP_ID = "theia.cloud.app.id"; - private static final String THEIA_CLOUD_USE_KEYCLOAK = "theia.cloud.use.keycloak"; + private static final String THEIACLOUD_SERVICE_AUTH_TOKEN = "theia.cloud.service.auth.token"; + private static final String THEIACLOUD_APP_ID = "theia.cloud.app.id"; // Deprecated - use + // THEIACLOUD_SERVICE_AUTH_TOKEN + private static final String THEIACLOUD_USE_KEYCLOAK = "theia.cloud.use.keycloak"; + private static final String THEIACLOUD_ADMIN_GROUP_NAME = "theia.cloud.auth.admin.group"; + private static final String THEIACLOUD_ADMIN_API_TOKEN = "theia.cloud.admin.api.token"; + + private static final String DEFAULT_ADMIN_GROUP_NAME = "theia-cloud/admin"; private final Logger logger; private final boolean useKeycloak; - private final String appId; + private final String serviceAuthToken; + private final String adminGroupName; + private final String adminApiToken; public ApplicationProperties() { logger = Logger.getLogger(getClass()); - appId = System.getProperty(THEIA_CLOUD_APP_ID, "asdfghjkl"); + serviceAuthToken = getServiceAuthTokenWithFallback(); + adminGroupName = System.getProperty(THEIACLOUD_ADMIN_GROUP_NAME, DEFAULT_ADMIN_GROUP_NAME); + adminApiToken = getAdminApiTokenFromConfig(); // Only disable keycloak if the value was explicitly set to exactly "false". - useKeycloak = !"false".equals(System.getProperty(THEIA_CLOUD_USE_KEYCLOAK)); + useKeycloak = !"false".equals(System.getProperty(THEIACLOUD_USE_KEYCLOAK)); if (!useKeycloak) { logger.warn("Keycloak integration was disabled. Anonymous requests are allowed!"); } } /** - * @return the configured application id + * @return the configured service auth token + */ + public String getServiceAuthToken() { + return serviceAuthToken; + } + + /** + * @deprecated Use {@link #getServiceAuthToken()} instead. This method is maintained for backwards compatibility. + * @return the configured service auth token */ + @Deprecated(since = "1.2.0", forRemoval = true) public String getAppId() { - return appId; + return serviceAuthToken; } /** @@ -56,4 +75,42 @@ public String getAppId() { public boolean isUseKeycloak() { return useKeycloak; } + + /** + * @return the group name that identifies admin users in the MicroProfile JWT token's groups claim + */ + public String getAdminGroupName() { + return adminGroupName; + } + + /** + * @return configured admin API token or an empty string if not configured. + */ + public String getAdminApiToken() { + return adminApiToken; + } + + /** + * Get the service auth token with fallback to deprecated app id property. Logs a deprecation warning if the old + * property is used. + */ + private String getServiceAuthTokenWithFallback() { + String serviceAuthToken = System.getProperty(THEIACLOUD_SERVICE_AUTH_TOKEN); + if (serviceAuthToken != null) { + return serviceAuthToken; + } + + String appId = System.getProperty(THEIACLOUD_APP_ID); + if (appId != null) { + logger.warn("Using deprecated property '" + THEIACLOUD_APP_ID + "'. " + "Please migrate to '" + + THEIACLOUD_SERVICE_AUTH_TOKEN + "' in your configuration."); + return appId; + } + + return "asdfghjkl"; // Default value + } + + private String getAdminApiTokenFromConfig() { + return System.getProperty(THEIACLOUD_ADMIN_API_TOKEN, "").trim(); + } } diff --git a/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/BaseResource.java b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/BaseResource.java index a86692a40..4d777ac82 100644 --- a/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/BaseResource.java +++ b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/BaseResource.java @@ -35,10 +35,11 @@ public class BaseResource { public BaseResource(ApplicationProperties applicationProperties) { this.applicationProperties = applicationProperties; - appId = applicationProperties.getAppId(); + appId = applicationProperties.getServiceAuthToken(); logger = Logger.getLogger(getClass().getSuperclass()); } + /** @return The correlation id for this request. */ protected String evaluateRequest(ServiceRequest request) { return basicEvaluateRequest(request); } @@ -81,7 +82,7 @@ protected EvaluatedRequest evaluateRequest(UserScopedServiceRequest request) { // (this might change if the concept of admin users is introduced later) if (theiaCloudUser.isAnonymous()) { info(correlationId, - "User is unexpectetly considered anonymous and, thus, must not access user scoped resources."); + "User is unexpectedly considered anonymous and, thus, must not access user scoped resources."); throw new TheiaCloudWebException(Status.UNAUTHORIZED); } else if (request.user == null || request.user.equals(theiaCloudUser.getIdentifier())) { return new EvaluatedRequest(correlationId, theiaCloudUser.getIdentifier()); @@ -100,6 +101,10 @@ public void warn(String correlationId, String message) { logger.warn(LogMessageUtil.formatLogMessage(correlationId, message)); } + public void warn(String correlationId, String message, Throwable throwable) { + logger.warn(LogMessageUtil.formatLogMessage(correlationId, message), throwable); + } + public void error(String correlationId, String message) { logger.error(LogMessageUtil.formatLogMessage(correlationId, message)); } diff --git a/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/ConfigurableAnonymousIdentityProvider.java b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/ConfigurableAnonymousIdentityProvider.java index d7e768c8b..c133e5e18 100644 --- a/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/ConfigurableAnonymousIdentityProvider.java +++ b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/ConfigurableAnonymousIdentityProvider.java @@ -28,7 +28,7 @@ * Custom {@link io.quarkus.security.identity.IdentityProvider IdentityProvider} that authenticates anonymous users if * the Theia Cloud service's usage of keycloak was disabled. This facilitates configuring resources as generally * requiring authentication while still being able to use the service without any authentication by setting the - * {@value #THEIA_CLOUD_USE_KEYCLOAK} system property. + * {@value #THEIACLOUD_USE_KEYCLOAK} system property. */ @ApplicationScoped public class ConfigurableAnonymousIdentityProvider extends AnonymousIdentityProvider { diff --git a/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/K8sUtil.java b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/K8sUtil.java index d138cf85d..0aa561555 100644 --- a/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/K8sUtil.java +++ b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/K8sUtil.java @@ -18,34 +18,39 @@ import static org.eclipse.theia.cloud.common.util.WorkspaceUtil.getSessionName; +import java.text.MessageFormat; import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.function.Consumer; import java.util.stream.Collectors; import org.eclipse.theia.cloud.common.k8s.client.DefaultTheiaCloudClient; import org.eclipse.theia.cloud.common.k8s.client.TheiaCloudClient; import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinition; +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinitionSpec; import org.eclipse.theia.cloud.common.k8s.resource.session.Session; import org.eclipse.theia.cloud.common.k8s.resource.session.SessionSpec; import org.eclipse.theia.cloud.common.k8s.resource.session.SessionStatus; import org.eclipse.theia.cloud.common.k8s.resource.workspace.Workspace; import org.eclipse.theia.cloud.common.k8s.resource.workspace.WorkspaceSpec; import org.eclipse.theia.cloud.common.util.CustomResourceUtil; +import org.eclipse.theia.cloud.common.util.LogMessageUtil; import org.eclipse.theia.cloud.service.session.SessionPerformance; import org.eclipse.theia.cloud.service.workspace.UserWorkspace; import org.jboss.logging.Logger; -import io.fabric8.kubernetes.api.model.Container; -import io.fabric8.kubernetes.api.model.EnvVar; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.PodList; import io.fabric8.kubernetes.api.model.Quantity; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.api.model.apps.ReplicaSet; import io.fabric8.kubernetes.api.model.metrics.v1beta1.ContainerMetrics; import io.fabric8.kubernetes.api.model.metrics.v1beta1.PodMetrics; import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.NamespacedKubernetesClient; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.core.Response.Status; @ApplicationScoped public final class K8sUtil { @@ -54,6 +59,10 @@ public final class K8sUtil { protected final Logger logger = Logger.getLogger(getClass()); + public String getNamespace() { + return CLIENT.namespace(); + } + public Workspace createWorkspace(String correlationId, UserWorkspace data) { WorkspaceSpec spec = new WorkspaceSpec(data.name, data.label, data.appDefinition, data.user); return CLIENT.workspaces().launch(correlationId, spec); @@ -68,26 +77,53 @@ public boolean deleteWorkspace(String correlationId, String workspaceName) { return true; } + public List listAppDefinitions() { + return CLIENT.appDefinitions().specs(); + } + + public List listAppDefinitionResources() { + return CLIENT.appDefinitions().list(); + } + public List listSessions(String user) { return CLIENT.sessions().specs(user); } + @Deprecated(forRemoval = true) + /** @deprecated use findSession instead */ public Optional findExistingSession(SessionSpec spec) { return CLIENT.sessions().specs().stream().filter(sessionSpec -> sessionSpec.equals(spec)).findAny(); } - public Optional findSession(String sessionName) { - return CLIENT.sessions().get(sessionName).map(Session::getSpec); + public Optional findSession(String sessionName) { + return CLIENT.sessions().get(sessionName); } public String launchEphemeralSession(String correlationId, String appDefinition, String user, int timeout, EnvironmentVars env) { + Optional appDef = CLIENT.appDefinitions().get(appDefinition); + if (appDef.isPresent() && requiresSharedWorkspace(appDef.get().getSpec())) { + logger.error(LogMessageUtil.formatLogMessage(correlationId, + "Refusing ephemeral session launch for app definition '" + appDefinition + + "' because one or more sidecars mount the workspace. Use workspace-backed session launch.")); + throw new TheiaCloudWebException(Status.BAD_REQUEST, + "Ephemeral sessions are not supported for app definitions whose sidecars mount the workspace. Use a workspace-backed session."); + } + SessionSpec sessionSpec = new SessionSpec(getSessionName(user, appDefinition, false), appDefinition, user); sessionSpec = sessionSpecWithEnv(sessionSpec, env); return launchSession(correlationId, sessionSpec, timeout); } + private boolean requiresSharedWorkspace(AppDefinitionSpec spec) { + if (spec == null) { + return false; + } + + return spec.requiresSharedWorkspace(); + } + public String launchWorkspaceSession(String correlationId, UserWorkspace workspace, int timeout, EnvironmentVars env) { SessionSpec sessionSpec = new SessionSpec(getSessionName(workspace.name), workspace.appDefinition, @@ -149,11 +185,15 @@ public List listWorkspaces(String user) { public SessionPerformance reportPerformance(String sessionName) { Optional optionalSession = CLIENT.sessions().get(sessionName); if (optionalSession.isEmpty()) { + logger.warn(MessageFormat.format("Cannot get performance data for session {0} because it does not exist.", + sessionName)); return null; } Session session = optionalSession.get(); Optional optionalPod = getPodForSession(session); if (optionalPod.isEmpty()) { + logger.warn(MessageFormat.format("Cannot get performance data for session {0} because no pod was found.", + sessionName)); return null; } PodMetrics test = CLIENT.kubernetes().top().pods().metrics(CLIENT.namespace(), @@ -161,6 +201,9 @@ public SessionPerformance reportPerformance(String sessionName) { Optional optionalContainer = test.getContainers().stream() .filter(con -> con.getName().equals(session.getSpec().getAppDefinition())).findFirst(); if (optionalContainer.isEmpty()) { + logger.warn(MessageFormat.format( + "Cannot get performance data for session {0} because the app container was not found in the pod.", + sessionName)); return null; } ContainerMetrics container = optionalContainer.get(); @@ -174,24 +217,52 @@ public Optional getPodForSession(Session session) { return podlist.getItems().stream().filter(pod -> isPodFromSession(pod, session)).findFirst(); } + /** + * Checks whether a Pod belongs to a Session by resolving the Pod's Deployment and checking whether the Deployment + * is owned by the Session. + */ private boolean isPodFromSession(Pod pod, Session session) { - Optional optionalContainer = pod.getSpec().getContainers().stream() - .filter(con -> con.getName().equals(session.getSpec().getAppDefinition())).findFirst(); - if (optionalContainer.isEmpty()) { - return false; - } - Container container = optionalContainer.get(); - Optional optionalEnv = container.getEnv().stream() - .filter(env -> env.getName().equals("THEIACLOUD_SESSION_NAME")).findFirst(); - if (optionalEnv.isEmpty()) { - return false; - } - EnvVar env = optionalEnv.get(); - return env.getValue().equals(session.getSpec().getName()); + return getDeploymentForPod(pod)// + .flatMap(deployment -> deployment.getOwnerReferenceFor(session.getMetadata().getUid()))// + .isPresent(); + } + + /** + *

+ * Returns the Deployment associated with a given Pod. + *

+ *

+ * The deployment is retrieved by following the owner references of the Pod. The Pod's owner references are filtered + * to find the ReplicaSet. Then, the ReplicaSet's owner references are filtered to find the Deployment and return + * it. + *

+ * + * @param Pod the Pod for which to retrieve the Deployment. + * @return the Deployment associated with the given Pod or an empty Optional if not found. + */ + private Optional getDeploymentForPod(Pod pod) { + Optional replicaSet = pod.getMetadata().getOwnerReferences().stream() + .filter(ownerReference -> "ReplicaSet".equals(ownerReference.getKind())) + .map(ownerReference -> CLIENT.apps().replicaSets().withName(ownerReference.getName()).get()) + .filter(Objects::nonNull).findFirst(); + + return replicaSet.flatMap(rs -> rs.getMetadata().getOwnerReferences().stream() + .filter(rsOwnerReference -> "Deployment".equals(rsOwnerReference.getKind())) + .map(rsOwnerReference -> CLIENT.apps().deployments().withName(rsOwnerReference.getName()).get()) + .filter(Objects::nonNull).findFirst()); } public boolean hasAppDefinition(String appDefinition) { - return CLIENT.appDefinitions().get(appDefinition).isPresent(); + return getAppDefinition(appDefinition).isPresent(); + } + + public Optional getAppDefinition(String appDefinition) { + return CLIENT.appDefinitions().get(appDefinition); + } + + public AppDefinition editAppDefinition(String correlationId, String appDefinition, + Consumer consumer) { + return CLIENT.appDefinitions().edit(correlationId, appDefinition, consumer); } public boolean isMaxInstancesReached(String appDefString) { @@ -207,9 +278,11 @@ public boolean isMaxInstancesReached(String appDefString) { if (maxInstances < 0) { return false; // max instances is set to negative, so we can ignore it } - long podsOfAppDef = CLIENT.sessions().list().stream() // All sessions + + long sessionsOfAppDef = CLIENT.sessions().list().stream() // All sessions .filter(s -> s.getSpec().getAppDefinition().equals(appDefString)) // That are from the appDefinition - .filter(s -> getPodForSession(s).isPresent()).count(); // That already have a pod - return podsOfAppDef >= maxInstances; + .filter(s -> s.getStatus() == null || !s.getStatus().hasError()) // That are not in error state + .count(); + return sessionsOfAppDef >= maxInstances; } } diff --git a/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/RootResource.java b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/RootResource.java index 21a195e34..15fda84a4 100644 --- a/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/RootResource.java +++ b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/RootResource.java @@ -20,6 +20,10 @@ import java.util.Optional; import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.metrics.MetricRegistry; +import org.eclipse.microprofile.metrics.Tag; +import org.eclipse.microprofile.metrics.Timer; +import io.sentry.Sentry; import org.eclipse.theia.cloud.common.k8s.resource.workspace.Workspace; import org.eclipse.theia.cloud.common.util.TheiaCloudError; import org.eclipse.theia.cloud.service.workspace.UserWorkspace; @@ -44,6 +48,9 @@ public RootResource(ApplicationProperties applicationProperties) { @Inject private K8sUtil k8sUtil; + @Inject + private MetricRegistry metricRegistry; + @Operation(summary = "Ping", description = "Replies if the service is available.") @GET @Path("/{appId}") @@ -60,55 +67,90 @@ public String launch(LaunchRequest request) { final String correlationId = evaluatedRequest.getCorrelationId(); final String user = evaluatedRequest.getUser(); - if (!k8sUtil.hasAppDefinition(request.appDefinition)) { - error(correlationId, - "Failed to launch session. App Definition '" + request.appDefinition + "' does not exist."); - throw new TheiaCloudWebException(TheiaCloudError.INVALID_APP_DEFINITION_NAME); - } + String sanitizedAppDef = sanitizeMetricTagValue(request.appDefinition); + String sanitizedNamespace = sanitizeMetricTagValue(k8sUtil.getNamespace()); + Tag appDefinitionTag = new Tag("app_definition", sanitizedAppDef); + Tag namespaceTag = new Tag("namespace", sanitizedNamespace); - if (k8sUtil.isMaxInstancesReached(request.appDefinition)) { - error(correlationId, "Failed to launch session. App Definition '" + request.appDefinition - + "' has max instances reached."); - throw new TheiaCloudWebException(TheiaCloudError.SESSION_SERVER_LIMIT_REACHED); - } + // Get or create timer with dynamic tags + Timer timer = metricRegistry.timer( + "application.theiacloud.session.startup.seconds", + appDefinitionTag, + namespaceTag + ); - if (request.isEphemeral()) { - info(correlationId, "Launching ephemeral session " + request); - return k8sUtil.launchEphemeralSession(correlationId, request.appDefinition, user, request.timeout, - request.env); - } + // Start timing + Timer.Context timerContext = timer.time(); - if (request.isExistingWorkspace()) { - Optional workspace = k8sUtil.getWorkspace(user, asValidName(request.workspaceName)); - if (workspace.isPresent()) { - String workspaceAppDefinition = workspace.get().getSpec().getAppDefinition(); - if (!workspaceAppDefinition.equals(request.appDefinition)) { - error(correlationId, - "Failed to launch session. Workspace App Definition '" + workspaceAppDefinition - + "' does not match Request App Definition '" + request.appDefinition - + "' does not exist."); - throw new TheiaCloudWebException(TheiaCloudError.APP_DEFINITION_NAME_MISMATCH); - } + try { + if (!k8sUtil.hasAppDefinition(request.appDefinition)) { + error(correlationId, + "Failed to launch session. App Definition '" + request.appDefinition + "' does not exist."); + throw new TheiaCloudWebException(TheiaCloudError.INVALID_APP_DEFINITION_NAME); + } - info(correlationId, "Launching existing workspace session " + request); - return k8sUtil.launchWorkspaceSession(correlationId, new UserWorkspace(workspace.get().getSpec()), - request.timeout, request.env); + if (k8sUtil.isMaxInstancesReached(request.appDefinition)) { + error(correlationId, "Failed to launch session. App Definition '" + request.appDefinition + + "' has max instances reached."); + throw new TheiaCloudWebException(TheiaCloudError.SESSION_SERVER_LIMIT_REACHED); + } + + if (request.isEphemeral()) { + info(correlationId, "Launching ephemeral session " + request); + return k8sUtil.launchEphemeralSession(correlationId, request.appDefinition, user, request.timeout, + request.env); + } + + if (request.isExistingWorkspace()) { + Optional workspace = k8sUtil.getWorkspace(user, asValidName(request.workspaceName)); + if (workspace.isPresent()) { + String workspaceAppDefinition = workspace.get().getSpec().getAppDefinition(); + if (!workspaceAppDefinition.equals(request.appDefinition)) { + error(correlationId, + "Failed to launch session. Workspace App Definition '" + workspaceAppDefinition + + "' does not match Request App Definition '" + request.appDefinition + + "' does not exist."); + throw new TheiaCloudWebException(TheiaCloudError.APP_DEFINITION_NAME_MISMATCH); + } + + info(correlationId, "Launching existing workspace session " + request); + return k8sUtil.launchWorkspaceSession(correlationId, new UserWorkspace(workspace.get().getSpec()), + request.timeout, request.env); + } } - } - info(correlationId, "Create workspace " + request); - Workspace workspace = k8sUtil.createWorkspace(correlationId, - new UserWorkspace(request.appDefinition, user, request.workspaceName, request.label)); - TheiaCloudWebException.throwIfErroneous(workspace); + info(correlationId, "Create workspace " + request); + Workspace workspace = k8sUtil.createWorkspace(correlationId, + new UserWorkspace(request.appDefinition, user, request.workspaceName, request.label)); + TheiaCloudWebException.throwIfErroneous(workspace); - info(correlationId, "Launch workspace session " + request); - try { - return k8sUtil.launchWorkspaceSession(correlationId, new UserWorkspace(workspace.getSpec()), - request.timeout, request.env); + info(correlationId, "Launch workspace session " + request); + try { + return k8sUtil.launchWorkspaceSession(correlationId, new UserWorkspace(workspace.getSpec()), + request.timeout, request.env); + } catch (Exception exception) { + info(correlationId, "Delete workspace due to launch error " + request); + k8sUtil.deleteWorkspace(correlationId, workspace.getSpec().getName()); + throw exception; + } } catch (Exception exception) { - info(correlationId, "Delete workspace due to launch error " + request); - k8sUtil.deleteWorkspace(correlationId, workspace.getSpec().getName()); + error(correlationId, "Failed to launch session " + request, exception); + Sentry.captureException(exception); throw exception; + } finally { + // Stop timing and record the measurement + timerContext.stop(); + } + } + + private String sanitizeMetricTagValue(String value) { + if (value == null || value.isEmpty()) { + return "unknown"; + } + String sanitized = value.replaceAll("[^a-zA-Z0-9_]", "_"); + if (!sanitized.isEmpty() && Character.isDigit(sanitized.charAt(0))) { + sanitized = "_" + sanitized; } + return sanitized.isEmpty() ? "unknown" : sanitized; } } diff --git a/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/TheiaCloudApiApplication.java b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/TheiaCloudApiApplication.java index b3cfceb30..3dd03f910 100644 --- a/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/TheiaCloudApiApplication.java +++ b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/TheiaCloudApiApplication.java @@ -20,6 +20,6 @@ import jakarta.ws.rs.core.Application; -@OpenAPIDefinition(info = @Info(title = "Theia Cloud API", version = "0.12.0")) +@OpenAPIDefinition(info = @Info(title = "Theia Cloud API", version = "1.2.0")) public class TheiaCloudApiApplication extends Application { } diff --git a/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/TheiaCloudUser.java b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/TheiaCloudUser.java index 34a2bc9be..101f2b3fa 100644 --- a/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/TheiaCloudUser.java +++ b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/TheiaCloudUser.java @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (C) 2022-2023 EclipseSource, STMicroelectronics and others. + * Copyright (C) 2022-2025 EclipseSource, STMicroelectronics and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at @@ -22,12 +22,29 @@ */ public class TheiaCloudUser { - public static TheiaCloudUser ANONYMOUS = new TheiaCloudUser(null); + public static final TheiaCloudUser ANONYMOUS = new TheiaCloudUser(null, false); - private String identifier; + private final String identifier; + private final boolean admin; + /** + * Creates a non-admin user. + * + * @param identifier the user identifier + */ public TheiaCloudUser(String identifier) { + this(identifier, false); + } + + /** + * Creates a user with the specified admin flag. + * + * @param identifier the user identifier + * @param admin {@code true} if the user is an admin + */ + public TheiaCloudUser(String identifier, boolean admin) { this.identifier = identifier; + this.admin = admin; } /** @@ -41,13 +58,27 @@ public String getIdentifier() { return identifier; } + /** + * Returns whether the user is an admin. + * + * @return {@code true} if admin and not anonymous, {@code false} otherwise + */ + public boolean isAdmin() { + return !isAnonymous() && admin; + } + + /** + * Determines if the user is anonymous. + * + * @return {@code true} if the user is anonymous + */ public boolean isAnonymous() { return identifier == null || identifier.isBlank(); } @Override public int hashCode() { - return Objects.hash(identifier); + return Objects.hash(identifier, admin); } @Override @@ -59,12 +90,11 @@ public boolean equals(Object obj) { if (getClass() != obj.getClass()) return false; TheiaCloudUser other = (TheiaCloudUser) obj; - return Objects.equals(identifier, other.identifier); + return Objects.equals(identifier, other.identifier) && admin == other.admin; } @Override public String toString() { - return "TheiaCloudUser [identifier=" + identifier + "]"; + return "TheiaCloudUser [identifier=" + identifier + ", admin=" + admin + "]"; } - } diff --git a/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/TheiaCloudUserProducer.java b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/TheiaCloudUserProducer.java index 83e61b865..6a1c7c16f 100644 --- a/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/TheiaCloudUserProducer.java +++ b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/TheiaCloudUserProducer.java @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (C) 2022-2023 EclipseSource, STMicroelectronics and others. + * Copyright (C) 2022-2025 EclipseSource, STMicroelectronics and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at @@ -15,6 +15,8 @@ ********************************************************************************/ package org.eclipse.theia.cloud.service; +import java.util.Set; + import org.eclipse.microprofile.jwt.Claims; import org.eclipse.microprofile.jwt.JsonWebToken; import org.jboss.logging.Logger; @@ -32,6 +34,13 @@ *

* With this, the {@link TheiaCloudUser} can directly be injected into any resource. *

+ *

+ * This producer derives the user identity from an OIDC token. Thereby, the following claims from the MicroProfile JWT + * specification are used: + *

    + *
  • {@link Claims#email email} - The user's email address as their unique identifier
  • + *
  • {@link Claims#groups groups} - The user's groups to determine additional permissions, i.e. admin users
  • + *
*/ @RequestScoped public class TheiaCloudUserProducer { @@ -41,6 +50,9 @@ public class TheiaCloudUserProducer { @Inject private SecurityIdentity identity; + @Inject + private ApplicationProperties applicationProperties; + @Produces @RequestScoped TheiaCloudUser getTheiaCloudUser() { @@ -56,7 +68,19 @@ TheiaCloudUser getTheiaCloudUser() { logger.error("Cannot create user identity: The email claim is not available. Treat user as anonymous."); return TheiaCloudUser.ANONYMOUS; } - return new TheiaCloudUser(email); + + // Check if the user is an admin by looking for the configured admin group name in the groups claim. + boolean isAdmin = false; + Set groupsClaim = jwt.getClaim(Claims.groups); + if (groupsClaim != null) { + if (groupsClaim.contains(applicationProperties.getAdminGroupName())) { + isAdmin = true; + } + } else { + logger.warn("JWT groups claim is null. Cannot grant additional user privileges."); + } + + return new TheiaCloudUser(email, isAdmin); } else if (identity.getPrincipal() instanceof AnonymousPrincipal) { // When keycloak is disabled, the security identity is authenticated, i.e. not // anonymous. However, the user still has no identity and, thus, is regarded as diff --git a/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/admin/RootAdminResource.java b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/admin/RootAdminResource.java new file mode 100644 index 000000000..57e514021 --- /dev/null +++ b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/admin/RootAdminResource.java @@ -0,0 +1,45 @@ +/******************************************************************************** + * Copyright (C) 2025 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.service.admin; + +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.theia.cloud.service.AdminOnly; +import org.eclipse.theia.cloud.service.ApplicationProperties; +import org.eclipse.theia.cloud.service.BaseResource; +import org.eclipse.theia.cloud.service.PingRequest; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; + +@Path("/service/admin") +@AdminOnly +public class RootAdminResource extends BaseResource { + + @Inject + public RootAdminResource(ApplicationProperties applicationProperties) { + super(applicationProperties); + } + + @Operation(summary = "Admin Ping", description = "Replies with success if the service is available and the user an admin.") + @GET + @Path("/{appId}") + public boolean ping(@PathParam("appId") String appId) { + evaluateRequest(new PingRequest(appId)); + return true; + } +} diff --git a/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/admin/appdefinition/AppDefinitionAdminResource.java b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/admin/appdefinition/AppDefinitionAdminResource.java new file mode 100644 index 000000000..12dd1324a --- /dev/null +++ b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/admin/appdefinition/AppDefinitionAdminResource.java @@ -0,0 +1,139 @@ +/******************************************************************************** + * Copyright (C) 2025 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.service.admin.appdefinition; + +import java.util.List; +import java.util.Optional; + +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinition; +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinitionSpec; +import org.eclipse.theia.cloud.service.AdminApiTokenProtected; +import org.eclipse.theia.cloud.service.ApplicationProperties; +import org.eclipse.theia.cloud.service.BaseResource; +import org.eclipse.theia.cloud.service.K8sUtil; +import jakarta.inject.Inject; +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.InternalServerErrorException; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.PATCH; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +/** + * Resource for admin operations on app definitions. + */ +@Path("/service/admin/appdefinition") +@AdminApiTokenProtected +public class AppDefinitionAdminResource extends BaseResource { + + @Inject + private K8sUtil k8sUtil; + + @Inject + public AppDefinitionAdminResource(ApplicationProperties applicationProperties) { + super(applicationProperties); + } + + @Operation(summary = "List scaling settings for all app definitions", description = "Lists minInstances and maxInstances for all app definitions.") + @GET + @Produces(MediaType.APPLICATION_JSON) + public List list() { + return k8sUtil.listAppDefinitionResources().stream().map(this::toScaling).toList(); + } + + @Operation(summary = "Get scaling settings for an app definition", description = "Returns minInstances and maxInstances for a specific app definition.") + @Parameter(name = "appDefinitionName", description = "The K8S resource name of the app definition.") + @GET + @Path("/{appDefinitionName}") + @Produces(MediaType.APPLICATION_JSON) + public AppDefinitionScaling get(@PathParam("appDefinitionName") String appDefinitionName) { + AppDefinition appDefinition = k8sUtil.getAppDefinition(appDefinitionName) + .orElseThrow(() -> new NotFoundException("App definition does not exist.")); + return toScaling(appDefinition); + } + + @Operation(summary = "Updates an app definition", description = "Updates an app definition's properties. Allowed properties to update are defined by AppDefinitionUpdateRequest.") + @Parameter(name = "appDefinitionName", description = "The K8S resource name of the app definition to update.") + @PATCH + @Path("/{appDefinitionName}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @RequestBody(required = true) + public AppDefinition update(@PathParam("appDefinitionName") String appDefinitionName, + AppDefinitionUpdateRequest request) { + if (request == null) { + throw new BadRequestException("Request body is required."); + } + String correlationId = "appdefinition-admin-update"; + Optional appDefinition = k8sUtil.getAppDefinition(appDefinitionName); + if (appDefinition.isEmpty()) { + throw new NotFoundException("App definition does not exist."); + } + validateUpdateRequest(request, appDefinition.get().getSpec()); + + info(correlationId, "Update app definition " + request); + try { + return k8sUtil.editAppDefinition(correlationId, appDefinitionName, appDef -> { + AppDefinitionSpec spec = appDef.getSpec(); + if (request.minInstances != null) { + spec.setMinInstances(request.minInstances); + } + if (request.maxInstances != null) { + spec.setMaxInstances(request.maxInstances); + } + }); + } catch (BadRequestException e) { + throw e; + } catch (Exception e) { + error(correlationId, "Failed to update app definition ", e); + throw new InternalServerErrorException( + "Failed to update app definition. See the service logs for more details."); + } + } + + private void validateUpdateRequest(AppDefinitionUpdateRequest request, AppDefinitionSpec existingSpec) { + if (request.minInstances == null && request.maxInstances == null) { + throw new BadRequestException("At least one of minInstances or maxInstances must be set."); + } + + int resultingMin = request.minInstances != null ? request.minInstances : existingSpec.getMinInstances(); + Integer resultingMax = request.maxInstances != null ? request.maxInstances : existingSpec.getMaxInstances(); + + if (resultingMin < 0) { + throw new BadRequestException("minInstances must be greater than or equal to 0."); + } + if (resultingMax != null && resultingMax < 0) { + throw new BadRequestException("maxInstances must be greater than or equal to 0."); + } + if (resultingMax != null && resultingMin > resultingMax) { + throw new BadRequestException("minInstances must be less than or equal to maxInstances."); + } + } + + private AppDefinitionScaling toScaling(AppDefinition appDefinition) { + String resourceName = appDefinition.getMetadata() != null ? appDefinition.getMetadata().getName() : null; + AppDefinitionSpec spec = appDefinition.getSpec(); + return new AppDefinitionScaling(resourceName != null ? resourceName : spec.getName(), spec.getMinInstances(), + spec.getMaxInstances()); + } +} diff --git a/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/admin/appdefinition/AppDefinitionScaling.java b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/admin/appdefinition/AppDefinitionScaling.java new file mode 100644 index 000000000..9a46f18a6 --- /dev/null +++ b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/admin/appdefinition/AppDefinitionScaling.java @@ -0,0 +1,40 @@ +/******************************************************************************** + * Copyright (C) 2026 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.service.admin.appdefinition; + +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Schema(name = "AppDefinitionScaling", description = "Scaling configuration of an app definition.") +public class AppDefinitionScaling { + + @Schema(description = "The K8S resource name of the app definition.", required = true) + public String appDefinitionName; + + @Schema(description = "The minimum number of instances to run.", required = true) + public int minInstances; + + @Schema(description = "The maximum number of instances to run. Null means unbounded.", required = false) + public Integer maxInstances; + + public AppDefinitionScaling() { + } + + public AppDefinitionScaling(String appDefinitionName, int minInstances, Integer maxInstances) { + this.appDefinitionName = appDefinitionName; + this.minInstances = minInstances; + this.maxInstances = maxInstances; + } +} diff --git a/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/admin/appdefinition/AppDefinitionUpdateRequest.java b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/admin/appdefinition/AppDefinitionUpdateRequest.java new file mode 100644 index 000000000..ac97559d9 --- /dev/null +++ b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/admin/appdefinition/AppDefinitionUpdateRequest.java @@ -0,0 +1,32 @@ +/******************************************************************************** + * Copyright (C) 2025 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.service.admin.appdefinition; + +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +public class AppDefinitionUpdateRequest { + + @Schema(description = "The minimum number of instances to run.", required = false) + public Integer minInstances; + + @Schema(description = "The maximum number of instances to run.", required = false) + public Integer maxInstances; + + @Override + public String toString() { + return "AppDefinitionUpdateRequest [minInstances=" + minInstances + ", maxInstances=" + maxInstances + "]"; + } +} diff --git a/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/appdefinition/AppDefinitionListRequest.java b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/appdefinition/AppDefinitionListRequest.java new file mode 100644 index 000000000..32268380a --- /dev/null +++ b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/appdefinition/AppDefinitionListRequest.java @@ -0,0 +1,38 @@ +/******************************************************************************** + * Copyright (C) 2024 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.service.appdefinition; + +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.theia.cloud.service.ServiceRequest; + +@Schema(name = "AppDefinitionListRequest", description = "A request to list available app definitions.") +public class AppDefinitionListRequest extends ServiceRequest { + public static final String KIND = "appDefinitionListRequest"; + + public AppDefinitionListRequest() { + super(KIND); + } + + public AppDefinitionListRequest(String appId) { + super(KIND, appId); + } + + @Override + public String toString() { + return "AppDefinitionListRequest [appId=" + appId + ", kind=" + kind + "]"; + } + +} diff --git a/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/appdefinition/AppDefinitionResource.java b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/appdefinition/AppDefinitionResource.java new file mode 100644 index 000000000..83959b94d --- /dev/null +++ b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/appdefinition/AppDefinitionResource.java @@ -0,0 +1,50 @@ +/******************************************************************************** + * Copyright (C) 2024 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.service.appdefinition; + +import java.util.List; + +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.theia.cloud.common.k8s.resource.appdefinition.AppDefinitionSpec; +import org.eclipse.theia.cloud.service.ApplicationProperties; +import org.eclipse.theia.cloud.service.BaseResource; +import org.eclipse.theia.cloud.service.K8sUtil; +import io.quarkus.security.Authenticated; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; + +@Path("/service/appdefinition") +@Authenticated +public class AppDefinitionResource extends BaseResource { + + @Inject + private K8sUtil k8sUtil; + + public AppDefinitionResource(ApplicationProperties applicationProperties) { + super(applicationProperties); + } + + @Operation(summary = "List app definitions", description = "List available app definitions.") + @GET + @Path("/{appId}") + public List list(@PathParam("appId") String appId) { + evaluateRequest(new AppDefinitionListRequest(appId)); + List appDefinitions = k8sUtil.listAppDefinitions(); + return appDefinitions; + } +} diff --git a/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/databridge/DataBridgeService.java b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/databridge/DataBridgeService.java new file mode 100644 index 000000000..592f7e701 --- /dev/null +++ b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/databridge/DataBridgeService.java @@ -0,0 +1,27 @@ +package org.eclipse.theia.cloud.service.databridge; + +import org.eclipse.theia.cloud.common.k8s.client.DataBridgeClient; +import org.eclipse.theia.cloud.common.k8s.client.DefaultDataBridgeClient; +import org.eclipse.theia.cloud.service.K8sUtil; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +/** + * Service for managing data bridge operations. + */ +@ApplicationScoped +public class DataBridgeService { + + private final DataBridgeClient client; + + @Inject + public DataBridgeService(K8sUtil k8sUtil) { + this.client = new DefaultDataBridgeClient(k8sUtil.CLIENT); + } + + public DataBridgeClient getClient() { + return client; + } +} + diff --git a/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/serialization/SensitiveDataCustomSerializerRegistration.java b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/serialization/SensitiveDataCustomSerializerRegistration.java new file mode 100644 index 000000000..b774e2421 --- /dev/null +++ b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/serialization/SensitiveDataCustomSerializerRegistration.java @@ -0,0 +1,24 @@ +package org.eclipse.theia.cloud.service.serialization; + +import jakarta.inject.Singleton; + +import org.eclipse.theia.cloud.common.serialization.SensitiveDataBeanSerializerModifier; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import io.quarkus.jackson.ObjectMapperCustomizer; + +/** + * Registers the {@link SensitiveDataBeanSerializerModifier} in the Jackson ObjectMapper used by Quarkus to respect the + * {@link org.eclipse.theia.cloud.common.serialization.SensitiveData SensitiveData} annotation. + */ +@Singleton +public class SensitiveDataCustomSerializerRegistration implements ObjectMapperCustomizer { + + @Override + public void customize(ObjectMapper objectMapper) { + SimpleModule module = new SimpleModule(); + module.setSerializerModifier(new SensitiveDataBeanSerializerModifier()); + objectMapper.registerModule(module); + } +} diff --git a/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/session/SessionResource.java b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/session/SessionResource.java index 17019f17a..f42b18177 100644 --- a/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/session/SessionResource.java +++ b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/session/SessionResource.java @@ -15,12 +15,17 @@ ********************************************************************************/ package org.eclipse.theia.cloud.service.session; +import java.text.MessageFormat; import java.util.List; import java.util.Optional; import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.theia.cloud.common.k8s.resource.session.Session; import org.eclipse.theia.cloud.common.k8s.resource.session.SessionSpec; import org.eclipse.theia.cloud.common.k8s.resource.workspace.Workspace; +import org.eclipse.theia.cloud.common.util.SessionUtil; import org.eclipse.theia.cloud.common.util.TheiaCloudError; import org.eclipse.theia.cloud.service.ApplicationProperties; import org.eclipse.theia.cloud.service.BaseResource; @@ -31,26 +36,41 @@ import org.eclipse.theia.cloud.service.workspace.UserWorkspace; import io.quarkus.security.Authenticated; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.client.WebClientOptions; +import io.vertx.mutiny.core.Vertx; +import io.vertx.mutiny.ext.web.client.WebClient; import jakarta.annotation.security.PermitAll; import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; +import jakarta.ws.rs.InternalServerErrorException; import jakarta.ws.rs.PATCH; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response.Status; @Authenticated @Path("/service/session") public class SessionResource extends BaseResource { + private static final String CONFIG_STORE_PATH = "/theia-cloud/config-store"; + /** Timeout in milliseconds for calls to a session's config store. */ + private static final long CONFIG_STORE_HTTP_TIMEOUT = 5000L; + @Inject private K8sUtil k8sUtil; + protected final WebClient webClient; + @Inject - public SessionResource(ApplicationProperties applicationProperties) { + public SessionResource(Vertx vertx, ApplicationProperties applicationProperties) { super(applicationProperties); + WebClientOptions options = new WebClientOptions().setFollowRedirects(false); + webClient = WebClient.create(vertx, options); } @Operation(summary = "List sessions", description = "List sessions of a user.") @@ -105,7 +125,7 @@ public boolean stop(SessionStopRequest request) { throw new TheiaCloudWebException(TheiaCloudError.MISSING_SESSION_NAME); } - SessionSpec existingSession = k8sUtil.findSession(request.sessionName).orElse(null); + SessionSpec existingSession = k8sUtil.findSession(request.sessionName).map(Session::getSpec).orElse(null); if (existingSession == null) { info(correlationId, "Session " + request.sessionName + " does not exist."); // Return true because the goal of not having a running session of the @@ -149,7 +169,7 @@ public SessionPerformance performance(@PathParam("appId") String appId, final String user = theiaCloudUser.getIdentifier(); // Ensure session belongs to the requesting user. - SessionSpec existingSession = k8sUtil.findSession(request.sessionName).orElse(null); + SessionSpec existingSession = k8sUtil.findSession(request.sessionName).map(Session::getSpec).orElse(null); if (existingSession == null) { info(correlationId, "Session " + request.sessionName + " does not exist."); throw new TheiaCloudWebException(TheiaCloudError.INVALID_SESSION_NAME); @@ -162,7 +182,7 @@ public SessionPerformance performance(@PathParam("appId") String appId, try { performance = k8sUtil.reportPerformance(sessionName); } catch (Exception e) { - trace(correlationId, "", e); + warn(correlationId, "", e); performance = null; } if (performance == null) { @@ -171,6 +191,81 @@ public SessionPerformance performance(@PathParam("appId") String appId, return performance; } + @POST + @Path("/{session}/config") + @Operation(summary = "Set config value", description = "Sets a config value in the config store if it is available. This requires the @eclipse-theiacloud/config-store Theia extension to be present in the application.") + @Parameter(name = "session", description = "The name of the session") + @Consumes(MediaType.APPLICATION_JSON) + @APIResponse(responseCode = "204", description = "Config value set successfully.") + @APIResponse(responseCode = "403", description = "User does not own the session.") + @APIResponse(responseCode = "474", description = "Session not found.") + @APIResponse(responseCode = "500", description = "Internal server error while setting config value or other unexpected errors.") + @APIResponse(responseCode = "580", description = "Config store not available.") + @NoAnonymousAccess + public void setConfigValue(@PathParam("session") String sessionName, SessionSetConfigValueRequest request) { + String correlationId = evaluateRequest(request); + String user = theiaCloudUser.getIdentifier(); + + // It would be preferable to ping the pod first to see if it supports the config store. + // However, we need to know the session to get the pod IP and port. + Session session = k8sUtil.findSession(sessionName).orElseThrow(() -> { + info(correlationId, "Session " + sessionName + " does not exist."); + return new TheiaCloudWebException(TheiaCloudError.INVALID_SESSION_NAME); + }); + if (!isOwner(theiaCloudUser.getIdentifier(), session.getSpec())) { + info(correlationId, "User " + user + " does not own session " + sessionName); + throw new TheiaCloudWebException(Status.FORBIDDEN); + } + + // Use internal service URL for service-to-service communication to bypass OAuth2 proxy + + Optional sessionPodInternalUrl = SessionUtil.getInternalClusterURL(k8sUtil.CLIENT, session); + if (sessionPodInternalUrl.isEmpty()) { + error(correlationId, "Could not determine internal cluster URL for Session " + session); + throw new InternalServerErrorException("Could not get session internal service URL"); + } + + String configStoreUrl = sessionPodInternalUrl.get() + CONFIG_STORE_PATH; + logger.info("Config store URL (internal): " + configStoreUrl); + + try { + // First, ping the config store to see if it is available. + var pingResponse = webClient.getAbs(configStoreUrl).timeout(CONFIG_STORE_HTTP_TIMEOUT).send().await() + .indefinitely(); + + // Handle ping response. Theia returns a 404 if the config store is not installed. + if (pingResponse.statusCode() < 200 || pingResponse.statusCode() >= 300) { + String message = MessageFormat.format( + "Failed to reach config store of session '{0}' with app definition '{1}'. Config store is probably not installed in application.", + sessionName, session.getSpec().getAppDefinition()); + error(correlationId, message); + throw new TheiaCloudWebException(TheiaCloudError.CONFIG_STORE_NOT_AVAILABLE); + } + + trace(correlationId, MessageFormat.format("Successfully pinged config store at: %s", configStoreUrl)); + + // Second, send the request to set the config value. + JsonObject body = new JsonObject().put("key", request.key).put("value", request.value); + var setValueResponse = webClient.postAbs(configStoreUrl).timeout(CONFIG_STORE_HTTP_TIMEOUT) + .sendJsonObject(body).await().indefinitely(); + + if (setValueResponse.statusCode() < 200 || setValueResponse.statusCode() >= 300) { + error(correlationId, + "Failed to set config value with HTTP status code: " + setValueResponse.statusCode()); + throw new InternalServerErrorException("Failed to set config value."); + } + + info(correlationId, "Config value set successfully for key: " + request.key); + + } catch (TheiaCloudWebException | InternalServerErrorException e) { + // Re-throw expected exceptions + throw e; + } catch (Exception e) { + error(correlationId, "Unexpected error while trying to set config value.", e); + throw new InternalServerErrorException("Unexpected error while trying to set config value."); + } + } + protected boolean isOwner(String user, SessionSpec session) { if (session.getUser() == null || session.getUser().isBlank()) { logger.warnv("Session does not have a user. {0}", session); diff --git a/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/session/SessionSetConfigValueRequest.java b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/session/SessionSetConfigValueRequest.java new file mode 100644 index 000000000..87240c6e9 --- /dev/null +++ b/java/service/org.eclipse.theia.cloud.service/src/main/java/org/eclipse/theia/cloud/service/session/SessionSetConfigValueRequest.java @@ -0,0 +1,46 @@ +/******************************************************************************** + * Copyright (C) 2025 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.service.session; + +import org.eclipse.theia.cloud.service.ServiceRequest; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +public class SessionSetConfigValueRequest extends ServiceRequest { + + public static final String KIND = "sessionSetConfigValueRequest"; + + @Schema(description = "The configuration key", required = true) + public String key; + + @Schema(description = "The configuration value", required = true, nullable = true) + public String value; + + public SessionSetConfigValueRequest() { + super(KIND); + } + + public SessionSetConfigValueRequest(String appId, String key, String value) { + super(KIND, appId); + this.key = key; + this.value = value; + } + + @Override + public String toString() { + return "SessionSetConfigValueRequest [key=" + key + ", value=" + value + ", appId=" + appId + ", kind=" + kind + + "]"; + } +} \ No newline at end of file diff --git a/java/service/org.eclipse.theia.cloud.service/src/main/resources/META-INF/resources/index.html b/java/service/org.eclipse.theia.cloud.service/src/main/resources/META-INF/resources/index.html index cda9367e6..7792aa6c2 100644 --- a/java/service/org.eclipse.theia.cloud.service/src/main/resources/META-INF/resources/index.html +++ b/java/service/org.eclipse.theia.cloud.service/src/main/resources/META-INF/resources/index.html @@ -2,7 +2,7 @@ - service - 1.0.0-SNAPSHOT + service - 1.2.0-SNAPSHOT