diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index de550f174bc9f..907287634c2c4 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -9,5 +9,10 @@ } }, // SYS_PTRACE to enable go debugging - "runArgs": ["--cap-add=SYS_PTRACE"] + "runArgs": ["--cap-add=SYS_PTRACE"], + "customizations": { + "vscode": { + "extensions": ["biomejs.biome"] + } + } } diff --git a/.gitattributes b/.gitattributes index ca878291fe0b5..003a35b526213 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,5 @@ # Generated files +agent/agentcontainers/acmock/acmock.go linguist-generated=true coderd/apidoc/docs.go linguist-generated=true docs/reference/api/*.md linguist-generated=true docs/reference/cli/*.md linguist-generated=true diff --git a/.github/.linkspector.yml b/.github/.linkspector.yml index 3e19913c4b953..13a675813f566 100644 --- a/.github/.linkspector.yml +++ b/.github/.linkspector.yml @@ -20,5 +20,6 @@ ignorePatterns: - pattern: "www.emacswiki.org" - pattern: "linux.die.net/man" - pattern: "www.gnu.org" + - pattern: "wiki.ubuntu.com" aliveStatusCodes: - 200 diff --git a/.github/ISSUE_TEMPLATE/1-bug.yaml b/.github/ISSUE_TEMPLATE/1-bug.yaml new file mode 100644 index 0000000000000..d6cb29730e962 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1-bug.yaml @@ -0,0 +1,78 @@ +name: "🐞 Bug" +description: "File a bug report." +title: "" +labels: ["needs-triage"] +body: + - type: checkboxes + id: existing_issues + attributes: + label: "Is there an existing issue for this?" + description: "Please search to see if an issue already exists for the bug you encountered." + options: + - label: "I have searched the existing issues" + required: true + + - type: textarea + id: issue + attributes: + label: "Current Behavior" + description: "A concise description of what you're experiencing." + placeholder: "Tell us what you see!" + validations: + required: false + + - type: textarea + id: logs + attributes: + label: "Relevant Log Output" + description: "Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks." + render: shell + + - type: textarea + id: expected + attributes: + label: "Expected Behavior" + description: "A concise description of what you expected to happen." + validations: + required: false + + - type: textarea + id: steps_to_reproduce + attributes: + label: "Steps to Reproduce" + description: "Provide step-by-step instructions to reproduce the issue." + placeholder: | + 1. First step + 2. Second step + 3. Another step + 4. Issue occurs + validations: + required: true + + - type: textarea + id: environment + attributes: + label: "Environment" + description: | + Provide details about your environment: + - **Host OS**: (e.g., Ubuntu 24.04, Debian 12) + - **Coder Version**: (e.g., v2.18.4) + placeholder: | + Run `coder version` to get Coder version + value: | + - Host OS: + - Coder version: + validations: + required: false + + - type: dropdown + id: additional_info + attributes: + label: "Additional Context" + description: "Select any applicable options:" + multiple: true + options: + - "The issue occurs consistently" + - "The issue is new (previously worked fine)" + - "The issue happens on multiple deployments" + - "I have tested this on the latest version" diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000000..d38f9c823d51d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,10 @@ +contact_links: + - name: Questions, suggestion or feature requests? + url: https://github.com/coder/coder/discussions/new/choose + about: Our preferred starting point if you have any questions or suggestions about configuration, features or unexpected behavior. + - name: Coder Docs + url: https://coder.com/docs + about: Check our docs. + - name: Coder Discord Community + url: https://discord.gg/coder + about: Get in touch with the Coder developers and community for support. diff --git a/.github/actions/setup-tf/action.yaml b/.github/actions/setup-tf/action.yaml index c52f1138e03ca..f130bcdb7d028 100644 --- a/.github/actions/setup-tf/action.yaml +++ b/.github/actions/setup-tf/action.yaml @@ -7,5 +7,5 @@ runs: - name: Install Terraform uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 with: - terraform_version: 1.9.8 + terraform_version: 1.10.5 terraform_wrapper: false diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 68539f0f4088f..f9c5410df0ce2 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -9,21 +9,6 @@ updates: labels: [] commit-message: prefix: "ci" - ignore: - # These actions deliver the latest versions by updating the major - # release tag, so ignore minor and patch versions - - dependency-name: "actions/*" - update-types: - - version-update:semver-minor - - version-update:semver-patch - - dependency-name: "Apple-Actions/import-codesign-certs" - update-types: - - version-update:semver-minor - - version-update:semver-patch - - dependency-name: "marocchino/sticky-pull-request-comment" - update-types: - - version-update:semver-minor - - version-update:semver-patch groups: github-actions: patterns: diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a400913bc292c..fbc4b9f373cfc 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -34,12 +34,12 @@ jobs: tailnet-integration: ${{ steps.filter.outputs.tailnet-integration }} steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 # For pull requests it's not necessary to checkout the code @@ -122,7 +122,7 @@ jobs: # runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }} # steps: # - name: Checkout - # uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + # uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 # with: # fetch-depth: 1 # # See: https://github.com/stefanzweifel/git-auto-commit-action?tab=readme-ov-file#commits-made-by-this-action-do-not-trigger-new-workflow-runs @@ -155,12 +155,12 @@ jobs: runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }} steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 @@ -178,7 +178,7 @@ jobs: echo "LINT_CACHE_DIR=$dir" >> $GITHUB_ENV - name: golangci-lint cache - uses: actions/cache@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4.1.0 + uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1 with: path: | ${{ env.LINT_CACHE_DIR }} @@ -188,7 +188,7 @@ jobs: # Check for any typos - name: Check for typos - uses: crate-ci/typos@685eb3d55be2f85191e8c84acb9f44d7756f84ab # v1.29.4 + uses: crate-ci/typos@212923e4ff05b7fc2294a204405eec047b807138 # v1.29.9 with: config: .github/workflows/typos.toml @@ -201,7 +201,7 @@ jobs: # Needed for helm chart linting - name: Install helm - uses: azure/setup-helm@fe7b79cd5ee1e45176fcad797de68ecaf3ca4814 # v4.2.0 + uses: azure/setup-helm@b9e51907a09c216f16ebe8536097933489208112 # v4.3.0 with: version: v3.9.2 @@ -227,12 +227,12 @@ jobs: if: always() steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 @@ -290,12 +290,12 @@ jobs: timeout-minutes: 7 steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 @@ -331,12 +331,12 @@ jobs: - windows-2022 steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 @@ -391,12 +391,12 @@ jobs: - windows-2022 steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 @@ -447,12 +447,12 @@ jobs: - ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 @@ -504,12 +504,12 @@ jobs: timeout-minutes: 25 steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 @@ -541,12 +541,12 @@ jobs: timeout-minutes: 25 steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 @@ -579,12 +579,12 @@ jobs: timeout-minutes: 25 steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 @@ -627,12 +627,12 @@ jobs: timeout-minutes: 20 steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 @@ -653,12 +653,12 @@ jobs: timeout-minutes: 20 steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 @@ -685,12 +685,12 @@ jobs: name: ${{ matrix.variant.name }} steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 @@ -733,7 +733,7 @@ jobs: - name: Upload Playwright Failed Tests if: always() && github.actor != 'dependabot[bot]' && runner.os == 'Linux' && !github.event.pull_request.head.repo.fork - uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4.4.1 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: failed-test-videos${{ matrix.variant.premium && '-premium' || '' }} path: ./site/test-results/**/*.webm @@ -741,7 +741,7 @@ jobs: - name: Upload pprof dumps if: always() && github.actor != 'dependabot[bot]' && runner.os == 'Linux' && !github.event.pull_request.head.repo.fork - uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4.4.1 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: debug-pprof-dumps${{ matrix.variant.premium && '-premium' || '' }} path: ./site/test-results/**/debug-pprof-*.txt @@ -754,12 +754,12 @@ jobs: if: needs.changes.outputs.ts == 'true' || needs.changes.outputs.ci == 'true' steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: # Required by Chromatic for build-over-build history, otherwise we # only get 1 commit on shallow checkout. @@ -831,12 +831,12 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: # 0 is required here for version.sh to work. fetch-depth: 0 @@ -905,7 +905,7 @@ jobs: if: always() steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit @@ -940,13 +940,9 @@ jobs: if: needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main' runs-on: ${{ github.repository_owner == 'coder' && 'depot-macos-latest' || 'macos-latest' }} steps: - - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 - with: - egress-policy: audit - + # Harden Runner doesn't work on macOS - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 @@ -957,6 +953,11 @@ jobs: echo "$(brew --prefix gnu-getopt)/bin" >> $GITHUB_PATH echo "$(brew --prefix make)/libexec/gnubin" >> $GITHUB_PATH + - name: Switch XCode Version + uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0 + with: + xcode-version: "16.0.0" + - name: Setup Go uses: ./.github/actions/setup-go @@ -999,7 +1000,7 @@ jobs: - name: Upload build artifacts if: ${{ github.repository_owner == 'coder' && github.ref == 'refs/heads/main' }} - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: dylibs path: | @@ -1020,19 +1021,22 @@ jobs: if: github.ref == 'refs/heads/main' && needs.changes.outputs.docs-only == 'false' && !github.event.pull_request.head.repo.fork runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-22.04' }} permissions: - packages: write # Needed to push images to ghcr.io + # Necessary to push docker images to ghcr.io. + packages: write + # Necessary for GCP authentication (https://github.com/google-github-actions/setup-gcloud#usage) + id-token: write env: DOCKER_CLI_EXPERIMENTAL: "enabled" outputs: IMAGE: ghcr.io/coder/coder-preview:${{ steps.build-docker.outputs.tag }} steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 @@ -1049,12 +1053,44 @@ jobs: - name: Setup Go uses: ./.github/actions/setup-go + # Necessary for signing Windows binaries. + - name: Setup Java + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 + with: + distribution: "zulu" + java-version: "11.0" + + - name: Install go-winres + run: go install github.com/tc-hib/go-winres@d743268d7ea168077ddd443c4240562d4f5e8c3e # v0.3.3 + - name: Install nfpm run: go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.35.1 - name: Install zstd run: sudo apt-get install -y zstd + - name: Setup Windows EV Signing Certificate + run: | + set -euo pipefail + touch /tmp/ev_cert.pem + chmod 600 /tmp/ev_cert.pem + echo "$EV_SIGNING_CERT" > /tmp/ev_cert.pem + wget https://github.com/ebourg/jsign/releases/download/6.0/jsign-6.0.jar -O /tmp/jsign-6.0.jar + env: + EV_SIGNING_CERT: ${{ secrets.EV_SIGNING_CERT }} + + # Setup GCloud for signing Windows binaries. + - name: Authenticate to Google Cloud + id: gcloud_auth + uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8 + with: + workload_identity_provider: ${{ secrets.GCP_CODE_SIGNING_WORKLOAD_ID_PROVIDER }} + service_account: ${{ secrets.GCP_CODE_SIGNING_SERVICE_ACCOUNT }} + token_format: "access_token" + + - name: Setup GCloud SDK + uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4 + - name: Download dylibs uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: @@ -1081,6 +1117,18 @@ jobs: build/coder_linux_{amd64,arm64,armv7} \ build/coder_"$version"_windows_amd64.zip \ build/coder_"$version"_linux_amd64.{tar.gz,deb} + env: + # The Windows slim binary must be signed for Coder Desktop to accept + # it. The darwin executables don't need to be signed, but the dylibs + # do (see above). + CODER_SIGN_WINDOWS: "1" + CODER_WINDOWS_RESOURCES: "1" + EV_KEY: ${{ secrets.EV_KEY }} + EV_KEYSTORE: ${{ secrets.EV_KEYSTORE }} + EV_TSA_URL: ${{ secrets.EV_TSA_URL }} + EV_CERTIFICATE_PATH: /tmp/ev_cert.pem + GCLOUD_ACCESS_TOKEN: ${{ steps.gcloud_auth.outputs.access_token }} + JSIGN_PATH: /tmp/jsign-6.0.jar - name: Build Linux Docker images id: build-docker @@ -1139,7 +1187,7 @@ jobs: - name: Upload build artifacts if: github.ref == 'refs/heads/main' - uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4.4.1 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: coder path: | @@ -1163,32 +1211,32 @@ jobs: id-token: write steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 - name: Authenticate to Google Cloud - uses: google-github-actions/auth@6fc4af4b145ae7821d527454aa9bd537d1f2dc5f # v2.1.7 + uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8 with: workload_identity_provider: projects/573722524737/locations/global/workloadIdentityPools/github/providers/github service_account: coder-ci@coder-dogfood.iam.gserviceaccount.com - name: Set up Google Cloud SDK - uses: google-github-actions/setup-gcloud@6189d56e4096ee891640bb02ac264be376592d6a # v2.1.2 + uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4 - name: Set up Flux CLI - uses: fluxcd/flux2/action@5350425cdcd5fa015337e09fa502153c0275bd4b # v2.4.0 + uses: fluxcd/flux2/action@8d5f40dca5aa5d3c0fc3414457dda15a0ac92fa4 # v2.5.1 with: # Keep this and the github action up to date with the version of flux installed in dogfood cluster - version: "2.2.1" + version: "2.5.1" - name: Get Cluster Credentials - uses: google-github-actions/get-gke-credentials@9025e8f90f2d8e0c3dafc3128cc705a26d992a6a # v2.3.0 + uses: google-github-actions/get-gke-credentials@7a108e64ed8546fe38316b4086e91da13f4785e1 # v2.3.1 with: cluster_name: dogfood-v2 location: us-central1-a @@ -1225,12 +1273,12 @@ jobs: if: github.ref == 'refs/heads/main' && !github.event.pull_request.head.repo.fork steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 @@ -1260,12 +1308,12 @@ jobs: if: needs.changes.outputs.db == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main' steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 # We need golang to run the migration main.go diff --git a/.github/workflows/contrib.yaml b/.github/workflows/contrib.yaml index f9ef209777aa8..6a893243810c2 100644 --- a/.github/workflows/contrib.yaml +++ b/.github/workflows/contrib.yaml @@ -2,15 +2,14 @@ name: contrib on: issue_comment: - types: [created] - pull_request: + types: [created, edited] + pull_request_target: types: - opened - closed - synchronize - labeled - unlabeled - - opened - reopened - edited # For jobs that don't run on draft PRs. @@ -23,88 +22,13 @@ permissions: concurrency: pr-${{ github.ref }} jobs: - # Dependabot is annoying, but this makes it a bit less so. - dependabot-automerge: - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'coder/coder' - permissions: - pull-requests: write - contents: write - steps: - - name: Dependabot metadata - id: metadata - uses: dependabot/fetch-metadata@d7267f607e9d3fb96fc2fbe83e0af444713e90b7 # v2.3.0 - with: - github-token: "${{ secrets.GITHUB_TOKEN }}" - - - name: Approve the PR - run: gh pr review --approve "$PR_URL" - env: - PR_URL: ${{github.event.pull_request.html_url}} - GH_TOKEN: ${{secrets.GITHUB_TOKEN}} - - - name: Enable auto-merge for Dependabot PRs - run: gh pr merge --auto --squash "$PR_URL" - env: - PR_URL: ${{github.event.pull_request.html_url}} - GH_TOKEN: ${{secrets.GITHUB_TOKEN}} - - dependabot-automerge-notify: - # Send a slack notification when a dependabot PR is merged. - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'coder/coder' && github.event.pull_request.merged - steps: - - name: Send Slack notification - env: - PR_URL: ${{github.event.pull_request.html_url}} - PR_TITLE: ${{github.event.pull_request.title}} - PR_NUMBER: ${{github.event.pull_request.number}} - run: | - curl -X POST -H 'Content-type: application/json' \ - --data '{ - "username": "dependabot", - "icon_url": "https://avatars.githubusercontent.com/u/27347476", - "blocks": [ - { - "type": "header", - "text": { - "type": "plain_text", - "text": ":pr-merged: Auto merged Dependabot PR #${{ env.PR_NUMBER }}", - "emoji": true - } - }, - { - "type": "section", - "fields": [ - { - "type": "mrkdwn", - "text": "${{ env.PR_TITLE }}" - } - ] - }, - { - "type": "actions", - "elements": [ - { - "type": "button", - "text": { - "type": "plain_text", - "text": "View PR" - }, - "url": "${{ env.PR_URL }}" - } - ] - } - ] - }' ${{ secrets.DEPENDABOT_PRS_SLACK_WEBHOOK }} - cla: runs-on: ubuntu-latest permissions: pull-requests: write steps: - name: cla - if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request' + if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' uses: contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -122,8 +46,10 @@ jobs: release-labels: runs-on: ubuntu-latest + permissions: + pull-requests: write # Skip tagging for draft PRs. - if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.draft }} + if: ${{ github.event_name == 'pull_request_target' && !github.event.pull_request.draft }} steps: - name: release-labels uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 @@ -158,7 +84,7 @@ jobs: repo: context.repo.repo, } - if (action === "opened" || action === "reopened") { + if (action === "opened" || action === "reopened" || action === "ready_for_review") { if (isBreakingTitle && !labels.includes(releaseLabels.breaking)) { console.log('Add "%s" label', releaseLabels.breaking) await github.rest.issues.addLabels({ diff --git a/.github/workflows/dependabot.yaml b/.github/workflows/dependabot.yaml new file mode 100644 index 0000000000000..16401475b48fc --- /dev/null +++ b/.github/workflows/dependabot.yaml @@ -0,0 +1,88 @@ +name: dependabot + +on: + pull_request: + types: + - opened + +permissions: + contents: read + +jobs: + dependabot-automerge: + runs-on: ubuntu-latest + if: > + github.event_name == 'pull_request' && + github.event.action == 'opened' && + github.event.pull_request.user.login == 'dependabot[bot]' && + github.actor_id == 49699333 && + github.repository == 'coder/coder' + permissions: + pull-requests: write + contents: write + steps: + - name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@d7267f607e9d3fb96fc2fbe83e0af444713e90b7 # v2.3.0 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + + - name: Approve the PR + run: | + echo "Approving $PR_URL" + gh pr review --approve "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GH_TOKEN: ${{secrets.GITHUB_TOKEN}} + + - name: Enable auto-merge + run: | + echo "Enabling auto-merge for $PR_URL" + gh pr merge --auto --squash "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GH_TOKEN: ${{secrets.GITHUB_TOKEN}} + + - name: Send Slack notification + env: + PR_URL: ${{github.event.pull_request.html_url}} + PR_TITLE: ${{github.event.pull_request.title}} + PR_NUMBER: ${{github.event.pull_request.number}} + run: | + curl -X POST -H 'Content-type: application/json' \ + --data '{ + "username": "dependabot", + "icon_url": "https://avatars.githubusercontent.com/u/27347476", + "blocks": [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": ":pr-merged: Auto merge enabled for Dependabot PR #${{ env.PR_NUMBER }}", + "emoji": true + } + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "${{ env.PR_TITLE }}" + } + ] + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "View PR" + }, + "url": "${{ env.PR_URL }}" + } + ] + } + ] + }' ${{ secrets.DEPENDABOT_PRS_SLACK_WEBHOOK }} diff --git a/.github/workflows/docker-base.yaml b/.github/workflows/docker-base.yaml index 7a5135a4cb293..6ec4c6f7fc78c 100644 --- a/.github/workflows/docker-base.yaml +++ b/.github/workflows/docker-base.yaml @@ -38,12 +38,12 @@ jobs: if: github.repository_owner == 'coder' steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Docker login uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 diff --git a/.github/workflows/docs-ci.yaml b/.github/workflows/docs-ci.yaml index 601f13e756830..37e8c56268db3 100644 --- a/.github/workflows/docs-ci.yaml +++ b/.github/workflows/docs-ci.yaml @@ -15,17 +15,20 @@ on: - "**.md" - ".github/workflows/docs-ci.yaml" +permissions: + contents: read + jobs: docs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Node uses: ./.github/actions/setup-node - - uses: tj-actions/changed-files@d6e91a2266cdb9d62096cebf1e8546899c6aa18f # v45.0.6 + - uses: tj-actions/changed-files@dcc7a0cba800f454d79fff4b993e8c3555bcc0a8 # v45.0.7 id: changed-files with: files: | diff --git a/.github/workflows/dogfood.yaml b/.github/workflows/dogfood.yaml index d0f912454211f..f2c70a5844df6 100644 --- a/.github/workflows/dogfood.yaml +++ b/.github/workflows/dogfood.yaml @@ -27,19 +27,16 @@ jobs: runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-4' || 'ubuntu-latest' }} steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Nix uses: DeterminateSystems/nix-installer-action@e50d5f73bfe71c2dd0aa4218de8f4afa59f8f81d # v16 - - name: Setup GHA Nix cache - uses: DeterminateSystems/magic-nix-cache-action@6221693898146dc97e38ad0e013488a16477a4c4 # v9 - - name: Get branch name id: branch-name uses: tj-actions/branch-names@6871f53176ad61624f978536bbf089c574dc19a2 # v8.0.1 @@ -56,7 +53,7 @@ jobs: uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0 + uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca # v3.9.0 - name: Login to DockerHub if: github.ref == 'refs/heads/main' @@ -98,18 +95,18 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Terraform uses: ./.github/actions/setup-tf - name: Authenticate to Google Cloud - uses: google-github-actions/auth@6fc4af4b145ae7821d527454aa9bd537d1f2dc5f # v2.1.7 + uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8 with: workload_identity_provider: projects/573722524737/locations/global/workloadIdentityPools/github/providers/github service_account: coder-ci@coder-dogfood.iam.gserviceaccount.com diff --git a/.github/workflows/nightly-gauntlet.yaml b/.github/workflows/nightly-gauntlet.yaml index 2aba755daa3f8..3965aeab34c55 100644 --- a/.github/workflows/nightly-gauntlet.yaml +++ b/.github/workflows/nightly-gauntlet.yaml @@ -26,12 +26,12 @@ jobs: - windows-2022 steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 diff --git a/.github/workflows/pr-auto-assign.yaml b/.github/workflows/pr-auto-assign.yaml index 6157918a33f7d..ef8245bbff0e3 100644 --- a/.github/workflows/pr-auto-assign.yaml +++ b/.github/workflows/pr-auto-assign.yaml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit diff --git a/.github/workflows/pr-cleanup.yaml b/.github/workflows/pr-cleanup.yaml index 845c16eeaecc2..201cc386f0052 100644 --- a/.github/workflows/pr-cleanup.yaml +++ b/.github/workflows/pr-cleanup.yaml @@ -19,7 +19,7 @@ jobs: packages: write steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit diff --git a/.github/workflows/pr-deploy.yaml b/.github/workflows/pr-deploy.yaml index 89d19822227fa..19bad3fc77b84 100644 --- a/.github/workflows/pr-deploy.yaml +++ b/.github/workflows/pr-deploy.yaml @@ -39,12 +39,12 @@ jobs: PR_OPEN: ${{ steps.check_pr.outputs.pr_open }} steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Check if PR is open id: check_pr @@ -74,12 +74,12 @@ jobs: runs-on: "ubuntu-latest" steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 @@ -174,7 +174,7 @@ jobs: pull-requests: write # needed for commenting on PRs steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit @@ -218,12 +218,12 @@ jobs: CODER_IMAGE_TAG: ${{ needs.get_info.outputs.CODER_IMAGE_TAG }} steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 @@ -276,7 +276,7 @@ jobs: PR_HOSTNAME: "pr${{ needs.get_info.outputs.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}" steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit @@ -325,7 +325,7 @@ jobs: kubectl create namespace "pr${{ env.PR_NUMBER }}" - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Check and Create Certificate if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == 'true' diff --git a/.github/workflows/release-validation.yaml b/.github/workflows/release-validation.yaml index d15eb1b7c0769..54111aa876916 100644 --- a/.github/workflows/release-validation.yaml +++ b/.github/workflows/release-validation.yaml @@ -14,7 +14,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index e7dc9c1ce839f..614b3542d5a80 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -36,13 +36,9 @@ jobs: build-dylib: runs-on: ${{ github.repository_owner == 'coder' && 'depot-macos-latest' || 'macos-latest' }} steps: - - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 - with: - egress-policy: audit - + # Harden Runner doesn't work on macOS. - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 @@ -61,6 +57,11 @@ jobs: echo "$(brew --prefix gnu-getopt)/bin" >> $GITHUB_PATH echo "$(brew --prefix make)/libexec/gnubin" >> $GITHUB_PATH + - name: Switch XCode Version + uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0 + with: + xcode-version: "16.0.0" + - name: Setup Go uses: ./.github/actions/setup-go @@ -100,7 +101,7 @@ jobs: AC_CERTIFICATE_PASSWORD_FILE: /tmp/apple_cert_password.txt - name: Upload build artifacts - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: dylibs path: | @@ -129,12 +130,12 @@ jobs: version: ${{ steps.version.outputs.version }} steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 @@ -217,26 +218,17 @@ jobs: # Necessary for signing Windows binaries. - name: Setup Java - uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: distribution: "zulu" java-version: "11.0" + - name: Install go-winres + run: go install github.com/tc-hib/go-winres@d743268d7ea168077ddd443c4240562d4f5e8c3e # v0.3.3 + - name: Install nsis and zstd run: sudo apt-get install -y nsis zstd - - name: Download dylibs - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - with: - name: dylibs - path: ./build - - - name: Insert dylibs - run: | - mv ./build/*amd64.dylib ./site/out/bin/coder-vpn-darwin-amd64.dylib - mv ./build/*arm64.dylib ./site/out/bin/coder-vpn-darwin-arm64.dylib - mv ./build/*arm64.h ./site/out/bin/coder-vpn-darwin-dylib.h - - name: Install nfpm run: | set -euo pipefail @@ -284,14 +276,26 @@ jobs: # Setup GCloud for signing Windows binaries. - name: Authenticate to Google Cloud id: gcloud_auth - uses: google-github-actions/auth@6fc4af4b145ae7821d527454aa9bd537d1f2dc5f # v2.1.7 + uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8 with: workload_identity_provider: ${{ secrets.GCP_CODE_SIGNING_WORKLOAD_ID_PROVIDER }} service_account: ${{ secrets.GCP_CODE_SIGNING_SERVICE_ACCOUNT }} token_format: "access_token" - name: Setup GCloud SDK - uses: google-github-actions/setup-gcloud@6189d56e4096ee891640bb02ac264be376592d6a # v2.1.2 + uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4 + + - name: Download dylibs + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: dylibs + path: ./build + + - name: Insert dylibs + run: | + mv ./build/*amd64.dylib ./site/out/bin/coder-vpn-darwin-amd64.dylib + mv ./build/*arm64.dylib ./site/out/bin/coder-vpn-darwin-arm64.dylib + mv ./build/*arm64.h ./site/out/bin/coder-vpn-darwin-dylib.h - name: Build binaries run: | @@ -309,6 +313,7 @@ jobs: env: CODER_SIGN_WINDOWS: "1" CODER_SIGN_DARWIN: "1" + CODER_WINDOWS_RESOURCES: "1" AC_CERTIFICATE_FILE: /tmp/apple_cert.p12 AC_CERTIFICATE_PASSWORD_FILE: /tmp/apple_cert_password.txt AC_APIKEY_ISSUER_ID: ${{ secrets.AC_APIKEY_ISSUER_ID }} @@ -459,13 +464,13 @@ jobs: CODER_GPG_RELEASE_KEY_BASE64: ${{ secrets.GPG_RELEASE_KEY_BASE64 }} - name: Authenticate to Google Cloud - uses: google-github-actions/auth@6fc4af4b145ae7821d527454aa9bd537d1f2dc5f # v2.1.7 + uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8 with: workload_identity_provider: ${{ secrets.GCP_WORKLOAD_ID_PROVIDER }} service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} - name: Setup GCloud SDK - uses: google-github-actions/setup-gcloud@6189d56e4096ee891640bb02ac264be376592d6a # 2.1.2 + uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # 2.1.4 - name: Publish Helm Chart if: ${{ !inputs.dry_run }} @@ -484,7 +489,7 @@ jobs: - name: Upload artifacts to actions (if dry-run) if: ${{ inputs.dry_run }} - uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4.4.1 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: release-artifacts path: | @@ -516,7 +521,7 @@ jobs: # TODO: skip this if it's not a new release (i.e. a backport). This is # fine right now because it just makes a PR that we can close. - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit @@ -592,7 +597,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit @@ -602,7 +607,7 @@ jobs: GH_TOKEN: ${{ secrets.CDRCI_GITHUB_TOKEN }} - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 @@ -682,12 +687,12 @@ jobs: if: ${{ !inputs.dry_run }} steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index cf089f59257fe..64cba664f435c 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -20,17 +20,17 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: "Checkout code" - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 + uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 with: results_file: results.sarif results_format: sarif @@ -39,7 +39,7 @@ jobs: # Upload the results as artifacts. - name: "Upload artifact" - uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4.4.1 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: SARIF file path: results.sarif @@ -47,6 +47,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4 # v3.28.5 + uses: github/codeql-action/upload-sarif@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10 with: sarif_file: results.sarif diff --git a/.github/workflows/security.yaml b/.github/workflows/security.yaml index ebf574d33ac86..059ef8cebf20d 100644 --- a/.github/workflows/security.yaml +++ b/.github/workflows/security.yaml @@ -27,18 +27,18 @@ jobs: runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }} steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Go uses: ./.github/actions/setup-go - name: Initialize CodeQL - uses: github/codeql-action/init@f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4 # v3.28.5 + uses: github/codeql-action/init@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10 with: languages: go, javascript @@ -48,7 +48,7 @@ jobs: rm Makefile - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4 # v3.28.5 + uses: github/codeql-action/analyze@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10 - name: Send Slack notification on failure if: ${{ failure() }} @@ -67,12 +67,12 @@ jobs: runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }} steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 @@ -144,13 +144,13 @@ jobs: severity: "CRITICAL,HIGH" - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4 # v3.28.5 + uses: github/codeql-action/upload-sarif@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10 with: sarif_file: trivy-results.sarif category: "Trivy" - name: Upload Trivy scan results as an artifact - uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4.4.1 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: trivy path: trivy-results.sarif diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index c96028b8a6ea3..4de6df9434ecc 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -18,12 +18,12 @@ jobs: pull-requests: write steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: stale - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 + uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 with: stale-issue-label: "stale" stale-pr-label: "stale" @@ -96,12 +96,12 @@ jobs: contents: write steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: Checkout repository - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Run delete-old-branches-action uses: beatlabs/delete-old-branches-action@6e94df089372a619c01ae2c2f666bf474f890911 # v0.0.10 with: @@ -118,7 +118,7 @@ jobs: actions: write steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit diff --git a/.github/workflows/weekly-docs.yaml b/.github/workflows/weekly-docs.yaml index 581b0126f1719..c7af081113909 100644 --- a/.github/workflows/weekly-docs.yaml +++ b/.github/workflows/weekly-docs.yaml @@ -21,12 +21,12 @@ jobs: pull-requests: write # required to post PR review comments by the action steps: - name: Harden Runner - uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Check Markdown links uses: umbrelladocs/action-linkspector@de84085e0f51452a470558693d7d308fbb2fa261 # v1.2.5 diff --git a/Makefile b/Makefile index d71b1173f36b7..fbd324974f218 100644 --- a/Makefile +++ b/Makefile @@ -116,7 +116,7 @@ endif clean: rm -rf build/ site/build/ site/out/ - mkdir -p build/ site/out/bin/ + mkdir -p build/ git restore site/out/ .PHONY: clean @@ -563,7 +563,8 @@ GEN_FILES := \ site/e2e/provisionerGenerated.ts \ examples/examples.gen.json \ $(TAILNETTEST_MOCKS) \ - coderd/database/pubsub/psmock/psmock.go + coderd/database/pubsub/psmock/psmock.go \ + agent/agentcontainers/acmock/acmock.go # all gen targets should be added here and to gen/mark-fresh @@ -598,6 +599,7 @@ gen/mark-fresh: examples/examples.gen.json \ $(TAILNETTEST_MOCKS) \ coderd/database/pubsub/psmock/psmock.go \ + agent/agentcontainers/acmock/acmock.go \ " for file in $$files; do @@ -629,6 +631,9 @@ coderd/database/dbmock/dbmock.go: coderd/database/db.go coderd/database/querier. coderd/database/pubsub/psmock/psmock.go: coderd/database/pubsub/pubsub.go go generate ./coderd/database/pubsub/psmock +agent/agentcontainers/acmock/acmock.go: agent/agentcontainers/containers.go + go generate ./agent/agentcontainers/acmock/ + $(TAILNETTEST_MOCKS): tailnet/coordinator.go tailnet/service.go go generate ./tailnet/tailnettest/ @@ -804,7 +809,7 @@ provisioner/terraform/testdata/version: .PHONY: provisioner/terraform/testdata/version test: - $(GIT_FLAGS) gotestsum --format standard-quiet -- -v -short -count=1 ./... + $(GIT_FLAGS) gotestsum --format standard-quiet -- -v -short -count=1 ./... $(if $(RUN),-run $(RUN)) .PHONY: test test-cli: diff --git a/agent/agent.go b/agent/agent.go index 2daba701b4e89..c2d2ef77c33c8 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "hash/fnv" "io" "net/http" "net/netip" @@ -33,12 +34,15 @@ import ( "tailscale.com/util/clientmetric" "cdr.dev/slog" + "github.com/coder/coder/v2/agent/agentcontainers" "github.com/coder/coder/v2/agent/agentexec" "github.com/coder/coder/v2/agent/agentscripts" "github.com/coder/coder/v2/agent/agentssh" "github.com/coder/coder/v2/agent/proto" + "github.com/coder/coder/v2/agent/proto/resourcesmonitor" "github.com/coder/coder/v2/agent/reconnectingpty" "github.com/coder/coder/v2/buildinfo" + "github.com/coder/coder/v2/cli/clistat" "github.com/coder/coder/v2/cli/gitauth" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/codersdk" @@ -46,6 +50,7 @@ import ( "github.com/coder/coder/v2/codersdk/workspacesdk" "github.com/coder/coder/v2/tailnet" tailnetproto "github.com/coder/coder/v2/tailnet/proto" + "github.com/coder/quartz" "github.com/coder/retry" ) @@ -82,11 +87,12 @@ type Options struct { ServiceBannerRefreshInterval time.Duration BlockFileTransfer bool Execer agentexec.Execer + ContainerLister agentcontainers.Lister } type Client interface { - ConnectRPC23(ctx context.Context) ( - proto.DRPCAgentClient23, tailnetproto.DRPCTailnetClient23, error, + ConnectRPC24(ctx context.Context) ( + proto.DRPCAgentClient24, tailnetproto.DRPCTailnetClient24, error, ) RewriteDERPMap(derpMap *tailcfg.DERPMap) } @@ -122,7 +128,7 @@ func New(options Options) Agent { options.ScriptDataDir = options.TempDir } if options.ExchangeToken == nil { - options.ExchangeToken = func(ctx context.Context) (string, error) { + options.ExchangeToken = func(_ context.Context) (string, error) { return "", nil } } @@ -144,6 +150,9 @@ func New(options Options) Agent { if options.Execer == nil { options.Execer = agentexec.DefaultExecer } + if options.ContainerLister == nil { + options.ContainerLister = agentcontainers.NewDocker(options.Execer) + } hardCtx, hardCancel := context.WithCancel(context.Background()) gracefulCtx, gracefulCancel := context.WithCancel(hardCtx) @@ -178,6 +187,7 @@ func New(options Options) Agent { prometheusRegistry: prometheusRegistry, metrics: newAgentMetrics(prometheusRegistry), execer: options.Execer, + lister: options.ContainerLister, } // Initially, we have a closed channel, reflecting the fact that we are not initially connected. // Each time we connect we replace the channel (while holding the closeMutex) with a new one @@ -247,6 +257,7 @@ type agent struct { // labeled in Coder with the agent + workspace. metrics *agentMetrics execer agentexec.Execer + lister agentcontainers.Lister } func (a *agent) TailnetConn() *tailnet.Conn { @@ -330,7 +341,7 @@ func (a *agent) collectMetadata(ctx context.Context, md codersdk.WorkspaceAgentM // if it can guarantee the clocks are synchronized. CollectedAt: now, } - cmdPty, err := a.sshServer.CreateCommand(ctx, md.Script, nil) + cmdPty, err := a.sshServer.CreateCommand(ctx, md.Script, nil, nil) if err != nil { result.Error = fmt.Sprintf("create cmd: %+v", err) return result @@ -362,7 +373,6 @@ func (a *agent) collectMetadata(ctx context.Context, md codersdk.WorkspaceAgentM // Important: if the command times out, we may see a misleading error like // "exit status 1", so it's important to include the context error. err = errors.Join(err, ctx.Err()) - if err != nil { result.Error = fmt.Sprintf("run cmd: %+v", err) } @@ -399,7 +409,7 @@ func (t *trySingleflight) Do(key string, fn func()) { fn() } -func (a *agent) reportMetadata(ctx context.Context, aAPI proto.DRPCAgentClient23) error { +func (a *agent) reportMetadata(ctx context.Context, aAPI proto.DRPCAgentClient24) error { tickerDone := make(chan struct{}) collectDone := make(chan struct{}) ctx, cancel := context.WithCancel(ctx) @@ -615,7 +625,7 @@ func (a *agent) reportMetadata(ctx context.Context, aAPI proto.DRPCAgentClient23 // reportLifecycle reports the current lifecycle state once. All state // changes are reported in order. -func (a *agent) reportLifecycle(ctx context.Context, aAPI proto.DRPCAgentClient23) error { +func (a *agent) reportLifecycle(ctx context.Context, aAPI proto.DRPCAgentClient24) error { for { select { case <-a.lifecycleUpdate: @@ -697,7 +707,7 @@ func (a *agent) setLifecycle(state codersdk.WorkspaceAgentLifecycle) { // fetchServiceBannerLoop fetches the service banner on an interval. It will // not be fetched immediately; the expectation is that it is primed elsewhere // (and must be done before the session actually starts). -func (a *agent) fetchServiceBannerLoop(ctx context.Context, aAPI proto.DRPCAgentClient23) error { +func (a *agent) fetchServiceBannerLoop(ctx context.Context, aAPI proto.DRPCAgentClient24) error { ticker := time.NewTicker(a.announcementBannersRefreshInterval) defer ticker.Stop() for { @@ -733,7 +743,7 @@ func (a *agent) run() (retErr error) { a.sessionToken.Store(&sessionToken) // ConnectRPC returns the dRPC connection we use for the Agent and Tailnet v2+ APIs - aAPI, tAPI, err := a.client.ConnectRPC23(a.hardCtx) + aAPI, tAPI, err := a.client.ConnectRPC24(a.hardCtx) if err != nil { return err } @@ -750,7 +760,7 @@ func (a *agent) run() (retErr error) { connMan := newAPIConnRoutineManager(a.gracefulCtx, a.hardCtx, a.logger, aAPI, tAPI) connMan.startAgentAPI("init notification banners", gracefulShutdownBehaviorStop, - func(ctx context.Context, aAPI proto.DRPCAgentClient23) error { + func(ctx context.Context, aAPI proto.DRPCAgentClient24) error { bannersProto, err := aAPI.GetAnnouncementBanners(ctx, &proto.GetAnnouncementBannersRequest{}) if err != nil { return xerrors.Errorf("fetch service banner: %w", err) @@ -767,7 +777,7 @@ func (a *agent) run() (retErr error) { // sending logs gets gracefulShutdownBehaviorRemain because we want to send logs generated by // shutdown scripts. connMan.startAgentAPI("send logs", gracefulShutdownBehaviorRemain, - func(ctx context.Context, aAPI proto.DRPCAgentClient23) error { + func(ctx context.Context, aAPI proto.DRPCAgentClient24) error { err := a.logSender.SendLoop(ctx, aAPI) if xerrors.Is(err, agentsdk.LogLimitExceededError) { // we don't want this error to tear down the API connection and propagate to the @@ -785,6 +795,25 @@ func (a *agent) run() (retErr error) { // metadata reporting can cease as soon as we start gracefully shutting down connMan.startAgentAPI("report metadata", gracefulShutdownBehaviorStop, a.reportMetadata) + // resources monitor can cease as soon as we start gracefully shutting down. + connMan.startAgentAPI("resources monitor", gracefulShutdownBehaviorStop, func(ctx context.Context, aAPI proto.DRPCAgentClient24) error { + logger := a.logger.Named("resources_monitor") + clk := quartz.NewReal() + config, err := aAPI.GetResourcesMonitoringConfiguration(ctx, &proto.GetResourcesMonitoringConfigurationRequest{}) + if err != nil { + return xerrors.Errorf("failed to get resources monitoring configuration: %w", err) + } + + statfetcher, err := clistat.New() + if err != nil { + return xerrors.Errorf("failed to create resources fetcher: %w", err) + } + resourcesFetcher := resourcesmonitor.NewFetcher(statfetcher) + + resourcesmonitor := resourcesmonitor.NewResourcesMonitor(logger, clk, config, resourcesFetcher, aAPI) + return resourcesmonitor.Start(ctx) + }) + // channels to sync goroutines below // handle manifest // | @@ -807,7 +836,7 @@ func (a *agent) run() (retErr error) { connMan.startAgentAPI("handle manifest", gracefulShutdownBehaviorStop, a.handleManifest(manifestOK)) connMan.startAgentAPI("app health reporter", gracefulShutdownBehaviorStop, - func(ctx context.Context, aAPI proto.DRPCAgentClient23) error { + func(ctx context.Context, aAPI proto.DRPCAgentClient24) error { if err := manifestOK.wait(ctx); err != nil { return xerrors.Errorf("no manifest: %w", err) } @@ -822,7 +851,7 @@ func (a *agent) run() (retErr error) { a.createOrUpdateNetwork(manifestOK, networkOK)) connMan.startTailnetAPI("coordination", gracefulShutdownBehaviorStop, - func(ctx context.Context, tAPI tailnetproto.DRPCTailnetClient23) error { + func(ctx context.Context, tAPI tailnetproto.DRPCTailnetClient24) error { if err := networkOK.wait(ctx); err != nil { return xerrors.Errorf("no network: %w", err) } @@ -831,7 +860,7 @@ func (a *agent) run() (retErr error) { ) connMan.startTailnetAPI("derp map subscriber", gracefulShutdownBehaviorStop, - func(ctx context.Context, tAPI tailnetproto.DRPCTailnetClient23) error { + func(ctx context.Context, tAPI tailnetproto.DRPCTailnetClient24) error { if err := networkOK.wait(ctx); err != nil { return xerrors.Errorf("no network: %w", err) } @@ -840,7 +869,7 @@ func (a *agent) run() (retErr error) { connMan.startAgentAPI("fetch service banner loop", gracefulShutdownBehaviorStop, a.fetchServiceBannerLoop) - connMan.startAgentAPI("stats report loop", gracefulShutdownBehaviorStop, func(ctx context.Context, aAPI proto.DRPCAgentClient23) error { + connMan.startAgentAPI("stats report loop", gracefulShutdownBehaviorStop, func(ctx context.Context, aAPI proto.DRPCAgentClient24) error { if err := networkOK.wait(ctx); err != nil { return xerrors.Errorf("no network: %w", err) } @@ -851,8 +880,8 @@ func (a *agent) run() (retErr error) { } // handleManifest returns a function that fetches and processes the manifest -func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context, aAPI proto.DRPCAgentClient23) error { - return func(ctx context.Context, aAPI proto.DRPCAgentClient23) error { +func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context, aAPI proto.DRPCAgentClient24) error { + return func(ctx context.Context, aAPI proto.DRPCAgentClient24) error { var ( sentResult = false err error @@ -961,12 +990,11 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context, // createOrUpdateNetwork waits for the manifest to be set using manifestOK, then creates or updates // the tailnet using the information in the manifest -func (a *agent) createOrUpdateNetwork(manifestOK, networkOK *checkpoint) func(context.Context, proto.DRPCAgentClient23) error { - return func(ctx context.Context, _ proto.DRPCAgentClient23) (retErr error) { +func (a *agent) createOrUpdateNetwork(manifestOK, networkOK *checkpoint) func(context.Context, proto.DRPCAgentClient24) error { + return func(ctx context.Context, _ proto.DRPCAgentClient24) (retErr error) { if err := manifestOK.wait(ctx); err != nil { return xerrors.Errorf("no manifest: %w", err) } - var err error defer func() { networkOK.complete(retErr) }() @@ -975,9 +1003,20 @@ func (a *agent) createOrUpdateNetwork(manifestOK, networkOK *checkpoint) func(co network := a.network a.closeMutex.Unlock() if network == nil { + keySeed, err := WorkspaceKeySeed(manifest.WorkspaceID, manifest.AgentName) + if err != nil { + return xerrors.Errorf("generate seed from workspace id: %w", err) + } // use the graceful context here, because creating the tailnet is not itself tied to the // agent API. - network, err = a.createTailnet(a.gracefulCtx, manifest.AgentID, manifest.DERPMap, manifest.DERPForceWebSockets, manifest.DisableDirectConnections) + network, err = a.createTailnet( + a.gracefulCtx, + manifest.AgentID, + manifest.DERPMap, + manifest.DERPForceWebSockets, + manifest.DisableDirectConnections, + keySeed, + ) if err != nil { return xerrors.Errorf("create tailnet: %w", err) } @@ -1117,7 +1156,13 @@ func (a *agent) trackGoroutine(fn func()) error { return nil } -func (a *agent) createTailnet(ctx context.Context, agentID uuid.UUID, derpMap *tailcfg.DERPMap, derpForceWebSockets, disableDirectConnections bool) (_ *tailnet.Conn, err error) { +func (a *agent) createTailnet( + ctx context.Context, + agentID uuid.UUID, + derpMap *tailcfg.DERPMap, + derpForceWebSockets, disableDirectConnections bool, + keySeed int64, +) (_ *tailnet.Conn, err error) { // Inject `CODER_AGENT_HEADER` into the DERP header. var header http.Header if client, ok := a.client.(*agentsdk.Client); ok { @@ -1144,19 +1189,26 @@ func (a *agent) createTailnet(ctx context.Context, agentID uuid.UUID, derpMap *t } }() - sshListener, err := network.Listen("tcp", ":"+strconv.Itoa(workspacesdk.AgentSSHPort)) - if err != nil { - return nil, xerrors.Errorf("listen on the ssh port: %w", err) + if err := a.sshServer.UpdateHostSigner(keySeed); err != nil { + return nil, xerrors.Errorf("update host signer: %w", err) } - defer func() { + + for _, port := range []int{workspacesdk.AgentSSHPort, workspacesdk.AgentStandardSSHPort} { + sshListener, err := network.Listen("tcp", ":"+strconv.Itoa(port)) if err != nil { - _ = sshListener.Close() + return nil, xerrors.Errorf("listen on the ssh port (%v): %w", port, err) + } + // nolint:revive // We do want to run the deferred functions when createTailnet returns. + defer func() { + if err != nil { + _ = sshListener.Close() + } + }() + if err = a.trackGoroutine(func() { + _ = a.sshServer.Serve(sshListener) + }); err != nil { + return nil, err } - }() - if err = a.trackGoroutine(func() { - _ = a.sshServer.Serve(sshListener) - }); err != nil { - return nil, err } reconnectingPTYListener, err := network.Listen("tcp", ":"+strconv.Itoa(workspacesdk.AgentReconnectingPTYPort)) @@ -1266,7 +1318,7 @@ func (a *agent) createTailnet(ctx context.Context, agentID uuid.UUID, derpMap *t // runCoordinator runs a coordinator and returns whether a reconnect // should occur. -func (a *agent) runCoordinator(ctx context.Context, tClient tailnetproto.DRPCTailnetClient23, network *tailnet.Conn) error { +func (a *agent) runCoordinator(ctx context.Context, tClient tailnetproto.DRPCTailnetClient24, network *tailnet.Conn) error { defer a.logger.Debug(ctx, "disconnected from coordination RPC") // we run the RPC on the hardCtx so that we have a chance to send the disconnect message if we // gracefully shut down. @@ -1313,7 +1365,7 @@ func (a *agent) runCoordinator(ctx context.Context, tClient tailnetproto.DRPCTai } // runDERPMapSubscriber runs a coordinator and returns if a reconnect should occur. -func (a *agent) runDERPMapSubscriber(ctx context.Context, tClient tailnetproto.DRPCTailnetClient23, network *tailnet.Conn) error { +func (a *agent) runDERPMapSubscriber(ctx context.Context, tClient tailnetproto.DRPCTailnetClient24, network *tailnet.Conn) error { defer a.logger.Debug(ctx, "disconnected from derp map RPC") ctx, cancel := context.WithCancel(ctx) defer cancel() @@ -1683,8 +1735,8 @@ const ( type apiConnRoutineManager struct { logger slog.Logger - aAPI proto.DRPCAgentClient23 - tAPI tailnetproto.DRPCTailnetClient23 + aAPI proto.DRPCAgentClient24 + tAPI tailnetproto.DRPCTailnetClient24 eg *errgroup.Group stopCtx context.Context remainCtx context.Context @@ -1692,7 +1744,7 @@ type apiConnRoutineManager struct { func newAPIConnRoutineManager( gracefulCtx, hardCtx context.Context, logger slog.Logger, - aAPI proto.DRPCAgentClient23, tAPI tailnetproto.DRPCTailnetClient23, + aAPI proto.DRPCAgentClient24, tAPI tailnetproto.DRPCTailnetClient24, ) *apiConnRoutineManager { // routines that remain in operation during graceful shutdown use the remainCtx. They'll still // exit if the errgroup hits an error, which usually means a problem with the conn. @@ -1725,7 +1777,7 @@ func newAPIConnRoutineManager( // but for Tailnet. func (a *apiConnRoutineManager) startAgentAPI( name string, behavior gracefulShutdownBehavior, - f func(context.Context, proto.DRPCAgentClient23) error, + f func(context.Context, proto.DRPCAgentClient24) error, ) { logger := a.logger.With(slog.F("name", name)) var ctx context.Context @@ -1762,7 +1814,7 @@ func (a *apiConnRoutineManager) startAgentAPI( // but for the Agent API. func (a *apiConnRoutineManager) startTailnetAPI( name string, behavior gracefulShutdownBehavior, - f func(context.Context, tailnetproto.DRPCTailnetClient23) error, + f func(context.Context, tailnetproto.DRPCTailnetClient24) error, ) { logger := a.logger.With(slog.F("name", name)) var ctx context.Context @@ -1821,3 +1873,20 @@ func PrometheusMetricsHandler(prometheusRegistry *prometheus.Registry, logger sl } }) } + +// WorkspaceKeySeed converts a WorkspaceID UUID and agent name to an int64 hash. +// This uses the FNV-1a hash algorithm which provides decent distribution and collision +// resistance for string inputs. +func WorkspaceKeySeed(workspaceID uuid.UUID, agentName string) (int64, error) { + h := fnv.New64a() + _, err := h.Write(workspaceID[:]) + if err != nil { + return 42, err + } + _, err = h.Write([]byte(agentName)) + if err != nil { + return 42, err + } + + return int64(h.Sum64()), nil +} diff --git a/agent/agent_test.go b/agent/agent_test.go index 7674c906ff486..6c4b36945ccef 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -61,38 +61,48 @@ func TestMain(m *testing.M) { goleak.VerifyTestMain(m, testutil.GoleakOptions...) } +var sshPorts = []uint16{workspacesdk.AgentSSHPort, workspacesdk.AgentStandardSSHPort} + // NOTE: These tests only work when your default shell is bash for some reason. func TestAgent_Stats_SSH(t *testing.T) { t.Parallel() - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() - //nolint:dogsled - conn, _, stats, _, _ := setupAgent(t, agentsdk.Manifest{}, 0) + for _, port := range sshPorts { + port := port + t.Run(fmt.Sprintf("(:%d)", port), func(t *testing.T) { + t.Parallel() - sshClient, err := conn.SSHClient(ctx) - require.NoError(t, err) - defer sshClient.Close() - session, err := sshClient.NewSession() - require.NoError(t, err) - defer session.Close() - stdin, err := session.StdinPipe() - require.NoError(t, err) - err = session.Shell() - require.NoError(t, err) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() - var s *proto.Stats - require.Eventuallyf(t, func() bool { - var ok bool - s, ok = <-stats - return ok && s.ConnectionCount > 0 && s.RxBytes > 0 && s.TxBytes > 0 && s.SessionCountSsh == 1 - }, testutil.WaitLong, testutil.IntervalFast, - "never saw stats: %+v", s, - ) - _ = stdin.Close() - err = session.Wait() - require.NoError(t, err) + //nolint:dogsled + conn, _, stats, _, _ := setupAgent(t, agentsdk.Manifest{}, 0) + + sshClient, err := conn.SSHClientOnPort(ctx, port) + require.NoError(t, err) + defer sshClient.Close() + session, err := sshClient.NewSession() + require.NoError(t, err) + defer session.Close() + stdin, err := session.StdinPipe() + require.NoError(t, err) + err = session.Shell() + require.NoError(t, err) + + var s *proto.Stats + require.Eventuallyf(t, func() bool { + var ok bool + s, ok = <-stats + return ok && s.ConnectionCount > 0 && s.RxBytes > 0 && s.TxBytes > 0 && s.SessionCountSsh == 1 + }, testutil.WaitLong, testutil.IntervalFast, + "never saw stats: %+v", s, + ) + _ = stdin.Close() + err = session.Wait() + require.NoError(t, err) + }) + } } func TestAgent_Stats_ReconnectingPTY(t *testing.T) { @@ -138,7 +148,7 @@ func TestAgent_Stats_Magic(t *testing.T) { defer sshClient.Close() session, err := sshClient.NewSession() require.NoError(t, err) - session.Setenv(agentssh.MagicSessionTypeEnvironmentVariable, agentssh.MagicSessionTypeVSCode) + session.Setenv(agentssh.MagicSessionTypeEnvironmentVariable, string(agentssh.MagicSessionTypeVSCode)) defer session.Close() command := "sh -c 'echo $" + agentssh.MagicSessionTypeEnvironmentVariable + "'" @@ -165,7 +175,7 @@ func TestAgent_Stats_Magic(t *testing.T) { defer sshClient.Close() session, err := sshClient.NewSession() require.NoError(t, err) - session.Setenv(agentssh.MagicSessionTypeEnvironmentVariable, agentssh.MagicSessionTypeVSCode) + session.Setenv(agentssh.MagicSessionTypeEnvironmentVariable, string(agentssh.MagicSessionTypeVSCode)) defer session.Close() stdin, err := session.StdinPipe() require.NoError(t, err) @@ -266,15 +276,23 @@ func TestAgent_Stats_Magic(t *testing.T) { func TestAgent_SessionExec(t *testing.T) { t.Parallel() - session := setupSSHSession(t, agentsdk.Manifest{}, codersdk.ServiceBannerConfig{}, nil) - command := "echo test" - if runtime.GOOS == "windows" { - command = "cmd.exe /c echo test" + for _, port := range sshPorts { + port := port + t.Run(fmt.Sprintf("(:%d)", port), func(t *testing.T) { + t.Parallel() + + session := setupSSHSessionOnPort(t, agentsdk.Manifest{}, codersdk.ServiceBannerConfig{}, nil, port) + + command := "echo test" + if runtime.GOOS == "windows" { + command = "cmd.exe /c echo test" + } + output, err := session.Output(command) + require.NoError(t, err) + require.Equal(t, "test", strings.TrimSpace(string(output))) + }) } - output, err := session.Output(command) - require.NoError(t, err) - require.Equal(t, "test", strings.TrimSpace(string(output))) } //nolint:tparallel // Sub tests need to run sequentially. @@ -384,25 +402,33 @@ func TestAgent_SessionTTYShell(t *testing.T) { // it seems like it could be either. t.Skip("ConPTY appears to be inconsistent on Windows.") } - session := setupSSHSession(t, agentsdk.Manifest{}, codersdk.ServiceBannerConfig{}, nil) - command := "sh" - if runtime.GOOS == "windows" { - command = "cmd.exe" + + for _, port := range sshPorts { + port := port + t.Run(fmt.Sprintf("(%d)", port), func(t *testing.T) { + t.Parallel() + + session := setupSSHSessionOnPort(t, agentsdk.Manifest{}, codersdk.ServiceBannerConfig{}, nil, port) + command := "sh" + if runtime.GOOS == "windows" { + command = "cmd.exe" + } + err := session.RequestPty("xterm", 128, 128, ssh.TerminalModes{}) + require.NoError(t, err) + ptty := ptytest.New(t) + session.Stdout = ptty.Output() + session.Stderr = ptty.Output() + session.Stdin = ptty.Input() + err = session.Start(command) + require.NoError(t, err) + _ = ptty.Peek(ctx, 1) // wait for the prompt + ptty.WriteLine("echo test") + ptty.ExpectMatch("test") + ptty.WriteLine("exit") + err = session.Wait() + require.NoError(t, err) + }) } - err := session.RequestPty("xterm", 128, 128, ssh.TerminalModes{}) - require.NoError(t, err) - ptty := ptytest.New(t) - session.Stdout = ptty.Output() - session.Stderr = ptty.Output() - session.Stdin = ptty.Input() - err = session.Start(command) - require.NoError(t, err) - _ = ptty.Peek(ctx, 1) // wait for the prompt - ptty.WriteLine("echo test") - ptty.ExpectMatch("test") - ptty.WriteLine("exit") - err = session.Wait() - require.NoError(t, err) } func TestAgent_SessionTTYExitCode(t *testing.T) { @@ -596,37 +622,41 @@ func TestAgent_Session_TTY_MOTD_Update(t *testing.T) { //nolint:dogsled // Allow the blank identifiers. conn, client, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0, setSBInterval) - sshClient, err := conn.SSHClient(ctx) - require.NoError(t, err) - t.Cleanup(func() { - _ = sshClient.Close() - }) - //nolint:paralleltest // These tests need to swap the banner func. - for i, test := range tests { - test := test - t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { - // Set new banner func and wait for the agent to call it to update the - // banner. - ready := make(chan struct{}, 2) - client.SetAnnouncementBannersFunc(func() ([]codersdk.BannerConfig, error) { - select { - case ready <- struct{}{}: - default: - } - return []codersdk.BannerConfig{test.banner}, nil - }) - <-ready - <-ready // Wait for two updates to ensure the value has propagated. - - session, err := sshClient.NewSession() - require.NoError(t, err) - t.Cleanup(func() { - _ = session.Close() - }) + for _, port := range sshPorts { + port := port - testSessionOutput(t, session, test.expected, test.unexpected, nil) + sshClient, err := conn.SSHClientOnPort(ctx, port) + require.NoError(t, err) + t.Cleanup(func() { + _ = sshClient.Close() }) + + for i, test := range tests { + test := test + t.Run(fmt.Sprintf("(:%d)/%d", port, i), func(t *testing.T) { + // Set new banner func and wait for the agent to call it to update the + // banner. + ready := make(chan struct{}, 2) + client.SetAnnouncementBannersFunc(func() ([]codersdk.BannerConfig, error) { + select { + case ready <- struct{}{}: + default: + } + return []codersdk.BannerConfig{test.banner}, nil + }) + <-ready + <-ready // Wait for two updates to ensure the value has propagated. + + session, err := sshClient.NewSession() + require.NoError(t, err) + t.Cleanup(func() { + _ = session.Close() + }) + + testSessionOutput(t, session, test.expected, test.unexpected, nil) + }) + } } } @@ -977,7 +1007,7 @@ func TestAgent_FileTransferBlocked(t *testing.T) { isErr := strings.Contains(errorMessage, agentssh.BlockedFileTransferErrorMessage) || strings.Contains(errorMessage, "EOF") || strings.Contains(errorMessage, "Process exited with status 65") - require.True(t, isErr, fmt.Sprintf("Message: "+errorMessage)) + require.True(t, isErr, "Message: "+errorMessage) } t.Run("SFTP", func(t *testing.T) { @@ -1962,7 +1992,7 @@ func TestAgent_UpdatedDERP(t *testing.T) { // Push a new DERP map to the agent. err := client.PushDERPMapUpdate(newDerpMap) require.NoError(t, err) - t.Logf("pushed DERPMap update to agent") + t.Log("pushed DERPMap update to agent") require.Eventually(t, func() bool { conn := uut.TailnetConn() @@ -1974,7 +2004,7 @@ func TestAgent_UpdatedDERP(t *testing.T) { t.Logf("agent Conn DERPMap with regionIDs %v, PreferredDERP %d", regionIDs, preferredDERP) return len(regionIDs) == 1 && regionIDs[0] == 2 && preferredDERP == 2 }, testutil.WaitLong, testutil.IntervalFast) - t.Logf("agent got the new DERPMap") + t.Log("agent got the new DERPMap") // Connect from a second client and make sure it uses the new DERP map. conn2 := newClientConn(newDerpMap, "client2") @@ -2313,6 +2343,17 @@ func setupSSHSession( banner codersdk.BannerConfig, prepareFS func(fs afero.Fs), opts ...func(*agenttest.Client, *agent.Options), +) *ssh.Session { + return setupSSHSessionOnPort(t, manifest, banner, prepareFS, workspacesdk.AgentSSHPort, opts...) +} + +func setupSSHSessionOnPort( + t *testing.T, + manifest agentsdk.Manifest, + banner codersdk.BannerConfig, + prepareFS func(fs afero.Fs), + port uint16, + opts ...func(*agenttest.Client, *agent.Options), ) *ssh.Session { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() @@ -2326,7 +2367,7 @@ func setupSSHSession( if prepareFS != nil { prepareFS(fs) } - sshClient, err := conn.SSHClient(ctx) + sshClient, err := conn.SSHClientOnPort(ctx, port) require.NoError(t, err) t.Cleanup(func() { _ = sshClient.Close() diff --git a/agent/agentcontainers/acmock/acmock.go b/agent/agentcontainers/acmock/acmock.go new file mode 100644 index 0000000000000..93c84e8c54fd3 --- /dev/null +++ b/agent/agentcontainers/acmock/acmock.go @@ -0,0 +1,57 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: .. (interfaces: Lister) +// +// Generated by this command: +// +// mockgen -destination ./acmock.go -package acmock .. Lister +// + +// Package acmock is a generated GoMock package. +package acmock + +import ( + context "context" + reflect "reflect" + + codersdk "github.com/coder/coder/v2/codersdk" + gomock "go.uber.org/mock/gomock" +) + +// MockLister is a mock of Lister interface. +type MockLister struct { + ctrl *gomock.Controller + recorder *MockListerMockRecorder + isgomock struct{} +} + +// MockListerMockRecorder is the mock recorder for MockLister. +type MockListerMockRecorder struct { + mock *MockLister +} + +// NewMockLister creates a new mock instance. +func NewMockLister(ctrl *gomock.Controller) *MockLister { + mock := &MockLister{ctrl: ctrl} + mock.recorder = &MockListerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockLister) EXPECT() *MockListerMockRecorder { + return m.recorder +} + +// List mocks base method. +func (m *MockLister) List(ctx context.Context) (codersdk.WorkspaceAgentListContainersResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", ctx) + ret0, _ := ret[0].(codersdk.WorkspaceAgentListContainersResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockListerMockRecorder) List(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockLister)(nil).List), ctx) +} diff --git a/agent/agentcontainers/acmock/doc.go b/agent/agentcontainers/acmock/doc.go new file mode 100644 index 0000000000000..47679708b0fc8 --- /dev/null +++ b/agent/agentcontainers/acmock/doc.go @@ -0,0 +1,4 @@ +// Package acmock contains a mock implementation of agentcontainers.Lister for use in tests. +package acmock + +//go:generate mockgen -destination ./acmock.go -package acmock .. Lister diff --git a/agent/agentcontainers/containers.go b/agent/agentcontainers/containers.go new file mode 100644 index 0000000000000..031d3c7208424 --- /dev/null +++ b/agent/agentcontainers/containers.go @@ -0,0 +1,151 @@ +package agentcontainers + +import ( + "context" + "errors" + "net/http" + "slices" + "time" + + "golang.org/x/xerrors" + + "github.com/coder/coder/v2/coderd/httpapi" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/quartz" +) + +const ( + defaultGetContainersCacheDuration = 10 * time.Second + dockerCreatedAtTimeFormat = "2006-01-02 15:04:05 -0700 MST" + getContainersTimeout = 5 * time.Second +) + +type devcontainersHandler struct { + cacheDuration time.Duration + cl Lister + clock quartz.Clock + + // lockCh protects the below fields. We use a channel instead of a mutex so we + // can handle cancellation properly. + lockCh chan struct{} + containers *codersdk.WorkspaceAgentListContainersResponse + mtime time.Time +} + +// Option is a functional option for devcontainersHandler. +type Option func(*devcontainersHandler) + +// WithLister sets the agentcontainers.Lister implementation to use. +// The default implementation uses the Docker CLI to list containers. +func WithLister(cl Lister) Option { + return func(ch *devcontainersHandler) { + ch.cl = cl + } +} + +// New returns a new devcontainersHandler with the given options applied. +func New(options ...Option) http.Handler { + ch := &devcontainersHandler{ + lockCh: make(chan struct{}, 1), + } + for _, opt := range options { + opt(ch) + } + return ch +} + +func (ch *devcontainersHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + select { + case <-r.Context().Done(): + // Client went away. + return + default: + ct, err := ch.getContainers(r.Context()) + if err != nil { + if errors.Is(err, context.Canceled) { + httpapi.Write(r.Context(), rw, http.StatusRequestTimeout, codersdk.Response{ + Message: "Could not get containers.", + Detail: "Took too long to list containers.", + }) + return + } + httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Could not get containers.", + Detail: err.Error(), + }) + return + } + + httpapi.Write(r.Context(), rw, http.StatusOK, ct) + } +} + +func (ch *devcontainersHandler) getContainers(ctx context.Context) (codersdk.WorkspaceAgentListContainersResponse, error) { + select { + case <-ctx.Done(): + return codersdk.WorkspaceAgentListContainersResponse{}, ctx.Err() + default: + ch.lockCh <- struct{}{} + } + defer func() { + <-ch.lockCh + }() + + // make zero-value usable + if ch.cacheDuration == 0 { + ch.cacheDuration = defaultGetContainersCacheDuration + } + if ch.cl == nil { + ch.cl = &DockerCLILister{} + } + if ch.containers == nil { + ch.containers = &codersdk.WorkspaceAgentListContainersResponse{} + } + if ch.clock == nil { + ch.clock = quartz.NewReal() + } + + now := ch.clock.Now() + if now.Sub(ch.mtime) < ch.cacheDuration { + // Return a copy of the cached data to avoid accidental modification by the caller. + cpy := codersdk.WorkspaceAgentListContainersResponse{ + Containers: slices.Clone(ch.containers.Containers), + Warnings: slices.Clone(ch.containers.Warnings), + } + return cpy, nil + } + + timeoutCtx, timeoutCancel := context.WithTimeout(ctx, getContainersTimeout) + defer timeoutCancel() + updated, err := ch.cl.List(timeoutCtx) + if err != nil { + return codersdk.WorkspaceAgentListContainersResponse{}, xerrors.Errorf("get containers: %w", err) + } + ch.containers = &updated + ch.mtime = now + + // Return a copy of the cached data to avoid accidental modification by the + // caller. + cpy := codersdk.WorkspaceAgentListContainersResponse{ + Containers: slices.Clone(ch.containers.Containers), + Warnings: slices.Clone(ch.containers.Warnings), + } + return cpy, nil +} + +// Lister is an interface for listing containers visible to the +// workspace agent. +type Lister interface { + // List returns a list of containers visible to the workspace agent. + // This should include running and stopped containers. + List(ctx context.Context) (codersdk.WorkspaceAgentListContainersResponse, error) +} + +// NoopLister is a Lister interface that never returns any containers. +type NoopLister struct{} + +var _ Lister = NoopLister{} + +func (NoopLister) List(_ context.Context) (codersdk.WorkspaceAgentListContainersResponse, error) { + return codersdk.WorkspaceAgentListContainersResponse{}, nil +} diff --git a/agent/agentcontainers/containers_dockercli.go b/agent/agentcontainers/containers_dockercli.go new file mode 100644 index 0000000000000..64f264c1ba730 --- /dev/null +++ b/agent/agentcontainers/containers_dockercli.go @@ -0,0 +1,448 @@ +package agentcontainers + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "fmt" + "os" + "os/user" + "slices" + "sort" + "strconv" + "strings" + "time" + + "github.com/coder/coder/v2/agent/agentexec" + "github.com/coder/coder/v2/codersdk" + + "golang.org/x/exp/maps" + "golang.org/x/xerrors" +) + +// DockerCLILister is a ContainerLister that lists containers using the docker CLI +type DockerCLILister struct { + execer agentexec.Execer +} + +var _ Lister = &DockerCLILister{} + +func NewDocker(execer agentexec.Execer) Lister { + return &DockerCLILister{ + execer: agentexec.DefaultExecer, + } +} + +// DockerEnvInfoer is an implementation of agentssh.EnvInfoer that returns +// information about a container. +type DockerEnvInfoer struct { + container string + user *user.User + userShell string + env []string +} + +// EnvInfo returns information about the environment of a container. +func EnvInfo(ctx context.Context, execer agentexec.Execer, container, containerUser string) (*DockerEnvInfoer, error) { + var dei DockerEnvInfoer + dei.container = container + + if containerUser == "" { + // Get the "default" user of the container if no user is specified. + // TODO: handle different container runtimes. + cmd, args := wrapDockerExec(container, "", "whoami") + stdout, stderr, err := run(ctx, execer, cmd, args...) + if err != nil { + return nil, xerrors.Errorf("get container user: run whoami: %w: %s", err, stderr) + } + if len(stdout) == 0 { + return nil, xerrors.Errorf("get container user: run whoami: empty output") + } + containerUser = stdout + } + // Now that we know the username, get the required info from the container. + // We can't assume the presence of `getent` so we'll just have to sniff /etc/passwd. + cmd, args := wrapDockerExec(container, containerUser, "cat", "/etc/passwd") + stdout, stderr, err := run(ctx, execer, cmd, args...) + if err != nil { + return nil, xerrors.Errorf("get container user: read /etc/passwd: %w: %q", err, stderr) + } + + scanner := bufio.NewScanner(strings.NewReader(stdout)) + var foundLine string + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if !strings.HasPrefix(line, containerUser+":") { + continue + } + foundLine = line + break + } + if err := scanner.Err(); err != nil { + return nil, xerrors.Errorf("get container user: scan /etc/passwd: %w", err) + } + if foundLine == "" { + return nil, xerrors.Errorf("get container user: no matching entry for %q found in /etc/passwd", containerUser) + } + + // Parse the output of /etc/passwd. It looks like this: + // postgres:x:999:999::/var/lib/postgresql:/bin/bash + passwdFields := strings.Split(foundLine, ":") + if len(passwdFields) != 7 { + return nil, xerrors.Errorf("get container user: invalid line in /etc/passwd: %q", foundLine) + } + + // The fifth entry in /etc/passwd contains GECOS information, which is a + // comma-separated list of fields. The first field is the user's full name. + gecos := strings.Split(passwdFields[4], ",") + fullName := "" + if len(gecos) > 1 { + fullName = gecos[0] + } + + dei.user = &user.User{ + Gid: passwdFields[3], + HomeDir: passwdFields[5], + Name: fullName, + Uid: passwdFields[2], + Username: containerUser, + } + dei.userShell = passwdFields[6] + + // We need to inspect the container labels for remoteEnv and append these to + // the resulting docker exec command. + // ref: https://code.visualstudio.com/docs/devcontainers/attach-container + env, err := devcontainerEnv(ctx, execer, container) + if err != nil { // best effort. + return nil, xerrors.Errorf("read devcontainer remoteEnv: %w", err) + } + dei.env = env + + return &dei, nil +} + +func (dei *DockerEnvInfoer) CurrentUser() (*user.User, error) { + // Clone the user so that the caller can't modify it + u := *dei.user + return &u, nil +} + +func (*DockerEnvInfoer) Environ() []string { + // Return a clone of the environment so that the caller can't modify it + return os.Environ() +} + +func (*DockerEnvInfoer) UserHomeDir() (string, error) { + // We default the working directory of the command to the user's home + // directory. Since this came from inside the container, we cannot guarantee + // that this exists on the host. Return the "real" home directory of the user + // instead. + return os.UserHomeDir() +} + +func (dei *DockerEnvInfoer) UserShell(string) (string, error) { + return dei.userShell, nil +} + +func (dei *DockerEnvInfoer) ModifyCommand(cmd string, args ...string) (string, []string) { + // Wrap the command with `docker exec` and run it as the container user. + // There is some additional munging here regarding the container user and environment. + dockerArgs := []string{ + "exec", + // The assumption is that this command will be a shell command, so allocate a PTY. + "--interactive", + "--tty", + // Run the command as the user in the container. + "--user", + dei.user.Username, + // Set the working directory to the user's home directory as a sane default. + "--workdir", + dei.user.HomeDir, + } + + // Append the environment variables from the container. + for _, e := range dei.env { + dockerArgs = append(dockerArgs, "--env", e) + } + + // Append the container name and the command. + dockerArgs = append(dockerArgs, dei.container, cmd) + return "docker", append(dockerArgs, args...) +} + +// devcontainerEnv is a helper function that inspects the container labels to +// find the required environment variables for running a command in the container. +func devcontainerEnv(ctx context.Context, execer agentexec.Execer, container string) ([]string, error) { + ins, stderr, err := runDockerInspect(ctx, execer, container) + if err != nil { + return nil, xerrors.Errorf("inspect container: %w: %q", err, stderr) + } + + if len(ins) != 1 { + return nil, xerrors.Errorf("inspect container: expected 1 container, got %d", len(ins)) + } + + in := ins[0] + if in.Config.Labels == nil { + return nil, nil + } + + // We want to look for the devcontainer metadata, which is in the + // value of the label `devcontainer.metadata`. + rawMeta, ok := in.Config.Labels["devcontainer.metadata"] + if !ok { + return nil, nil + } + meta := struct { + RemoteEnv map[string]string `json:"remoteEnv"` + }{} + if err := json.Unmarshal([]byte(rawMeta), &meta); err != nil { + return nil, xerrors.Errorf("unmarshal devcontainer.metadata: %w", err) + } + + // The environment variables are stored in the `remoteEnv` key. + env := make([]string, 0, len(meta.RemoteEnv)) + for k, v := range meta.RemoteEnv { + env = append(env, fmt.Sprintf("%s=%s", k, v)) + } + slices.Sort(env) + return env, nil +} + +// wrapDockerExec is a helper function that wraps the given command and arguments +// with a docker exec command that runs as the given user in the given +// container. This is used to fetch information about a container prior to +// running the actual command. +func wrapDockerExec(containerName, userName, cmd string, args ...string) (string, []string) { + dockerArgs := []string{"exec", "--interactive"} + if userName != "" { + dockerArgs = append(dockerArgs, "--user", userName) + } + dockerArgs = append(dockerArgs, containerName, cmd) + return "docker", append(dockerArgs, args...) +} + +// Helper function to run a command and return its stdout and stderr. +// We want to differentiate stdout and stderr instead of using CombinedOutput. +// We also want to differentiate between a command running successfully with +// output to stderr and a non-zero exit code. +func run(ctx context.Context, execer agentexec.Execer, cmd string, args ...string) (stdout, stderr string, err error) { + var stdoutBuf, stderrBuf strings.Builder + execCmd := execer.CommandContext(ctx, cmd, args...) + execCmd.Stdout = &stdoutBuf + execCmd.Stderr = &stderrBuf + err = execCmd.Run() + stdout = strings.TrimSpace(stdoutBuf.String()) + stderr = strings.TrimSpace(stderrBuf.String()) + return stdout, stderr, err +} + +func (dcl *DockerCLILister) List(ctx context.Context) (codersdk.WorkspaceAgentListContainersResponse, error) { + var stdoutBuf, stderrBuf bytes.Buffer + // List all container IDs, one per line, with no truncation + cmd := dcl.execer.CommandContext(ctx, "docker", "ps", "--all", "--quiet", "--no-trunc") + cmd.Stdout = &stdoutBuf + cmd.Stderr = &stderrBuf + if err := cmd.Run(); err != nil { + // TODO(Cian): detect specific errors: + // - docker not installed + // - docker not running + // - no permissions to talk to docker + return codersdk.WorkspaceAgentListContainersResponse{}, xerrors.Errorf("run docker ps: %w: %q", err, strings.TrimSpace(stderrBuf.String())) + } + + ids := make([]string, 0) + scanner := bufio.NewScanner(&stdoutBuf) + for scanner.Scan() { + tmp := strings.TrimSpace(scanner.Text()) + if tmp == "" { + continue + } + ids = append(ids, tmp) + } + if err := scanner.Err(); err != nil { + return codersdk.WorkspaceAgentListContainersResponse{}, xerrors.Errorf("scan docker ps output: %w", err) + } + + dockerPsStderr := strings.TrimSpace(stderrBuf.String()) + if len(ids) == 0 { + return codersdk.WorkspaceAgentListContainersResponse{ + Warnings: []string{dockerPsStderr}, + }, nil + } + + // now we can get the detailed information for each container + // Run `docker inspect` on each container ID. + // NOTE: There is an unavoidable potential race condition where a + // container is removed between `docker ps` and `docker inspect`. + // In this case, stderr will contain an error message but stdout + // will still contain valid JSON. We will just end up missing + // information about the removed container. We could potentially + // log this error, but I'm not sure it's worth it. + ins, dockerInspectStderr, err := runDockerInspect(ctx, dcl.execer, ids...) + if err != nil { + return codersdk.WorkspaceAgentListContainersResponse{}, xerrors.Errorf("run docker inspect: %w", err) + } + + res := codersdk.WorkspaceAgentListContainersResponse{ + Containers: make([]codersdk.WorkspaceAgentDevcontainer, len(ins)), + } + for idx, in := range ins { + out, warns := convertDockerInspect(in) + res.Warnings = append(res.Warnings, warns...) + res.Containers[idx] = out + } + + if dockerPsStderr != "" { + res.Warnings = append(res.Warnings, dockerPsStderr) + } + if dockerInspectStderr != "" { + res.Warnings = append(res.Warnings, dockerInspectStderr) + } + + return res, nil +} + +// runDockerInspect is a helper function that runs `docker inspect` on the given +// container IDs and returns the parsed output. +// The stderr output is also returned for logging purposes. +func runDockerInspect(ctx context.Context, execer agentexec.Execer, ids ...string) ([]dockerInspect, string, error) { + var stdoutBuf, stderrBuf bytes.Buffer + cmd := execer.CommandContext(ctx, "docker", append([]string{"inspect"}, ids...)...) + cmd.Stdout = &stdoutBuf + cmd.Stderr = &stderrBuf + err := cmd.Run() + stderr := strings.TrimSpace(stderrBuf.String()) + if err != nil { + return nil, stderr, err + } + + var ins []dockerInspect + if err := json.NewDecoder(&stdoutBuf).Decode(&ins); err != nil { + return nil, stderr, xerrors.Errorf("decode docker inspect output: %w", err) + } + + return ins, stderr, nil +} + +// To avoid a direct dependency on the Docker API, we use the docker CLI +// to fetch information about containers. +type dockerInspect struct { + ID string `json:"Id"` + Created time.Time `json:"Created"` + Config dockerInspectConfig `json:"Config"` + HostConfig dockerInspectHostConfig `json:"HostConfig"` + Name string `json:"Name"` + Mounts []dockerInspectMount `json:"Mounts"` + State dockerInspectState `json:"State"` +} + +type dockerInspectConfig struct { + Image string `json:"Image"` + Labels map[string]string `json:"Labels"` +} + +type dockerInspectHostConfig struct { + PortBindings map[string]any `json:"PortBindings"` +} + +type dockerInspectMount struct { + Source string `json:"Source"` + Destination string `json:"Destination"` + Type string `json:"Type"` +} + +type dockerInspectState struct { + Running bool `json:"Running"` + ExitCode int `json:"ExitCode"` + Error string `json:"Error"` +} + +func (dis dockerInspectState) String() string { + if dis.Running { + return "running" + } + var sb strings.Builder + _, _ = sb.WriteString("exited") + if dis.ExitCode != 0 { + _, _ = sb.WriteString(fmt.Sprintf(" with code %d", dis.ExitCode)) + } else { + _, _ = sb.WriteString(" successfully") + } + if dis.Error != "" { + _, _ = sb.WriteString(fmt.Sprintf(": %s", dis.Error)) + } + return sb.String() +} + +func convertDockerInspect(in dockerInspect) (codersdk.WorkspaceAgentDevcontainer, []string) { + var warns []string + out := codersdk.WorkspaceAgentDevcontainer{ + CreatedAt: in.Created, + // Remove the leading slash from the container name + FriendlyName: strings.TrimPrefix(in.Name, "/"), + ID: in.ID, + Image: in.Config.Image, + Labels: in.Config.Labels, + Ports: make([]codersdk.WorkspaceAgentListeningPort, 0), + Running: in.State.Running, + Status: in.State.String(), + Volumes: make(map[string]string, len(in.Mounts)), + } + + if in.HostConfig.PortBindings == nil { + in.HostConfig.PortBindings = make(map[string]any) + } + portKeys := maps.Keys(in.HostConfig.PortBindings) + // Sort the ports for deterministic output. + sort.Strings(portKeys) + for _, p := range portKeys { + if port, network, err := convertDockerPort(p); err != nil { + warns = append(warns, err.Error()) + } else { + out.Ports = append(out.Ports, codersdk.WorkspaceAgentListeningPort{ + Network: network, + Port: port, + }) + } + } + + if in.Mounts == nil { + in.Mounts = []dockerInspectMount{} + } + // Sort the mounts for deterministic output. + sort.Slice(in.Mounts, func(i, j int) bool { + return in.Mounts[i].Source < in.Mounts[j].Source + }) + for _, k := range in.Mounts { + out.Volumes[k.Source] = k.Destination + } + + return out, warns +} + +// convertDockerPort converts a Docker port string to a port number and network +// example: "8080/tcp" -> 8080, "tcp" +// +// "8080" -> 8080, "tcp" +func convertDockerPort(in string) (uint16, string, error) { + parts := strings.Split(in, "/") + switch len(parts) { + case 1: + // assume it's a TCP port + p, err := strconv.Atoi(parts[0]) + if err != nil { + return 0, "", xerrors.Errorf("invalid port format: %s", in) + } + return uint16(p), "tcp", nil + case 2: + p, err := strconv.Atoi(parts[0]) + if err != nil { + return 0, "", xerrors.Errorf("invalid port format: %s", in) + } + return uint16(p), parts[1], nil + default: + return 0, "", xerrors.Errorf("invalid port format: %s", in) + } +} diff --git a/agent/agentcontainers/containers_internal_test.go b/agent/agentcontainers/containers_internal_test.go new file mode 100644 index 0000000000000..cdda03f9c8200 --- /dev/null +++ b/agent/agentcontainers/containers_internal_test.go @@ -0,0 +1,564 @@ +package agentcontainers + +import ( + "fmt" + "os" + "slices" + "strconv" + "strings" + "testing" + "time" + + "go.uber.org/mock/gomock" + + "github.com/google/uuid" + "github.com/ory/dockertest/v3" + "github.com/ory/dockertest/v3/docker" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/agent/agentcontainers/acmock" + "github.com/coder/coder/v2/agent/agentexec" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/pty" + "github.com/coder/coder/v2/testutil" + "github.com/coder/quartz" +) + +// TestIntegrationDocker tests agentcontainers functionality using a real +// Docker container. It starts a container with a known +// label, lists the containers, and verifies that the expected container is +// returned. It also executes a sample command inside the container. +// The container is deleted after the test is complete. +// As this test creates containers, it is skipped by default. +// It can be run manually as follows: +// +// CODER_TEST_USE_DOCKER=1 go test ./agent/agentcontainers -run TestDockerCLIContainerLister +func TestIntegrationDocker(t *testing.T) { + t.Parallel() + if ctud, ok := os.LookupEnv("CODER_TEST_USE_DOCKER"); !ok || ctud != "1" { + t.Skip("Set CODER_TEST_USE_DOCKER=1 to run this test") + } + + pool, err := dockertest.NewPool("") + require.NoError(t, err, "Could not connect to docker") + testLabelValue := uuid.New().String() + // Create a temporary directory to validate that we surface mounts correctly. + testTempDir := t.TempDir() + // Pick a random port to expose for testing port bindings. + testRandPort := testutil.RandomPortNoListen(t) + ct, err := pool.RunWithOptions(&dockertest.RunOptions{ + Repository: "busybox", + Tag: "latest", + Cmd: []string{"sleep", "infnity"}, + Labels: map[string]string{ + "com.coder.test": testLabelValue, + "devcontainer.metadata": `{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}`, + }, + Mounts: []string{testTempDir + ":" + testTempDir}, + ExposedPorts: []string{fmt.Sprintf("%d/tcp", testRandPort)}, + PortBindings: map[docker.Port][]docker.PortBinding{ + docker.Port(fmt.Sprintf("%d/tcp", testRandPort)): { + { + HostIP: "0.0.0.0", + HostPort: strconv.FormatInt(int64(testRandPort), 10), + }, + }, + }, + }, func(config *docker.HostConfig) { + config.AutoRemove = true + config.RestartPolicy = docker.RestartPolicy{Name: "no"} + }) + require.NoError(t, err, "Could not start test docker container") + t.Logf("Created container %q", ct.Container.Name) + t.Cleanup(func() { + assert.NoError(t, pool.Purge(ct), "Could not purge resource %q", ct.Container.Name) + t.Logf("Purged container %q", ct.Container.Name) + }) + // Wait for container to start + require.Eventually(t, func() bool { + ct, ok := pool.ContainerByName(ct.Container.Name) + return ok && ct.Container.State.Running + }, testutil.WaitShort, testutil.IntervalSlow, "Container did not start in time") + + dcl := NewDocker(agentexec.DefaultExecer) + ctx := testutil.Context(t, testutil.WaitShort) + actual, err := dcl.List(ctx) + require.NoError(t, err, "Could not list containers") + require.Empty(t, actual.Warnings, "Expected no warnings") + var found bool + for _, foundContainer := range actual.Containers { + if foundContainer.ID == ct.Container.ID { + found = true + assert.Equal(t, ct.Container.Created, foundContainer.CreatedAt) + // ory/dockertest pre-pends a forward slash to the container name. + assert.Equal(t, strings.TrimPrefix(ct.Container.Name, "/"), foundContainer.FriendlyName) + // ory/dockertest returns the sha256 digest of the image. + assert.Equal(t, "busybox:latest", foundContainer.Image) + assert.Equal(t, ct.Container.Config.Labels, foundContainer.Labels) + assert.True(t, foundContainer.Running) + assert.Equal(t, "running", foundContainer.Status) + if assert.Len(t, foundContainer.Ports, 1) { + assert.Equal(t, testRandPort, foundContainer.Ports[0].Port) + assert.Equal(t, "tcp", foundContainer.Ports[0].Network) + } + if assert.Len(t, foundContainer.Volumes, 1) { + assert.Equal(t, testTempDir, foundContainer.Volumes[testTempDir]) + } + // Test that EnvInfo is able to correctly modify a command to be + // executed inside the container. + dei, err := EnvInfo(ctx, agentexec.DefaultExecer, ct.Container.ID, "") + require.NoError(t, err, "Expected no error from DockerEnvInfo()") + ptyWrappedCmd, ptyWrappedArgs := dei.ModifyCommand("/bin/sh", "--norc") + ptyCmd, ptyPs, err := pty.Start(agentexec.DefaultExecer.PTYCommandContext(ctx, ptyWrappedCmd, ptyWrappedArgs...)) + require.NoError(t, err, "failed to start pty command") + t.Cleanup(func() { + _ = ptyPs.Kill() + _ = ptyCmd.Close() + }) + tr := testutil.NewTerminalReader(t, ptyCmd.OutputReader()) + matchPrompt := func(line string) bool { + return strings.Contains(line, "#") + } + matchHostnameCmd := func(line string) bool { + return strings.Contains(strings.TrimSpace(line), "hostname") + } + matchHostnameOuput := func(line string) bool { + return strings.Contains(strings.TrimSpace(line), ct.Container.Config.Hostname) + } + matchEnvCmd := func(line string) bool { + return strings.Contains(strings.TrimSpace(line), "env") + } + matchEnvOutput := func(line string) bool { + return strings.Contains(line, "FOO=bar") || strings.Contains(line, "MULTILINE=foo") + } + require.NoError(t, tr.ReadUntil(ctx, matchPrompt), "failed to match prompt") + t.Logf("Matched prompt") + _, err = ptyCmd.InputWriter().Write([]byte("hostname\r\n")) + require.NoError(t, err, "failed to write to pty") + t.Logf("Wrote hostname command") + require.NoError(t, tr.ReadUntil(ctx, matchHostnameCmd), "failed to match hostname command") + t.Logf("Matched hostname command") + require.NoError(t, tr.ReadUntil(ctx, matchHostnameOuput), "failed to match hostname output") + t.Logf("Matched hostname output") + _, err = ptyCmd.InputWriter().Write([]byte("env\r\n")) + require.NoError(t, err, "failed to write to pty") + t.Logf("Wrote env command") + require.NoError(t, tr.ReadUntil(ctx, matchEnvCmd), "failed to match env command") + t.Logf("Matched env command") + require.NoError(t, tr.ReadUntil(ctx, matchEnvOutput), "failed to match env output") + t.Logf("Matched env output") + break + } + } + assert.True(t, found, "Expected to find container with label 'com.coder.test=%s'", testLabelValue) +} + +func TestWrapDockerExec(t *testing.T) { + t.Parallel() + tests := []struct { + name string + containerUser string + cmdArgs []string + wantCmd []string + }{ + { + name: "cmd with no args", + containerUser: "my-user", + cmdArgs: []string{"my-cmd"}, + wantCmd: []string{"docker", "exec", "--interactive", "--user", "my-user", "my-container", "my-cmd"}, + }, + { + name: "cmd with args", + containerUser: "my-user", + cmdArgs: []string{"my-cmd", "arg1", "--arg2", "arg3", "--arg4"}, + wantCmd: []string{"docker", "exec", "--interactive", "--user", "my-user", "my-container", "my-cmd", "arg1", "--arg2", "arg3", "--arg4"}, + }, + { + name: "no user specified", + containerUser: "", + cmdArgs: []string{"my-cmd"}, + wantCmd: []string{"docker", "exec", "--interactive", "my-container", "my-cmd"}, + }, + } + for _, tt := range tests { + tt := tt // appease the linter even though this isn't needed anymore + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + actualCmd, actualArgs := wrapDockerExec("my-container", tt.containerUser, tt.cmdArgs[0], tt.cmdArgs[1:]...) + assert.Equal(t, tt.wantCmd[0], actualCmd) + assert.Equal(t, tt.wantCmd[1:], actualArgs) + }) + } +} + +// TestContainersHandler tests the containersHandler.getContainers method using +// a mock implementation. It specifically tests caching behavior. +func TestContainersHandler(t *testing.T) { + t.Parallel() + + t.Run("list", func(t *testing.T) { + t.Parallel() + + fakeCt := fakeContainer(t) + fakeCt2 := fakeContainer(t) + makeResponse := func(cts ...codersdk.WorkspaceAgentDevcontainer) codersdk.WorkspaceAgentListContainersResponse { + return codersdk.WorkspaceAgentListContainersResponse{Containers: cts} + } + + // Each test case is called multiple times to ensure idempotency + for _, tc := range []struct { + name string + // data to be stored in the handler + cacheData codersdk.WorkspaceAgentListContainersResponse + // duration of cache + cacheDur time.Duration + // relative age of the cached data + cacheAge time.Duration + // function to set up expectations for the mock + setupMock func(*acmock.MockLister) + // expected result + expected codersdk.WorkspaceAgentListContainersResponse + // expected error + expectedErr string + }{ + { + name: "no cache", + setupMock: func(mcl *acmock.MockLister) { + mcl.EXPECT().List(gomock.Any()).Return(makeResponse(fakeCt), nil).AnyTimes() + }, + expected: makeResponse(fakeCt), + }, + { + name: "no data", + cacheData: makeResponse(), + cacheAge: 2 * time.Second, + cacheDur: time.Second, + setupMock: func(mcl *acmock.MockLister) { + mcl.EXPECT().List(gomock.Any()).Return(makeResponse(fakeCt), nil).AnyTimes() + }, + expected: makeResponse(fakeCt), + }, + { + name: "cached data", + cacheAge: time.Second, + cacheData: makeResponse(fakeCt), + cacheDur: 2 * time.Second, + expected: makeResponse(fakeCt), + }, + { + name: "lister error", + setupMock: func(mcl *acmock.MockLister) { + mcl.EXPECT().List(gomock.Any()).Return(makeResponse(), assert.AnError).AnyTimes() + }, + expectedErr: assert.AnError.Error(), + }, + { + name: "stale cache", + cacheAge: 2 * time.Second, + cacheData: makeResponse(fakeCt), + cacheDur: time.Second, + setupMock: func(mcl *acmock.MockLister) { + mcl.EXPECT().List(gomock.Any()).Return(makeResponse(fakeCt2), nil).AnyTimes() + }, + expected: makeResponse(fakeCt2), + }, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + var ( + ctx = testutil.Context(t, testutil.WaitShort) + clk = quartz.NewMock(t) + ctrl = gomock.NewController(t) + mockLister = acmock.NewMockLister(ctrl) + now = time.Now().UTC() + ch = devcontainersHandler{ + cacheDuration: tc.cacheDur, + cl: mockLister, + clock: clk, + containers: &tc.cacheData, + lockCh: make(chan struct{}, 1), + } + ) + if tc.cacheAge != 0 { + ch.mtime = now.Add(-tc.cacheAge) + } + if tc.setupMock != nil { + tc.setupMock(mockLister) + } + + clk.Set(now).MustWait(ctx) + + // Repeat the test to ensure idempotency + for i := 0; i < 2; i++ { + actual, err := ch.getContainers(ctx) + if tc.expectedErr != "" { + require.Empty(t, actual, "expected no data (attempt %d)", i) + require.ErrorContains(t, err, tc.expectedErr, "expected error (attempt %d)", i) + } else { + require.NoError(t, err, "expected no error (attempt %d)", i) + require.Equal(t, tc.expected, actual, "expected containers to be equal (attempt %d)", i) + } + } + }) + } + }) +} + +func TestConvertDockerPort(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + in string + expectPort uint16 + expectNetwork string + expectError string + }{ + { + name: "empty port", + in: "", + expectError: "invalid port", + }, + { + name: "valid tcp port", + in: "8080/tcp", + expectPort: 8080, + expectNetwork: "tcp", + }, + { + name: "valid udp port", + in: "8080/udp", + expectPort: 8080, + expectNetwork: "udp", + }, + { + name: "valid port no network", + in: "8080", + expectPort: 8080, + expectNetwork: "tcp", + }, + { + name: "invalid port", + in: "invalid/tcp", + expectError: "invalid port", + }, + { + name: "invalid port no network", + in: "invalid", + expectError: "invalid port", + }, + { + name: "multiple network", + in: "8080/tcp/udp", + expectError: "invalid port", + }, + } { + tc := tc // not needed anymore but makes the linter happy + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + actualPort, actualNetwork, actualErr := convertDockerPort(tc.in) + if tc.expectError != "" { + assert.Zero(t, actualPort, "expected no port") + assert.Empty(t, actualNetwork, "expected no network") + assert.ErrorContains(t, actualErr, tc.expectError) + } else { + assert.NoError(t, actualErr, "expected no error") + assert.Equal(t, tc.expectPort, actualPort, "expected port to match") + assert.Equal(t, tc.expectNetwork, actualNetwork, "expected network to match") + } + }) + } +} + +func TestConvertDockerVolume(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + in string + expectHostPath string + expectContainerPath string + expectError string + }{ + { + name: "empty volume", + in: "", + expectError: "invalid volume", + }, + { + name: "length 1 volume", + in: "/path/to/something", + expectHostPath: "/path/to/something", + expectContainerPath: "/path/to/something", + }, + { + name: "length 2 volume", + in: "/path/to/something=/path/to/something/else", + expectHostPath: "/path/to/something", + expectContainerPath: "/path/to/something/else", + }, + { + name: "invalid length volume", + in: "/path/to/something=/path/to/something/else=/path/to/something/else/else", + expectError: "invalid volume", + }, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + }) + } +} + +// TestDockerEnvInfoer tests the ability of EnvInfo to extract information from +// running containers. Containers are deleted after the test is complete. +// As this test creates containers, it is skipped by default. +// It can be run manually as follows: +// +// CODER_TEST_USE_DOCKER=1 go test ./agent/agentcontainers -run TestDockerEnvInfoer +func TestDockerEnvInfoer(t *testing.T) { + t.Parallel() + if ctud, ok := os.LookupEnv("CODER_TEST_USE_DOCKER"); !ok || ctud != "1" { + t.Skip("Set CODER_TEST_USE_DOCKER=1 to run this test") + } + + pool, err := dockertest.NewPool("") + require.NoError(t, err, "Could not connect to docker") + // nolint:paralleltest // variable recapture no longer required + for idx, tt := range []struct { + image string + labels map[string]string + expectedEnv []string + containerUser string + expectedUsername string + expectedUserShell string + }{ + { + image: "busybox:latest", + labels: map[string]string{`devcontainer.metadata`: `{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}`}, + + expectedEnv: []string{"FOO=bar", "MULTILINE=foo\nbar\nbaz"}, + expectedUsername: "root", + expectedUserShell: "/bin/sh", + }, + { + image: "busybox:latest", + labels: map[string]string{`devcontainer.metadata`: `{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}`}, + expectedEnv: []string{"FOO=bar", "MULTILINE=foo\nbar\nbaz"}, + containerUser: "root", + expectedUsername: "root", + expectedUserShell: "/bin/sh", + }, + { + image: "codercom/enterprise-minimal:ubuntu", + labels: map[string]string{`devcontainer.metadata`: `{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}`}, + expectedEnv: []string{"FOO=bar", "MULTILINE=foo\nbar\nbaz"}, + expectedUsername: "coder", + expectedUserShell: "/bin/bash", + }, + { + image: "codercom/enterprise-minimal:ubuntu", + labels: map[string]string{`devcontainer.metadata`: `{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}`}, + expectedEnv: []string{"FOO=bar", "MULTILINE=foo\nbar\nbaz"}, + containerUser: "coder", + expectedUsername: "coder", + expectedUserShell: "/bin/bash", + }, + { + image: "codercom/enterprise-minimal:ubuntu", + labels: map[string]string{`devcontainer.metadata`: `{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}`}, + expectedEnv: []string{"FOO=bar", "MULTILINE=foo\nbar\nbaz"}, + containerUser: "root", + expectedUsername: "root", + expectedUserShell: "/bin/bash", + }, + } { + t.Run(fmt.Sprintf("#%d", idx), func(t *testing.T) { + t.Parallel() + + // Start a container with the given image + // and environment variables + image := strings.Split(tt.image, ":")[0] + tag := strings.Split(tt.image, ":")[1] + ct, err := pool.RunWithOptions(&dockertest.RunOptions{ + Repository: image, + Tag: tag, + Cmd: []string{"sleep", "infinity"}, + Labels: tt.labels, + }, func(config *docker.HostConfig) { + config.AutoRemove = true + config.RestartPolicy = docker.RestartPolicy{Name: "no"} + }) + require.NoError(t, err, "Could not start test docker container") + t.Logf("Created container %q", ct.Container.Name) + t.Cleanup(func() { + assert.NoError(t, pool.Purge(ct), "Could not purge resource %q", ct.Container.Name) + t.Logf("Purged container %q", ct.Container.Name) + }) + + ctx := testutil.Context(t, testutil.WaitShort) + dei, err := EnvInfo(ctx, agentexec.DefaultExecer, ct.Container.ID, tt.containerUser) + require.NoError(t, err, "Expected no error from DockerEnvInfo()") + + u, err := dei.CurrentUser() + require.NoError(t, err, "Expected no error from CurrentUser()") + require.Equal(t, tt.expectedUsername, u.Username, "Expected username to match") + + hd, err := dei.UserHomeDir() + require.NoError(t, err, "Expected no error from UserHomeDir()") + require.NotEmpty(t, hd, "Expected user homedir to be non-empty") + + sh, err := dei.UserShell(tt.containerUser) + require.NoError(t, err, "Expected no error from UserShell()") + require.Equal(t, tt.expectedUserShell, sh, "Expected user shell to match") + + // We don't need to test the actual environment variables here. + environ := dei.Environ() + require.NotEmpty(t, environ, "Expected environ to be non-empty") + + // Test that the environment variables are present in modified command + // output. + envCmd, envArgs := dei.ModifyCommand("env") + for _, env := range tt.expectedEnv { + require.Subset(t, envArgs, []string{"--env", env}) + } + // Run the command in the container and check the output + // HACK: we remove the --tty argument because we're not running in a tty + envArgs = slices.DeleteFunc(envArgs, func(s string) bool { return s == "--tty" }) + stdout, stderr, err := run(ctx, agentexec.DefaultExecer, envCmd, envArgs...) + require.Empty(t, stderr, "Expected no stderr output") + require.NoError(t, err, "Expected no error from running command") + for _, env := range tt.expectedEnv { + require.Contains(t, stdout, env) + } + }) + } +} + +func fakeContainer(t *testing.T, mut ...func(*codersdk.WorkspaceAgentDevcontainer)) codersdk.WorkspaceAgentDevcontainer { + t.Helper() + ct := codersdk.WorkspaceAgentDevcontainer{ + CreatedAt: time.Now().UTC(), + ID: uuid.New().String(), + FriendlyName: testutil.GetRandomName(t), + Image: testutil.GetRandomName(t) + ":" + strings.Split(uuid.New().String(), "-")[0], + Labels: map[string]string{ + testutil.GetRandomName(t): testutil.GetRandomName(t), + }, + Running: true, + Ports: []codersdk.WorkspaceAgentListeningPort{ + { + Network: "tcp", + Port: testutil.RandomPortNoListen(t), + }, + }, + Status: testutil.MustRandString(t, 10), + Volumes: map[string]string{testutil.GetRandomName(t): testutil.GetRandomName(t)}, + } + for _, m := range mut { + m(&ct) + } + return ct +} diff --git a/agent/agentscripts/agentscripts.go b/agent/agentscripts/agentscripts.go index b4def315fab50..bd83d71875c73 100644 --- a/agent/agentscripts/agentscripts.go +++ b/agent/agentscripts/agentscripts.go @@ -283,14 +283,14 @@ func (r *Runner) run(ctx context.Context, script codersdk.WorkspaceAgentScript, cmdCtx, ctxCancel = context.WithTimeout(ctx, script.Timeout) defer ctxCancel() } - cmdPty, err := r.SSHServer.CreateCommand(cmdCtx, script.Script, nil) + cmdPty, err := r.SSHServer.CreateCommand(cmdCtx, script.Script, nil, nil) if err != nil { return xerrors.Errorf("%s script: create command: %w", logPath, err) } cmd = cmdPty.AsExec() cmd.SysProcAttr = cmdSysProcAttr() cmd.WaitDelay = 10 * time.Second - cmd.Cancel = cmdCancel(cmd) + cmd.Cancel = cmdCancel(ctx, logger, cmd) // Expose env vars that can be used in the script for storing data // and binaries. In the future, we may want to expose more env vars diff --git a/agent/agentscripts/agentscripts_other.go b/agent/agentscripts/agentscripts_other.go index a7ab83276e67d..81be68951216f 100644 --- a/agent/agentscripts/agentscripts_other.go +++ b/agent/agentscripts/agentscripts_other.go @@ -3,8 +3,11 @@ package agentscripts import ( + "context" "os/exec" "syscall" + + "cdr.dev/slog" ) func cmdSysProcAttr() *syscall.SysProcAttr { @@ -13,8 +16,9 @@ func cmdSysProcAttr() *syscall.SysProcAttr { } } -func cmdCancel(cmd *exec.Cmd) func() error { +func cmdCancel(ctx context.Context, logger slog.Logger, cmd *exec.Cmd) func() error { return func() error { + logger.Debug(ctx, "cmdCancel: sending SIGHUP to process and children", slog.F("pid", cmd.Process.Pid)) return syscall.Kill(-cmd.Process.Pid, syscall.SIGHUP) } } diff --git a/agent/agentscripts/agentscripts_windows.go b/agent/agentscripts/agentscripts_windows.go index cda1b3fcc39e1..4799d0829c3bb 100644 --- a/agent/agentscripts/agentscripts_windows.go +++ b/agent/agentscripts/agentscripts_windows.go @@ -1,17 +1,21 @@ package agentscripts import ( + "context" "os" "os/exec" "syscall" + + "cdr.dev/slog" ) func cmdSysProcAttr() *syscall.SysProcAttr { return &syscall.SysProcAttr{} } -func cmdCancel(cmd *exec.Cmd) func() error { +func cmdCancel(ctx context.Context, logger slog.Logger, cmd *exec.Cmd) func() error { return func() error { + logger.Debug(ctx, "cmdCancel: sending interrupt to process", slog.F("pid", cmd.Process.Pid)) return cmd.Process.Signal(os.Interrupt) } } diff --git a/agent/agentssh/agentssh.go b/agent/agentssh/agentssh.go index dae1b73b2de6c..a7e028541aa6e 100644 --- a/agent/agentssh/agentssh.go +++ b/agent/agentssh/agentssh.go @@ -3,11 +3,12 @@ package agentssh import ( "bufio" "context" - "crypto/rand" "crypto/rsa" "errors" "fmt" "io" + "math/big" + "math/rand" "net" "os" "os/exec" @@ -26,6 +27,7 @@ import ( "github.com/spf13/afero" "go.uber.org/atomic" gossh "golang.org/x/crypto/ssh" + "golang.org/x/exp/slices" "golang.org/x/xerrors" "cdr.dev/slog" @@ -42,14 +44,6 @@ const ( // unlikely to shadow other exit codes, which are typically 1, 2, 3, etc. MagicSessionErrorCode = 229 - // MagicSessionTypeEnvironmentVariable is used to track the purpose behind an SSH connection. - // This is stripped from any commands being executed, and is counted towards connection stats. - MagicSessionTypeEnvironmentVariable = "CODER_SSH_SESSION_TYPE" - // MagicSessionTypeVSCode is set in the SSH config by the VS Code extension to identify itself. - MagicSessionTypeVSCode = "vscode" - // MagicSessionTypeJetBrains is set in the SSH config by the JetBrains - // extension to identify itself. - MagicSessionTypeJetBrains = "jetbrains" // MagicProcessCmdlineJetBrains is a string in a process's command line that // uniquely identifies it as JetBrains software. MagicProcessCmdlineJetBrains = "idea.vendor.name=JetBrains" @@ -60,6 +54,29 @@ const ( BlockedFileTransferErrorMessage = "File transfer has been disabled." ) +// MagicSessionType is a type that represents the type of session that is being +// established. +type MagicSessionType string + +const ( + // MagicSessionTypeEnvironmentVariable is used to track the purpose behind an SSH connection. + // This is stripped from any commands being executed, and is counted towards connection stats. + MagicSessionTypeEnvironmentVariable = "CODER_SSH_SESSION_TYPE" +) + +// MagicSessionType enums. +const ( + // MagicSessionTypeUnknown means the session type could not be determined. + MagicSessionTypeUnknown MagicSessionType = "unknown" + // MagicSessionTypeSSH is the default session type. + MagicSessionTypeSSH MagicSessionType = "ssh" + // MagicSessionTypeVSCode is set in the SSH config by the VS Code extension to identify itself. + MagicSessionTypeVSCode MagicSessionType = "vscode" + // MagicSessionTypeJetBrains is set in the SSH config by the JetBrains + // extension to identify itself. + MagicSessionTypeJetBrains MagicSessionType = "jetbrains" +) + // BlockedFileTransferCommands contains a list of restricted file transfer commands. var BlockedFileTransferCommands = []string{"nc", "rsync", "scp", "sftp"} @@ -112,17 +129,6 @@ type Server struct { } func NewServer(ctx context.Context, logger slog.Logger, prometheusRegistry *prometheus.Registry, fs afero.Fs, execer agentexec.Execer, config *Config) (*Server, error) { - // Clients' should ignore the host key when connecting. - // The agent needs to authenticate with coderd to SSH, - // so SSH authentication doesn't improve security. - randomHostKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return nil, err - } - randomSigner, err := gossh.NewSignerFromKey(randomHostKey) - if err != nil { - return nil, err - } if config == nil { config = &Config{} } @@ -189,8 +195,10 @@ func NewServer(ctx context.Context, logger slog.Logger, prometheusRegistry *prom slog.F("local_addr", conn.LocalAddr()), slog.Error(err)) }, - Handler: s.sessionHandler, - HostSigners: []ssh.Signer{randomSigner}, + Handler: s.sessionHandler, + // HostSigners are intentionally empty, as the host key will + // be set before we start listening. + HostSigners: []ssh.Signer{}, LocalPortForwardingCallback: func(ctx ssh.Context, destinationHost string, destinationPort uint32) bool { // Allow local port forwarding all! s.logger.Debug(ctx, "local port forward", @@ -255,14 +263,42 @@ func (s *Server) ConnStats() ConnStats { } } +func extractMagicSessionType(env []string) (magicType MagicSessionType, rawType string, filteredEnv []string) { + for _, kv := range env { + if !strings.HasPrefix(kv, MagicSessionTypeEnvironmentVariable) { + continue + } + + rawType = strings.TrimPrefix(kv, MagicSessionTypeEnvironmentVariable+"=") + // Keep going, we'll use the last instance of the env. + } + + // Always force lowercase checking to be case-insensitive. + switch MagicSessionType(strings.ToLower(rawType)) { + case MagicSessionTypeVSCode: + magicType = MagicSessionTypeVSCode + case MagicSessionTypeJetBrains: + magicType = MagicSessionTypeJetBrains + case "", MagicSessionTypeSSH: + magicType = MagicSessionTypeSSH + default: + magicType = MagicSessionTypeUnknown + } + + return magicType, rawType, slices.DeleteFunc(env, func(kv string) bool { + return strings.HasPrefix(kv, MagicSessionTypeEnvironmentVariable+"=") + }) +} + func (s *Server) sessionHandler(session ssh.Session) { ctx := session.Context() + id := uuid.New() logger := s.logger.With( slog.F("remote_addr", session.RemoteAddr()), slog.F("local_addr", session.LocalAddr()), // Assigning a random uuid for each session is useful for tracking // logs for the same ssh session. - slog.F("id", uuid.NewString()), + slog.F("id", id.String()), ) logger.Info(ctx, "handling ssh session") @@ -274,16 +310,21 @@ func (s *Server) sessionHandler(session ssh.Session) { } defer s.trackSession(session, false) - extraEnv := make([]string, 0) - x11, hasX11 := session.X11() - if hasX11 { - display, handled := s.x11Handler(session.Context(), x11) - if !handled { - _ = session.Exit(1) - logger.Error(ctx, "x11 handler failed") - return - } - extraEnv = append(extraEnv, fmt.Sprintf("DISPLAY=localhost:%d.%d", display, x11.ScreenNumber)) + env := session.Environ() + magicType, magicTypeRaw, env := extractMagicSessionType(env) + + switch magicType { + case MagicSessionTypeVSCode: + s.connCountVSCode.Add(1) + defer s.connCountVSCode.Add(-1) + case MagicSessionTypeJetBrains: + // Do nothing here because JetBrains launches hundreds of ssh sessions. + // We instead track JetBrains in the single persistent tcp forwarding channel. + case MagicSessionTypeSSH: + s.connCountSSHSession.Add(1) + defer s.connCountSSHSession.Add(-1) + case MagicSessionTypeUnknown: + logger.Warn(ctx, "invalid magic ssh session type specified", slog.F("raw_type", magicTypeRaw)) } if s.fileTransferBlocked(session) { @@ -309,7 +350,18 @@ func (s *Server) sessionHandler(session ssh.Session) { return } - err := s.sessionStart(logger, session, extraEnv) + x11, hasX11 := session.X11() + if hasX11 { + display, handled := s.x11Handler(session.Context(), x11) + if !handled { + _ = session.Exit(1) + logger.Error(ctx, "x11 handler failed") + return + } + env = append(env, fmt.Sprintf("DISPLAY=localhost:%d.%d", display, x11.ScreenNumber)) + } + + err := s.sessionStart(logger, session, env, magicType) var exitError *exec.ExitError if xerrors.As(err, &exitError) { code := exitError.ExitCode() @@ -379,37 +431,13 @@ func (s *Server) fileTransferBlocked(session ssh.Session) bool { return false } -func (s *Server) sessionStart(logger slog.Logger, session ssh.Session, extraEnv []string) (retErr error) { +func (s *Server) sessionStart(logger slog.Logger, session ssh.Session, env []string, magicType MagicSessionType) (retErr error) { ctx := session.Context() - env := append(session.Environ(), extraEnv...) - var magicType string - for index, kv := range env { - if !strings.HasPrefix(kv, MagicSessionTypeEnvironmentVariable) { - continue - } - magicType = strings.ToLower(strings.TrimPrefix(kv, MagicSessionTypeEnvironmentVariable+"=")) - env = append(env[:index], env[index+1:]...) - } - - // Always force lowercase checking to be case-insensitive. - switch magicType { - case MagicSessionTypeVSCode: - s.connCountVSCode.Add(1) - defer s.connCountVSCode.Add(-1) - case MagicSessionTypeJetBrains: - // Do nothing here because JetBrains launches hundreds of ssh sessions. - // We instead track JetBrains in the single persistent tcp forwarding channel. - case "": - s.connCountSSHSession.Add(1) - defer s.connCountSSHSession.Add(-1) - default: - logger.Warn(ctx, "invalid magic ssh session type specified", slog.F("type", magicType)) - } magicTypeLabel := magicTypeMetricLabel(magicType) sshPty, windowSize, isPty := session.Pty() - cmd, err := s.CreateCommand(ctx, session.RawCommand(), env) + cmd, err := s.CreateCommand(ctx, session.RawCommand(), env, nil) if err != nil { ptyLabel := "no" if isPty { @@ -473,7 +501,7 @@ func (s *Server) startNonPTYSession(logger slog.Logger, session ssh.Session, mag }() go func() { for sig := range sigs { - s.handleSignal(logger, sig, cmd.Process, magicTypeLabel) + handleSignal(logger, sig, cmd.Process, s.metrics, magicTypeLabel) } }() return cmd.Wait() @@ -558,7 +586,7 @@ func (s *Server) startPTYSession(logger slog.Logger, session ptySession, magicTy sigs = nil continue } - s.handleSignal(logger, sig, process, magicTypeLabel) + handleSignal(logger, sig, process, s.metrics, magicTypeLabel) case win, ok := <-windowSize: if !ok { windowSize = nil @@ -612,7 +640,7 @@ func (s *Server) startPTYSession(logger slog.Logger, session ptySession, magicTy return nil } -func (s *Server) handleSignal(logger slog.Logger, ssig ssh.Signal, signaler interface{ Signal(os.Signal) error }, magicTypeLabel string) { +func handleSignal(logger slog.Logger, ssig ssh.Signal, signaler interface{ Signal(os.Signal) error }, metrics *sshServerMetrics, magicTypeLabel string) { ctx := context.Background() sig := osSignalFrom(ssig) logger = logger.With(slog.F("ssh_signal", ssig), slog.F("signal", sig.String())) @@ -620,7 +648,7 @@ func (s *Server) handleSignal(logger slog.Logger, ssig ssh.Signal, signaler inte err := signaler.Signal(sig) if err != nil { logger.Warn(ctx, "signaling the process failed", slog.Error(err)) - s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, "yes", "signal").Add(1) + metrics.sessionErrors.WithLabelValues(magicTypeLabel, "yes", "signal").Add(1) } } @@ -670,17 +698,63 @@ func (s *Server) sftpHandler(logger slog.Logger, session ssh.Session) { _ = session.Exit(1) } +// EnvInfoer encapsulates external information required by CreateCommand. +type EnvInfoer interface { + // CurrentUser returns the current user. + CurrentUser() (*user.User, error) + // Environ returns the environment variables of the current process. + Environ() []string + // UserHomeDir returns the home directory of the current user. + UserHomeDir() (string, error) + // UserShell returns the shell of the given user. + UserShell(username string) (string, error) +} + +type systemEnvInfoer struct{} + +var defaultEnvInfoer EnvInfoer = &systemEnvInfoer{} + +// DefaultEnvInfoer returns a default implementation of +// EnvInfoer. This reads information using the default Go +// implementations. +func DefaultEnvInfoer() EnvInfoer { + return defaultEnvInfoer +} + +func (systemEnvInfoer) CurrentUser() (*user.User, error) { + return user.Current() +} + +func (systemEnvInfoer) Environ() []string { + return os.Environ() +} + +func (systemEnvInfoer) UserHomeDir() (string, error) { + return userHomeDir() +} + +func (systemEnvInfoer) UserShell(username string) (string, error) { + return usershell.Get(username) +} + // CreateCommand processes raw command input with OpenSSH-like behavior. // If the script provided is empty, it will default to the users shell. // This injects environment variables specified by the user at launch too. -func (s *Server) CreateCommand(ctx context.Context, script string, env []string) (*pty.Cmd, error) { - currentUser, err := user.Current() +// The final argument is an interface that allows the caller to provide +// alternative implementations for the dependencies of CreateCommand. +// This is useful when creating a command to be run in a separate environment +// (for example, a Docker container). Pass in nil to use the default. +func (s *Server) CreateCommand(ctx context.Context, script string, env []string, deps EnvInfoer) (*pty.Cmd, error) { + if deps == nil { + deps = DefaultEnvInfoer() + } + currentUser, err := deps.CurrentUser() if err != nil { return nil, xerrors.Errorf("get current user: %w", err) } username := currentUser.Username - shell, err := usershell.Get(username) + shell, err := deps.UserShell(username) if err != nil { return nil, xerrors.Errorf("get user shell: %w", err) } @@ -736,13 +810,13 @@ func (s *Server) CreateCommand(ctx context.Context, script string, env []string) _, err = os.Stat(cmd.Dir) if cmd.Dir == "" || err != nil { // Default to user home if a directory is not set. - homedir, err := userHomeDir() + homedir, err := deps.UserHomeDir() if err != nil { return nil, xerrors.Errorf("get home dir: %w", err) } cmd.Dir = homedir } - cmd.Env = append(os.Environ(), env...) + cmd.Env = append(deps.Environ(), env...) cmd.Env = append(cmd.Env, fmt.Sprintf("USER=%s", username)) // Set SSH connection environment variables (these are also set by OpenSSH @@ -762,7 +836,13 @@ func (s *Server) CreateCommand(ctx context.Context, script string, env []string) return cmd, nil } +// Serve starts the server to handle incoming connections on the provided listener. +// It returns an error if no host keys are set or if there is an issue accepting connections. func (s *Server) Serve(l net.Listener) (retErr error) { + if len(s.srv.HostSigners) == 0 { + return xerrors.New("no host keys set") + } + s.logger.Info(context.Background(), "started serving listener", slog.F("listen_addr", l.Addr())) defer func() { s.logger.Info(context.Background(), "stopped serving listener", @@ -1017,3 +1097,99 @@ func userHomeDir() (string, error) { } return u.HomeDir, nil } + +// UpdateHostSigner updates the host signer with a new key generated from the provided seed. +// If an existing host key exists with the same algorithm, it is overwritten +func (s *Server) UpdateHostSigner(seed int64) error { + key, err := CoderSigner(seed) + if err != nil { + return err + } + + s.mu.Lock() + defer s.mu.Unlock() + + s.srv.AddHostKey(key) + + return nil +} + +// CoderSigner generates a deterministic SSH signer based on the provided seed. +// It uses RSA with a key size of 2048 bits. +func CoderSigner(seed int64) (gossh.Signer, error) { + // Clients should ignore the host key when connecting. + // The agent needs to authenticate with coderd to SSH, + // so SSH authentication doesn't improve security. + + // Since the standard lib purposefully does not generate + // deterministic rsa keys, we need to do it ourselves. + coderHostKey := func() *rsa.PrivateKey { + // Create deterministic random source + // nolint: gosec + deterministicRand := rand.New(rand.NewSource(seed)) + + // Use fixed values for p and q based on the seed + p := big.NewInt(0) + q := big.NewInt(0) + e := big.NewInt(65537) // Standard RSA public exponent + + // Generate deterministic primes using the seeded random + // Each prime should be ~1024 bits to get a 2048-bit key + for { + p.SetBit(p, 1024, 1) // Ensure it's large enough + for i := 0; i < 1024; i++ { + if deterministicRand.Int63()%2 == 1 { + p.SetBit(p, i, 1) + } else { + p.SetBit(p, i, 0) + } + } + if p.ProbablyPrime(20) { + break + } + } + + for { + q.SetBit(q, 1024, 1) // Ensure it's large enough + for i := 0; i < 1024; i++ { + if deterministicRand.Int63()%2 == 1 { + q.SetBit(q, i, 1) + } else { + q.SetBit(q, i, 0) + } + } + if q.ProbablyPrime(20) && p.Cmp(q) != 0 { + break + } + } + + // Calculate n = p * q + n := new(big.Int).Mul(p, q) + + // Calculate phi = (p-1) * (q-1) + p1 := new(big.Int).Sub(p, big.NewInt(1)) + q1 := new(big.Int).Sub(q, big.NewInt(1)) + phi := new(big.Int).Mul(p1, q1) + + // Calculate private exponent d + d := new(big.Int).ModInverse(e, phi) + + // Create the private key + privateKey := &rsa.PrivateKey{ + PublicKey: rsa.PublicKey{ + N: n, + E: int(e.Int64()), + }, + D: d, + Primes: []*big.Int{p, q}, + } + + // Compute precomputed values + privateKey.Precompute() + + return privateKey + }() + + coderSigner, err := gossh.NewSignerFromKey(coderHostKey) + return coderSigner, err +} diff --git a/agent/agentssh/agentssh_internal_test.go b/agent/agentssh/agentssh_internal_test.go index 0ffa45df19b0d..5a319fa0055c9 100644 --- a/agent/agentssh/agentssh_internal_test.go +++ b/agent/agentssh/agentssh_internal_test.go @@ -39,6 +39,8 @@ func Test_sessionStart_orphan(t *testing.T) { s, err := NewServer(ctx, logger, prometheus.NewRegistry(), afero.NewMemMapFs(), agentexec.DefaultExecer, nil) require.NoError(t, err) defer s.Close() + err = s.UpdateHostSigner(42) + assert.NoError(t, err) // Here we're going to call the handler directly with a faked SSH session // that just uses io.Pipes instead of a network socket. There is a large diff --git a/agent/agentssh/agentssh_test.go b/agent/agentssh/agentssh_test.go index 76321e6e19d85..378657ebee5ad 100644 --- a/agent/agentssh/agentssh_test.go +++ b/agent/agentssh/agentssh_test.go @@ -8,6 +8,7 @@ import ( "context" "fmt" "net" + "os/user" "runtime" "strings" "sync" @@ -40,6 +41,8 @@ func TestNewServer_ServeClient(t *testing.T) { s, err := agentssh.NewServer(ctx, logger, prometheus.NewRegistry(), afero.NewMemMapFs(), agentexec.DefaultExecer, nil) require.NoError(t, err) defer s.Close() + err = s.UpdateHostSigner(42) + assert.NoError(t, err) ln, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) @@ -87,7 +90,7 @@ func TestNewServer_ExecuteShebang(t *testing.T) { t.Run("Basic", func(t *testing.T) { t.Parallel() cmd, err := s.CreateCommand(ctx, `#!/bin/bash - echo test`, nil) + echo test`, nil, nil) require.NoError(t, err) output, err := cmd.AsExec().CombinedOutput() require.NoError(t, err) @@ -96,12 +99,45 @@ func TestNewServer_ExecuteShebang(t *testing.T) { t.Run("Args", func(t *testing.T) { t.Parallel() cmd, err := s.CreateCommand(ctx, `#!/usr/bin/env bash - echo test`, nil) + echo test`, nil, nil) require.NoError(t, err) output, err := cmd.AsExec().CombinedOutput() require.NoError(t, err) require.Equal(t, "test\n", string(output)) }) + t.Run("CustomEnvInfoer", func(t *testing.T) { + t.Parallel() + ei := &fakeEnvInfoer{ + CurrentUserFn: func() (u *user.User, err error) { + return nil, assert.AnError + }, + } + _, err := s.CreateCommand(ctx, `whatever`, nil, ei) + require.ErrorIs(t, err, assert.AnError) + }) +} + +type fakeEnvInfoer struct { + CurrentUserFn func() (*user.User, error) + EnvironFn func() []string + UserHomeDirFn func() (string, error) + UserShellFn func(string) (string, error) +} + +func (f *fakeEnvInfoer) CurrentUser() (u *user.User, err error) { + return f.CurrentUserFn() +} + +func (f *fakeEnvInfoer) Environ() []string { + return f.EnvironFn() +} + +func (f *fakeEnvInfoer) UserHomeDir() (string, error) { + return f.UserHomeDirFn() +} + +func (f *fakeEnvInfoer) UserShell(u string) (string, error) { + return f.UserShellFn(u) } func TestNewServer_CloseActiveConnections(t *testing.T) { @@ -112,6 +148,8 @@ func TestNewServer_CloseActiveConnections(t *testing.T) { s, err := agentssh.NewServer(ctx, logger, prometheus.NewRegistry(), afero.NewMemMapFs(), agentexec.DefaultExecer, nil) require.NoError(t, err) defer s.Close() + err = s.UpdateHostSigner(42) + assert.NoError(t, err) ln, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) @@ -163,6 +201,8 @@ func TestNewServer_Signal(t *testing.T) { s, err := agentssh.NewServer(ctx, logger, prometheus.NewRegistry(), afero.NewMemMapFs(), agentexec.DefaultExecer, nil) require.NoError(t, err) defer s.Close() + err = s.UpdateHostSigner(42) + assert.NoError(t, err) ln, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) @@ -228,6 +268,8 @@ func TestNewServer_Signal(t *testing.T) { s, err := agentssh.NewServer(ctx, logger, prometheus.NewRegistry(), afero.NewMemMapFs(), agentexec.DefaultExecer, nil) require.NoError(t, err) defer s.Close() + err = s.UpdateHostSigner(42) + assert.NoError(t, err) ln, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) diff --git a/agent/agentssh/metrics.go b/agent/agentssh/metrics.go index 9c6f2fbb3c5d5..22bbf1fd80743 100644 --- a/agent/agentssh/metrics.go +++ b/agent/agentssh/metrics.go @@ -71,15 +71,15 @@ func newSSHServerMetrics(registerer prometheus.Registerer) *sshServerMetrics { } } -func magicTypeMetricLabel(magicType string) string { +func magicTypeMetricLabel(magicType MagicSessionType) string { switch magicType { case MagicSessionTypeVSCode: case MagicSessionTypeJetBrains: - case "": - magicType = "ssh" + case MagicSessionTypeSSH: + case MagicSessionTypeUnknown: default: - magicType = "unknown" + magicType = MagicSessionTypeUnknown } // Always be case insensitive - return strings.ToLower(magicType) + return strings.ToLower(string(magicType)) } diff --git a/agent/agentssh/x11_test.go b/agent/agentssh/x11_test.go index 057da9a21e642..2ccbbfe69ca5c 100644 --- a/agent/agentssh/x11_test.go +++ b/agent/agentssh/x11_test.go @@ -38,6 +38,8 @@ func TestServer_X11(t *testing.T) { s, err := agentssh.NewServer(ctx, logger, prometheus.NewRegistry(), fs, agentexec.DefaultExecer, &agentssh.Config{}) require.NoError(t, err) defer s.Close() + err = s.UpdateHostSigner(42) + assert.NoError(t, err) ln, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) diff --git a/agent/agenttest/client.go b/agent/agenttest/client.go index 6b2581e7831f2..ed734c6df9f6c 100644 --- a/agent/agenttest/client.go +++ b/agent/agenttest/client.go @@ -15,6 +15,7 @@ import ( "golang.org/x/exp/slices" "golang.org/x/xerrors" "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/emptypb" "storj.io/drpc/drpcmux" "storj.io/drpc/drpcserver" "tailscale.com/tailcfg" @@ -96,8 +97,8 @@ func (c *Client) Close() { c.derpMapOnce.Do(func() { close(c.derpMapUpdates) }) } -func (c *Client) ConnectRPC23(ctx context.Context) ( - agentproto.DRPCAgentClient23, proto.DRPCTailnetClient23, error, +func (c *Client) ConnectRPC24(ctx context.Context) ( + agentproto.DRPCAgentClient24, proto.DRPCTailnetClient24, error, ) { conn, lis := drpcsdk.MemTransportPipe() c.LastWorkspaceAgent = func() { @@ -170,8 +171,11 @@ type FakeAgentAPI struct { lifecycleStates []codersdk.WorkspaceAgentLifecycle metadata map[string]agentsdk.Metadata timings []*agentproto.Timing + connections []*agentproto.Connection - getAnnouncementBannersFunc func() ([]codersdk.BannerConfig, error) + getAnnouncementBannersFunc func() ([]codersdk.BannerConfig, error) + getResourcesMonitoringConfigurationFunc func() (*agentproto.GetResourcesMonitoringConfigurationResponse, error) + pushResourcesMonitoringUsageFunc func(*agentproto.PushResourcesMonitoringUsageRequest) (*agentproto.PushResourcesMonitoringUsageResponse, error) } func (f *FakeAgentAPI) GetManifest(context.Context, *agentproto.GetManifestRequest) (*agentproto.Manifest, error) { @@ -212,6 +216,33 @@ func (f *FakeAgentAPI) GetAnnouncementBanners(context.Context, *agentproto.GetAn return &agentproto.GetAnnouncementBannersResponse{AnnouncementBanners: bannersProto}, nil } +func (f *FakeAgentAPI) GetResourcesMonitoringConfiguration(_ context.Context, _ *agentproto.GetResourcesMonitoringConfigurationRequest) (*agentproto.GetResourcesMonitoringConfigurationResponse, error) { + f.Lock() + defer f.Unlock() + + if f.getResourcesMonitoringConfigurationFunc == nil { + return &agentproto.GetResourcesMonitoringConfigurationResponse{ + Config: &agentproto.GetResourcesMonitoringConfigurationResponse_Config{ + CollectionIntervalSeconds: 10, + NumDatapoints: 20, + }, + }, nil + } + + return f.getResourcesMonitoringConfigurationFunc() +} + +func (f *FakeAgentAPI) PushResourcesMonitoringUsage(_ context.Context, req *agentproto.PushResourcesMonitoringUsageRequest) (*agentproto.PushResourcesMonitoringUsageResponse, error) { + f.Lock() + defer f.Unlock() + + if f.pushResourcesMonitoringUsageFunc == nil { + return &agentproto.PushResourcesMonitoringUsageResponse{}, nil + } + + return f.pushResourcesMonitoringUsageFunc(req) +} + func (f *FakeAgentAPI) UpdateStats(ctx context.Context, req *agentproto.UpdateStatsRequest) (*agentproto.UpdateStatsResponse, error) { f.logger.Debug(ctx, "update stats called", slog.F("req", req)) // empty request is sent to get the interval; but our tests don't want empty stats requests @@ -309,12 +340,20 @@ func (f *FakeAgentAPI) BatchCreateLogs(ctx context.Context, req *agentproto.Batc func (f *FakeAgentAPI) ScriptCompleted(_ context.Context, req *agentproto.WorkspaceAgentScriptCompletedRequest) (*agentproto.WorkspaceAgentScriptCompletedResponse, error) { f.Lock() - f.timings = append(f.timings, req.Timing) + f.timings = append(f.timings, req.GetTiming()) f.Unlock() return &agentproto.WorkspaceAgentScriptCompletedResponse{}, nil } +func (f *FakeAgentAPI) ReportConnection(_ context.Context, req *agentproto.ReportConnectionRequest) (*emptypb.Empty, error) { + f.Lock() + f.connections = append(f.connections, req.GetConnection()) + f.Unlock() + + return &emptypb.Empty{}, nil +} + func NewFakeAgentAPI(t testing.TB, logger slog.Logger, manifest *agentproto.Manifest, statsCh chan *agentproto.Stats) *FakeAgentAPI { return &FakeAgentAPI{ t: t, diff --git a/agent/api.go b/agent/api.go index 2df791d6fbb68..a3241feb3b7ee 100644 --- a/agent/api.go +++ b/agent/api.go @@ -7,6 +7,7 @@ import ( "github.com/go-chi/chi/v5" + "github.com/coder/coder/v2/agent/agentcontainers" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/codersdk" ) @@ -35,7 +36,9 @@ func (a *agent) apiHandler() http.Handler { ignorePorts: cpy, cacheDuration: cacheDuration, } + ch := agentcontainers.New(agentcontainers.WithLister(a.lister)) promHandler := PrometheusMetricsHandler(a.prometheusRegistry, a.logger) + r.Get("/api/v0/containers", ch.ServeHTTP) r.Get("/api/v0/listening-ports", lp.handler) r.Get("/api/v0/netcheck", a.HandleNetcheck) r.Get("/debug/logs", a.HandleHTTPDebugLogs) diff --git a/agent/proto/agent.pb.go b/agent/proto/agent.pb.go index 4b90e0cf60736..e4318e6fdce4b 100644 --- a/agent/proto/agent.pb.go +++ b/agent/proto/agent.pb.go @@ -11,6 +11,7 @@ import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" durationpb "google.golang.org/protobuf/types/known/durationpb" + emptypb "google.golang.org/protobuf/types/known/emptypb" timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" @@ -515,6 +516,110 @@ func (Timing_Status) EnumDescriptor() ([]byte, []int) { return file_agent_proto_agent_proto_rawDescGZIP(), []int{27, 1} } +type Connection_Action int32 + +const ( + Connection_ACTION_UNSPECIFIED Connection_Action = 0 + Connection_CONNECT Connection_Action = 1 + Connection_DISCONNECT Connection_Action = 2 +) + +// Enum value maps for Connection_Action. +var ( + Connection_Action_name = map[int32]string{ + 0: "ACTION_UNSPECIFIED", + 1: "CONNECT", + 2: "DISCONNECT", + } + Connection_Action_value = map[string]int32{ + "ACTION_UNSPECIFIED": 0, + "CONNECT": 1, + "DISCONNECT": 2, + } +) + +func (x Connection_Action) Enum() *Connection_Action { + p := new(Connection_Action) + *p = x + return p +} + +func (x Connection_Action) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Connection_Action) Descriptor() protoreflect.EnumDescriptor { + return file_agent_proto_agent_proto_enumTypes[9].Descriptor() +} + +func (Connection_Action) Type() protoreflect.EnumType { + return &file_agent_proto_agent_proto_enumTypes[9] +} + +func (x Connection_Action) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Connection_Action.Descriptor instead. +func (Connection_Action) EnumDescriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{32, 0} +} + +type Connection_Type int32 + +const ( + Connection_TYPE_UNSPECIFIED Connection_Type = 0 + Connection_SSH Connection_Type = 1 + Connection_VSCODE Connection_Type = 2 + Connection_JETBRAINS Connection_Type = 3 + Connection_RECONNECTING_PTY Connection_Type = 4 +) + +// Enum value maps for Connection_Type. +var ( + Connection_Type_name = map[int32]string{ + 0: "TYPE_UNSPECIFIED", + 1: "SSH", + 2: "VSCODE", + 3: "JETBRAINS", + 4: "RECONNECTING_PTY", + } + Connection_Type_value = map[string]int32{ + "TYPE_UNSPECIFIED": 0, + "SSH": 1, + "VSCODE": 2, + "JETBRAINS": 3, + "RECONNECTING_PTY": 4, + } +) + +func (x Connection_Type) Enum() *Connection_Type { + p := new(Connection_Type) + *p = x + return p +} + +func (x Connection_Type) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Connection_Type) Descriptor() protoreflect.EnumDescriptor { + return file_agent_proto_agent_proto_enumTypes[10].Descriptor() +} + +func (Connection_Type) Type() protoreflect.EnumType { + return &file_agent_proto_agent_proto_enumTypes[10] +} + +func (x Connection_Type) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Connection_Type.Descriptor instead. +func (Connection_Type) EnumDescriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{32, 1} +} + type WorkspaceApp struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2304,18 +2409,14 @@ func (x *Timing) GetStatus() Timing_Status { return Timing_OK } -type WorkspaceApp_Healthcheck struct { +type GetResourcesMonitoringConfigurationRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - - Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` - Interval *durationpb.Duration `protobuf:"bytes,2,opt,name=interval,proto3" json:"interval,omitempty"` - Threshold int32 `protobuf:"varint,3,opt,name=threshold,proto3" json:"threshold,omitempty"` } -func (x *WorkspaceApp_Healthcheck) Reset() { - *x = WorkspaceApp_Healthcheck{} +func (x *GetResourcesMonitoringConfigurationRequest) Reset() { + *x = GetResourcesMonitoringConfigurationRequest{} if protoimpl.UnsafeEnabled { mi := &file_agent_proto_agent_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2323,13 +2424,13 @@ func (x *WorkspaceApp_Healthcheck) Reset() { } } -func (x *WorkspaceApp_Healthcheck) String() string { +func (x *GetResourcesMonitoringConfigurationRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*WorkspaceApp_Healthcheck) ProtoMessage() {} +func (*GetResourcesMonitoringConfigurationRequest) ProtoMessage() {} -func (x *WorkspaceApp_Healthcheck) ProtoReflect() protoreflect.Message { +func (x *GetResourcesMonitoringConfigurationRequest) ProtoReflect() protoreflect.Message { mi := &file_agent_proto_agent_proto_msgTypes[28] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2341,45 +2442,23 @@ func (x *WorkspaceApp_Healthcheck) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use WorkspaceApp_Healthcheck.ProtoReflect.Descriptor instead. -func (*WorkspaceApp_Healthcheck) Descriptor() ([]byte, []int) { - return file_agent_proto_agent_proto_rawDescGZIP(), []int{0, 0} -} - -func (x *WorkspaceApp_Healthcheck) GetUrl() string { - if x != nil { - return x.Url - } - return "" -} - -func (x *WorkspaceApp_Healthcheck) GetInterval() *durationpb.Duration { - if x != nil { - return x.Interval - } - return nil -} - -func (x *WorkspaceApp_Healthcheck) GetThreshold() int32 { - if x != nil { - return x.Threshold - } - return 0 +// Deprecated: Use GetResourcesMonitoringConfigurationRequest.ProtoReflect.Descriptor instead. +func (*GetResourcesMonitoringConfigurationRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{28} } -type WorkspaceAgentMetadata_Result struct { +type GetResourcesMonitoringConfigurationResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - CollectedAt *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=collected_at,json=collectedAt,proto3" json:"collected_at,omitempty"` - Age int64 `protobuf:"varint,2,opt,name=age,proto3" json:"age,omitempty"` - Value string `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"` - Error string `protobuf:"bytes,4,opt,name=error,proto3" json:"error,omitempty"` + Config *GetResourcesMonitoringConfigurationResponse_Config `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + Memory *GetResourcesMonitoringConfigurationResponse_Memory `protobuf:"bytes,2,opt,name=memory,proto3,oneof" json:"memory,omitempty"` + Volumes []*GetResourcesMonitoringConfigurationResponse_Volume `protobuf:"bytes,3,rep,name=volumes,proto3" json:"volumes,omitempty"` } -func (x *WorkspaceAgentMetadata_Result) Reset() { - *x = WorkspaceAgentMetadata_Result{} +func (x *GetResourcesMonitoringConfigurationResponse) Reset() { + *x = GetResourcesMonitoringConfigurationResponse{} if protoimpl.UnsafeEnabled { mi := &file_agent_proto_agent_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2387,13 +2466,13 @@ func (x *WorkspaceAgentMetadata_Result) Reset() { } } -func (x *WorkspaceAgentMetadata_Result) String() string { +func (x *GetResourcesMonitoringConfigurationResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*WorkspaceAgentMetadata_Result) ProtoMessage() {} +func (*GetResourcesMonitoringConfigurationResponse) ProtoMessage() {} -func (x *WorkspaceAgentMetadata_Result) ProtoReflect() protoreflect.Message { +func (x *GetResourcesMonitoringConfigurationResponse) ProtoReflect() protoreflect.Message { mi := &file_agent_proto_agent_proto_msgTypes[29] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2405,53 +2484,42 @@ func (x *WorkspaceAgentMetadata_Result) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use WorkspaceAgentMetadata_Result.ProtoReflect.Descriptor instead. -func (*WorkspaceAgentMetadata_Result) Descriptor() ([]byte, []int) { - return file_agent_proto_agent_proto_rawDescGZIP(), []int{2, 0} +// Deprecated: Use GetResourcesMonitoringConfigurationResponse.ProtoReflect.Descriptor instead. +func (*GetResourcesMonitoringConfigurationResponse) Descriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{29} } -func (x *WorkspaceAgentMetadata_Result) GetCollectedAt() *timestamppb.Timestamp { +func (x *GetResourcesMonitoringConfigurationResponse) GetConfig() *GetResourcesMonitoringConfigurationResponse_Config { if x != nil { - return x.CollectedAt + return x.Config } return nil } -func (x *WorkspaceAgentMetadata_Result) GetAge() int64 { - if x != nil { - return x.Age - } - return 0 -} - -func (x *WorkspaceAgentMetadata_Result) GetValue() string { +func (x *GetResourcesMonitoringConfigurationResponse) GetMemory() *GetResourcesMonitoringConfigurationResponse_Memory { if x != nil { - return x.Value + return x.Memory } - return "" + return nil } -func (x *WorkspaceAgentMetadata_Result) GetError() string { +func (x *GetResourcesMonitoringConfigurationResponse) GetVolumes() []*GetResourcesMonitoringConfigurationResponse_Volume { if x != nil { - return x.Error + return x.Volumes } - return "" + return nil } -type WorkspaceAgentMetadata_Description struct { +type PushResourcesMonitoringUsageRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - DisplayName string `protobuf:"bytes,1,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` - Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` - Script string `protobuf:"bytes,3,opt,name=script,proto3" json:"script,omitempty"` - Interval *durationpb.Duration `protobuf:"bytes,4,opt,name=interval,proto3" json:"interval,omitempty"` - Timeout *durationpb.Duration `protobuf:"bytes,5,opt,name=timeout,proto3" json:"timeout,omitempty"` + Datapoints []*PushResourcesMonitoringUsageRequest_Datapoint `protobuf:"bytes,1,rep,name=datapoints,proto3" json:"datapoints,omitempty"` } -func (x *WorkspaceAgentMetadata_Description) Reset() { - *x = WorkspaceAgentMetadata_Description{} +func (x *PushResourcesMonitoringUsageRequest) Reset() { + *x = PushResourcesMonitoringUsageRequest{} if protoimpl.UnsafeEnabled { mi := &file_agent_proto_agent_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2459,13 +2527,13 @@ func (x *WorkspaceAgentMetadata_Description) Reset() { } } -func (x *WorkspaceAgentMetadata_Description) String() string { +func (x *PushResourcesMonitoringUsageRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*WorkspaceAgentMetadata_Description) ProtoMessage() {} +func (*PushResourcesMonitoringUsageRequest) ProtoMessage() {} -func (x *WorkspaceAgentMetadata_Description) ProtoReflect() protoreflect.Message { +func (x *PushResourcesMonitoringUsageRequest) ProtoReflect() protoreflect.Message { mi := &file_agent_proto_agent_proto_msgTypes[30] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2477,74 +2545,87 @@ func (x *WorkspaceAgentMetadata_Description) ProtoReflect() protoreflect.Message return mi.MessageOf(x) } -// Deprecated: Use WorkspaceAgentMetadata_Description.ProtoReflect.Descriptor instead. -func (*WorkspaceAgentMetadata_Description) Descriptor() ([]byte, []int) { - return file_agent_proto_agent_proto_rawDescGZIP(), []int{2, 1} +// Deprecated: Use PushResourcesMonitoringUsageRequest.ProtoReflect.Descriptor instead. +func (*PushResourcesMonitoringUsageRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{30} } -func (x *WorkspaceAgentMetadata_Description) GetDisplayName() string { +func (x *PushResourcesMonitoringUsageRequest) GetDatapoints() []*PushResourcesMonitoringUsageRequest_Datapoint { if x != nil { - return x.DisplayName + return x.Datapoints } - return "" + return nil } -func (x *WorkspaceAgentMetadata_Description) GetKey() string { - if x != nil { - return x.Key - } - return "" +type PushResourcesMonitoringUsageResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields } -func (x *WorkspaceAgentMetadata_Description) GetScript() string { - if x != nil { - return x.Script +func (x *PushResourcesMonitoringUsageResponse) Reset() { + *x = PushResourcesMonitoringUsageResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_agent_proto_agent_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } - return "" } -func (x *WorkspaceAgentMetadata_Description) GetInterval() *durationpb.Duration { - if x != nil { - return x.Interval - } - return nil +func (x *PushResourcesMonitoringUsageResponse) String() string { + return protoimpl.X.MessageStringOf(x) } -func (x *WorkspaceAgentMetadata_Description) GetTimeout() *durationpb.Duration { - if x != nil { - return x.Timeout +func (*PushResourcesMonitoringUsageResponse) ProtoMessage() {} + +func (x *PushResourcesMonitoringUsageResponse) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_agent_proto_msgTypes[31] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } - return nil + return mi.MessageOf(x) } -type Stats_Metric struct { +// Deprecated: Use PushResourcesMonitoringUsageResponse.ProtoReflect.Descriptor instead. +func (*PushResourcesMonitoringUsageResponse) Descriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{31} +} + +type Connection struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Type Stats_Metric_Type `protobuf:"varint,2,opt,name=type,proto3,enum=coder.agent.v2.Stats_Metric_Type" json:"type,omitempty"` - Value float64 `protobuf:"fixed64,3,opt,name=value,proto3" json:"value,omitempty"` - Labels []*Stats_Metric_Label `protobuf:"bytes,4,rep,name=labels,proto3" json:"labels,omitempty"` + Id []byte `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Action Connection_Action `protobuf:"varint,2,opt,name=action,proto3,enum=coder.agent.v2.Connection_Action" json:"action,omitempty"` + Type Connection_Type `protobuf:"varint,3,opt,name=type,proto3,enum=coder.agent.v2.Connection_Type" json:"type,omitempty"` + Timestamp *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + Ip string `protobuf:"bytes,5,opt,name=ip,proto3" json:"ip,omitempty"` + StatusCode int32 `protobuf:"varint,6,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"` + Reason *string `protobuf:"bytes,7,opt,name=reason,proto3,oneof" json:"reason,omitempty"` } -func (x *Stats_Metric) Reset() { - *x = Stats_Metric{} +func (x *Connection) Reset() { + *x = Connection{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[33] + mi := &file_agent_proto_agent_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *Stats_Metric) String() string { +func (x *Connection) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Stats_Metric) ProtoMessage() {} +func (*Connection) ProtoMessage() {} -func (x *Stats_Metric) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[33] +func (x *Connection) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_agent_proto_msgTypes[32] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2555,65 +2636,85 @@ func (x *Stats_Metric) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Stats_Metric.ProtoReflect.Descriptor instead. -func (*Stats_Metric) Descriptor() ([]byte, []int) { - return file_agent_proto_agent_proto_rawDescGZIP(), []int{7, 1} +// Deprecated: Use Connection.ProtoReflect.Descriptor instead. +func (*Connection) Descriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{32} } -func (x *Stats_Metric) GetName() string { +func (x *Connection) GetId() []byte { if x != nil { - return x.Name + return x.Id } - return "" + return nil } -func (x *Stats_Metric) GetType() Stats_Metric_Type { +func (x *Connection) GetAction() Connection_Action { if x != nil { - return x.Type + return x.Action } - return Stats_Metric_TYPE_UNSPECIFIED + return Connection_ACTION_UNSPECIFIED } -func (x *Stats_Metric) GetValue() float64 { +func (x *Connection) GetType() Connection_Type { if x != nil { - return x.Value + return x.Type } - return 0 + return Connection_TYPE_UNSPECIFIED } -func (x *Stats_Metric) GetLabels() []*Stats_Metric_Label { +func (x *Connection) GetTimestamp() *timestamppb.Timestamp { if x != nil { - return x.Labels + return x.Timestamp } return nil } -type Stats_Metric_Label struct { +func (x *Connection) GetIp() string { + if x != nil { + return x.Ip + } + return "" +} + +func (x *Connection) GetStatusCode() int32 { + if x != nil { + return x.StatusCode + } + return 0 +} + +func (x *Connection) GetReason() string { + if x != nil && x.Reason != nil { + return *x.Reason + } + return "" +} + +type ReportConnectionRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + Connection *Connection `protobuf:"bytes,1,opt,name=connection,proto3" json:"connection,omitempty"` } -func (x *Stats_Metric_Label) Reset() { - *x = Stats_Metric_Label{} +func (x *ReportConnectionRequest) Reset() { + *x = ReportConnectionRequest{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[34] + mi := &file_agent_proto_agent_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *Stats_Metric_Label) String() string { +func (x *ReportConnectionRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Stats_Metric_Label) ProtoMessage() {} +func (*ReportConnectionRequest) ProtoMessage() {} -func (x *Stats_Metric_Label) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[34] +func (x *ReportConnectionRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_agent_proto_msgTypes[33] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2624,50 +2725,108 @@ func (x *Stats_Metric_Label) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Stats_Metric_Label.ProtoReflect.Descriptor instead. -func (*Stats_Metric_Label) Descriptor() ([]byte, []int) { - return file_agent_proto_agent_proto_rawDescGZIP(), []int{7, 1, 0} -} - -func (x *Stats_Metric_Label) GetName() string { - if x != nil { - return x.Name - } - return "" +// Deprecated: Use ReportConnectionRequest.ProtoReflect.Descriptor instead. +func (*ReportConnectionRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{33} } -func (x *Stats_Metric_Label) GetValue() string { +func (x *ReportConnectionRequest) GetConnection() *Connection { if x != nil { - return x.Value + return x.Connection } - return "" + return nil } -type BatchUpdateAppHealthRequest_HealthUpdate struct { +type WorkspaceApp_Healthcheck struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Id []byte `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Health AppHealth `protobuf:"varint,2,opt,name=health,proto3,enum=coder.agent.v2.AppHealth" json:"health,omitempty"` + Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` + Interval *durationpb.Duration `protobuf:"bytes,2,opt,name=interval,proto3" json:"interval,omitempty"` + Threshold int32 `protobuf:"varint,3,opt,name=threshold,proto3" json:"threshold,omitempty"` } -func (x *BatchUpdateAppHealthRequest_HealthUpdate) Reset() { - *x = BatchUpdateAppHealthRequest_HealthUpdate{} +func (x *WorkspaceApp_Healthcheck) Reset() { + *x = WorkspaceApp_Healthcheck{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[35] + mi := &file_agent_proto_agent_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *BatchUpdateAppHealthRequest_HealthUpdate) String() string { +func (x *WorkspaceApp_Healthcheck) String() string { return protoimpl.X.MessageStringOf(x) } -func (*BatchUpdateAppHealthRequest_HealthUpdate) ProtoMessage() {} +func (*WorkspaceApp_Healthcheck) ProtoMessage() {} -func (x *BatchUpdateAppHealthRequest_HealthUpdate) ProtoReflect() protoreflect.Message { +func (x *WorkspaceApp_Healthcheck) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_agent_proto_msgTypes[34] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkspaceApp_Healthcheck.ProtoReflect.Descriptor instead. +func (*WorkspaceApp_Healthcheck) Descriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{0, 0} +} + +func (x *WorkspaceApp_Healthcheck) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +func (x *WorkspaceApp_Healthcheck) GetInterval() *durationpb.Duration { + if x != nil { + return x.Interval + } + return nil +} + +func (x *WorkspaceApp_Healthcheck) GetThreshold() int32 { + if x != nil { + return x.Threshold + } + return 0 +} + +type WorkspaceAgentMetadata_Result struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CollectedAt *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=collected_at,json=collectedAt,proto3" json:"collected_at,omitempty"` + Age int64 `protobuf:"varint,2,opt,name=age,proto3" json:"age,omitempty"` + Value string `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"` + Error string `protobuf:"bytes,4,opt,name=error,proto3" json:"error,omitempty"` +} + +func (x *WorkspaceAgentMetadata_Result) Reset() { + *x = WorkspaceAgentMetadata_Result{} + if protoimpl.UnsafeEnabled { + mi := &file_agent_proto_agent_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *WorkspaceAgentMetadata_Result) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkspaceAgentMetadata_Result) ProtoMessage() {} + +func (x *WorkspaceAgentMetadata_Result) ProtoReflect() protoreflect.Message { mi := &file_agent_proto_agent_proto_msgTypes[35] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2679,23 +2838,635 @@ func (x *BatchUpdateAppHealthRequest_HealthUpdate) ProtoReflect() protoreflect.M return mi.MessageOf(x) } -// Deprecated: Use BatchUpdateAppHealthRequest_HealthUpdate.ProtoReflect.Descriptor instead. -func (*BatchUpdateAppHealthRequest_HealthUpdate) Descriptor() ([]byte, []int) { - return file_agent_proto_agent_proto_rawDescGZIP(), []int{12, 0} +// Deprecated: Use WorkspaceAgentMetadata_Result.ProtoReflect.Descriptor instead. +func (*WorkspaceAgentMetadata_Result) Descriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{2, 0} +} + +func (x *WorkspaceAgentMetadata_Result) GetCollectedAt() *timestamppb.Timestamp { + if x != nil { + return x.CollectedAt + } + return nil +} + +func (x *WorkspaceAgentMetadata_Result) GetAge() int64 { + if x != nil { + return x.Age + } + return 0 +} + +func (x *WorkspaceAgentMetadata_Result) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +func (x *WorkspaceAgentMetadata_Result) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +type WorkspaceAgentMetadata_Description struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + DisplayName string `protobuf:"bytes,1,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` + Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` + Script string `protobuf:"bytes,3,opt,name=script,proto3" json:"script,omitempty"` + Interval *durationpb.Duration `protobuf:"bytes,4,opt,name=interval,proto3" json:"interval,omitempty"` + Timeout *durationpb.Duration `protobuf:"bytes,5,opt,name=timeout,proto3" json:"timeout,omitempty"` +} + +func (x *WorkspaceAgentMetadata_Description) Reset() { + *x = WorkspaceAgentMetadata_Description{} + if protoimpl.UnsafeEnabled { + mi := &file_agent_proto_agent_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *WorkspaceAgentMetadata_Description) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkspaceAgentMetadata_Description) ProtoMessage() {} + +func (x *WorkspaceAgentMetadata_Description) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_agent_proto_msgTypes[36] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkspaceAgentMetadata_Description.ProtoReflect.Descriptor instead. +func (*WorkspaceAgentMetadata_Description) Descriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{2, 1} +} + +func (x *WorkspaceAgentMetadata_Description) GetDisplayName() string { + if x != nil { + return x.DisplayName + } + return "" +} + +func (x *WorkspaceAgentMetadata_Description) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *WorkspaceAgentMetadata_Description) GetScript() string { + if x != nil { + return x.Script + } + return "" +} + +func (x *WorkspaceAgentMetadata_Description) GetInterval() *durationpb.Duration { + if x != nil { + return x.Interval + } + return nil +} + +func (x *WorkspaceAgentMetadata_Description) GetTimeout() *durationpb.Duration { + if x != nil { + return x.Timeout + } + return nil +} + +type Stats_Metric struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Type Stats_Metric_Type `protobuf:"varint,2,opt,name=type,proto3,enum=coder.agent.v2.Stats_Metric_Type" json:"type,omitempty"` + Value float64 `protobuf:"fixed64,3,opt,name=value,proto3" json:"value,omitempty"` + Labels []*Stats_Metric_Label `protobuf:"bytes,4,rep,name=labels,proto3" json:"labels,omitempty"` +} + +func (x *Stats_Metric) Reset() { + *x = Stats_Metric{} + if protoimpl.UnsafeEnabled { + mi := &file_agent_proto_agent_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Stats_Metric) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Stats_Metric) ProtoMessage() {} + +func (x *Stats_Metric) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_agent_proto_msgTypes[39] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Stats_Metric.ProtoReflect.Descriptor instead. +func (*Stats_Metric) Descriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{7, 1} +} + +func (x *Stats_Metric) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Stats_Metric) GetType() Stats_Metric_Type { + if x != nil { + return x.Type + } + return Stats_Metric_TYPE_UNSPECIFIED +} + +func (x *Stats_Metric) GetValue() float64 { + if x != nil { + return x.Value + } + return 0 +} + +func (x *Stats_Metric) GetLabels() []*Stats_Metric_Label { + if x != nil { + return x.Labels + } + return nil +} + +type Stats_Metric_Label struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` +} + +func (x *Stats_Metric_Label) Reset() { + *x = Stats_Metric_Label{} + if protoimpl.UnsafeEnabled { + mi := &file_agent_proto_agent_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Stats_Metric_Label) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Stats_Metric_Label) ProtoMessage() {} + +func (x *Stats_Metric_Label) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_agent_proto_msgTypes[40] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Stats_Metric_Label.ProtoReflect.Descriptor instead. +func (*Stats_Metric_Label) Descriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{7, 1, 0} +} + +func (x *Stats_Metric_Label) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Stats_Metric_Label) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +type BatchUpdateAppHealthRequest_HealthUpdate struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id []byte `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Health AppHealth `protobuf:"varint,2,opt,name=health,proto3,enum=coder.agent.v2.AppHealth" json:"health,omitempty"` +} + +func (x *BatchUpdateAppHealthRequest_HealthUpdate) Reset() { + *x = BatchUpdateAppHealthRequest_HealthUpdate{} + if protoimpl.UnsafeEnabled { + mi := &file_agent_proto_agent_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *BatchUpdateAppHealthRequest_HealthUpdate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BatchUpdateAppHealthRequest_HealthUpdate) ProtoMessage() {} + +func (x *BatchUpdateAppHealthRequest_HealthUpdate) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_agent_proto_msgTypes[41] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BatchUpdateAppHealthRequest_HealthUpdate.ProtoReflect.Descriptor instead. +func (*BatchUpdateAppHealthRequest_HealthUpdate) Descriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{12, 0} +} + +func (x *BatchUpdateAppHealthRequest_HealthUpdate) GetId() []byte { + if x != nil { + return x.Id + } + return nil +} + +func (x *BatchUpdateAppHealthRequest_HealthUpdate) GetHealth() AppHealth { + if x != nil { + return x.Health + } + return AppHealth_APP_HEALTH_UNSPECIFIED +} + +type GetResourcesMonitoringConfigurationResponse_Config struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + NumDatapoints int32 `protobuf:"varint,1,opt,name=num_datapoints,json=numDatapoints,proto3" json:"num_datapoints,omitempty"` + CollectionIntervalSeconds int32 `protobuf:"varint,2,opt,name=collection_interval_seconds,json=collectionIntervalSeconds,proto3" json:"collection_interval_seconds,omitempty"` +} + +func (x *GetResourcesMonitoringConfigurationResponse_Config) Reset() { + *x = GetResourcesMonitoringConfigurationResponse_Config{} + if protoimpl.UnsafeEnabled { + mi := &file_agent_proto_agent_proto_msgTypes[42] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetResourcesMonitoringConfigurationResponse_Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetResourcesMonitoringConfigurationResponse_Config) ProtoMessage() {} + +func (x *GetResourcesMonitoringConfigurationResponse_Config) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_agent_proto_msgTypes[42] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetResourcesMonitoringConfigurationResponse_Config.ProtoReflect.Descriptor instead. +func (*GetResourcesMonitoringConfigurationResponse_Config) Descriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{29, 0} +} + +func (x *GetResourcesMonitoringConfigurationResponse_Config) GetNumDatapoints() int32 { + if x != nil { + return x.NumDatapoints + } + return 0 +} + +func (x *GetResourcesMonitoringConfigurationResponse_Config) GetCollectionIntervalSeconds() int32 { + if x != nil { + return x.CollectionIntervalSeconds + } + return 0 +} + +type GetResourcesMonitoringConfigurationResponse_Memory struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` +} + +func (x *GetResourcesMonitoringConfigurationResponse_Memory) Reset() { + *x = GetResourcesMonitoringConfigurationResponse_Memory{} + if protoimpl.UnsafeEnabled { + mi := &file_agent_proto_agent_proto_msgTypes[43] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetResourcesMonitoringConfigurationResponse_Memory) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetResourcesMonitoringConfigurationResponse_Memory) ProtoMessage() {} + +func (x *GetResourcesMonitoringConfigurationResponse_Memory) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_agent_proto_msgTypes[43] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetResourcesMonitoringConfigurationResponse_Memory.ProtoReflect.Descriptor instead. +func (*GetResourcesMonitoringConfigurationResponse_Memory) Descriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{29, 1} +} + +func (x *GetResourcesMonitoringConfigurationResponse_Memory) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +type GetResourcesMonitoringConfigurationResponse_Volume struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` +} + +func (x *GetResourcesMonitoringConfigurationResponse_Volume) Reset() { + *x = GetResourcesMonitoringConfigurationResponse_Volume{} + if protoimpl.UnsafeEnabled { + mi := &file_agent_proto_agent_proto_msgTypes[44] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetResourcesMonitoringConfigurationResponse_Volume) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetResourcesMonitoringConfigurationResponse_Volume) ProtoMessage() {} + +func (x *GetResourcesMonitoringConfigurationResponse_Volume) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_agent_proto_msgTypes[44] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetResourcesMonitoringConfigurationResponse_Volume.ProtoReflect.Descriptor instead. +func (*GetResourcesMonitoringConfigurationResponse_Volume) Descriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{29, 2} +} + +func (x *GetResourcesMonitoringConfigurationResponse_Volume) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +func (x *GetResourcesMonitoringConfigurationResponse_Volume) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +type PushResourcesMonitoringUsageRequest_Datapoint struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CollectedAt *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=collected_at,json=collectedAt,proto3" json:"collected_at,omitempty"` + Memory *PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage `protobuf:"bytes,2,opt,name=memory,proto3,oneof" json:"memory,omitempty"` + Volumes []*PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage `protobuf:"bytes,3,rep,name=volumes,proto3" json:"volumes,omitempty"` +} + +func (x *PushResourcesMonitoringUsageRequest_Datapoint) Reset() { + *x = PushResourcesMonitoringUsageRequest_Datapoint{} + if protoimpl.UnsafeEnabled { + mi := &file_agent_proto_agent_proto_msgTypes[45] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PushResourcesMonitoringUsageRequest_Datapoint) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PushResourcesMonitoringUsageRequest_Datapoint) ProtoMessage() {} + +func (x *PushResourcesMonitoringUsageRequest_Datapoint) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_agent_proto_msgTypes[45] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PushResourcesMonitoringUsageRequest_Datapoint.ProtoReflect.Descriptor instead. +func (*PushResourcesMonitoringUsageRequest_Datapoint) Descriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{30, 0} } -func (x *BatchUpdateAppHealthRequest_HealthUpdate) GetId() []byte { +func (x *PushResourcesMonitoringUsageRequest_Datapoint) GetCollectedAt() *timestamppb.Timestamp { if x != nil { - return x.Id + return x.CollectedAt } return nil } -func (x *BatchUpdateAppHealthRequest_HealthUpdate) GetHealth() AppHealth { +func (x *PushResourcesMonitoringUsageRequest_Datapoint) GetMemory() *PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage { if x != nil { - return x.Health + return x.Memory } - return AppHealth_APP_HEALTH_UNSPECIFIED + return nil +} + +func (x *PushResourcesMonitoringUsageRequest_Datapoint) GetVolumes() []*PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage { + if x != nil { + return x.Volumes + } + return nil +} + +type PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Used int64 `protobuf:"varint,1,opt,name=used,proto3" json:"used,omitempty"` + Total int64 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"` +} + +func (x *PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) Reset() { + *x = PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage{} + if protoimpl.UnsafeEnabled { + mi := &file_agent_proto_agent_proto_msgTypes[46] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) ProtoMessage() {} + +func (x *PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_agent_proto_msgTypes[46] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage.ProtoReflect.Descriptor instead. +func (*PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) Descriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{30, 0, 0} +} + +func (x *PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) GetUsed() int64 { + if x != nil { + return x.Used + } + return 0 +} + +func (x *PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) GetTotal() int64 { + if x != nil { + return x.Total + } + return 0 +} + +type PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Volume string `protobuf:"bytes,1,opt,name=volume,proto3" json:"volume,omitempty"` + Used int64 `protobuf:"varint,2,opt,name=used,proto3" json:"used,omitempty"` + Total int64 `protobuf:"varint,3,opt,name=total,proto3" json:"total,omitempty"` +} + +func (x *PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) Reset() { + *x = PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{} + if protoimpl.UnsafeEnabled { + mi := &file_agent_proto_agent_proto_msgTypes[47] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) ProtoMessage() {} + +func (x *PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_agent_proto_msgTypes[47] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage.ProtoReflect.Descriptor instead. +func (*PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) Descriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{30, 0, 1} +} + +func (x *PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) GetVolume() string { + if x != nil { + return x.Volume + } + return "" +} + +func (x *PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) GetUsed() int64 { + if x != nil { + return x.Used + } + return 0 +} + +func (x *PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) GetTotal() int64 { + if x != nil { + return x.Total + } + return 0 } var File_agent_proto_agent_proto protoreflect.FileDescriptor @@ -2709,462 +3480,596 @@ var file_agent_proto_agent_proto_rawDesc = []byte{ 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x94, 0x06, 0x0a, 0x0c, 0x57, 0x6f, 0x72, 0x6b, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x70, 0x70, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x78, - 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x65, 0x78, - 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x6c, 0x75, 0x67, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x73, 0x6c, 0x75, 0x67, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, - 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, - 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, - 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x18, - 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x73, - 0x75, 0x62, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, - 0x73, 0x75, 0x62, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x75, 0x62, - 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0d, 0x73, 0x75, 0x62, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4e, 0x61, 0x6d, 0x65, - 0x12, 0x4e, 0x0a, 0x0d, 0x73, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x6c, 0x65, 0x76, 0x65, - 0x6c, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x29, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, - 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x41, 0x70, 0x70, 0x2e, 0x53, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, - 0x65, 0x6c, 0x52, 0x0c, 0x73, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, - 0x12, 0x4a, 0x0a, 0x0b, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x18, - 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, - 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x41, 0x70, 0x70, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x52, - 0x0b, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x3b, 0x0a, 0x06, - 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x23, 0x2e, 0x63, - 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x70, 0x70, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, - 0x68, 0x52, 0x06, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x69, 0x64, - 0x64, 0x65, 0x6e, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x68, 0x69, 0x64, 0x64, 0x65, - 0x6e, 0x1a, 0x74, 0x0a, 0x0b, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, - 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, - 0x72, 0x6c, 0x12, 0x35, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, - 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x68, 0x72, - 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x74, 0x68, - 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x22, 0x57, 0x0a, 0x0c, 0x53, 0x68, 0x61, 0x72, 0x69, - 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1d, 0x0a, 0x19, 0x53, 0x48, 0x41, 0x52, 0x49, - 0x4e, 0x47, 0x5f, 0x4c, 0x45, 0x56, 0x45, 0x4c, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, - 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x4f, 0x57, 0x4e, 0x45, 0x52, 0x10, - 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, - 0x45, 0x44, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x10, 0x03, - 0x22, 0x5c, 0x0a, 0x06, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x12, 0x48, 0x45, - 0x41, 0x4c, 0x54, 0x48, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, - 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, 0x45, 0x44, 0x10, 0x01, - 0x12, 0x10, 0x0a, 0x0c, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, 0x4c, 0x49, 0x5a, 0x49, 0x4e, 0x47, - 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x03, 0x12, - 0x0d, 0x0a, 0x09, 0x55, 0x4e, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x04, 0x22, 0xd9, - 0x02, 0x0a, 0x14, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, - 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x12, 0x22, 0x0a, 0x0d, 0x6c, 0x6f, 0x67, 0x5f, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, - 0x6c, 0x6f, 0x67, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x6c, - 0x6f, 0x67, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, - 0x6f, 0x67, 0x50, 0x61, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x12, 0x12, - 0x0a, 0x04, 0x63, 0x72, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x72, - 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0c, 0x72, 0x75, 0x6e, 0x5f, 0x6f, 0x6e, 0x5f, 0x73, 0x74, 0x61, - 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x72, 0x75, 0x6e, 0x4f, 0x6e, 0x53, - 0x74, 0x61, 0x72, 0x74, 0x12, 0x1e, 0x0a, 0x0b, 0x72, 0x75, 0x6e, 0x5f, 0x6f, 0x6e, 0x5f, 0x73, - 0x74, 0x6f, 0x70, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x72, 0x75, 0x6e, 0x4f, 0x6e, - 0x53, 0x74, 0x6f, 0x70, 0x12, 0x2c, 0x0a, 0x12, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x62, 0x6c, - 0x6f, 0x63, 0x6b, 0x73, 0x5f, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x10, 0x73, 0x74, 0x61, 0x72, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x4c, 0x6f, 0x67, - 0x69, 0x6e, 0x12, 0x33, 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x08, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, - 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, - 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, - 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, - 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, 0x22, 0x86, 0x04, 0x0a, 0x16, 0x57, - 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x45, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, - 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x41, 0x67, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x52, 0x65, - 0x73, 0x75, 0x6c, 0x74, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x54, 0x0a, 0x0b, - 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x94, 0x06, 0x0a, 0x0c, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x41, 0x70, 0x70, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x78, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x65, 0x78, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x6c, 0x75, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x73, 0x6c, 0x75, 0x67, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, + 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, + 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, + 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, + 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x75, 0x62, 0x64, + 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x75, 0x62, + 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x75, 0x62, 0x64, 0x6f, 0x6d, + 0x61, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, + 0x73, 0x75, 0x62, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x4e, 0x0a, + 0x0d, 0x73, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x29, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, + 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, + 0x70, 0x70, 0x2e, 0x53, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, + 0x0c, 0x73, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x4a, 0x0a, + 0x0b, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x18, 0x0b, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, + 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x70, 0x70, + 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x0b, 0x68, 0x65, + 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x3b, 0x0a, 0x06, 0x68, 0x65, 0x61, + 0x6c, 0x74, 0x68, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x23, 0x2e, 0x63, 0x6f, 0x64, 0x65, + 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x41, 0x70, 0x70, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x06, + 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x69, 0x64, 0x64, 0x65, 0x6e, + 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x68, 0x69, 0x64, 0x64, 0x65, 0x6e, 0x1a, 0x74, + 0x0a, 0x0b, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x10, 0x0a, + 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, + 0x35, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, + 0x6f, 0x6c, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x74, 0x68, 0x72, 0x65, 0x73, + 0x68, 0x6f, 0x6c, 0x64, 0x22, 0x57, 0x0a, 0x0c, 0x53, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x4c, + 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1d, 0x0a, 0x19, 0x53, 0x48, 0x41, 0x52, 0x49, 0x4e, 0x47, 0x5f, + 0x4c, 0x45, 0x56, 0x45, 0x4c, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, + 0x44, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x4f, 0x57, 0x4e, 0x45, 0x52, 0x10, 0x01, 0x12, 0x11, + 0x0a, 0x0d, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x45, 0x44, 0x10, + 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x10, 0x03, 0x22, 0x5c, 0x0a, + 0x06, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x12, 0x48, 0x45, 0x41, 0x4c, 0x54, + 0x48, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, + 0x0c, 0x0a, 0x08, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, 0x45, 0x44, 0x10, 0x01, 0x12, 0x10, 0x0a, + 0x0c, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, 0x4c, 0x49, 0x5a, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, + 0x0b, 0x0a, 0x07, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x03, 0x12, 0x0d, 0x0a, 0x09, + 0x55, 0x4e, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x04, 0x22, 0xd9, 0x02, 0x0a, 0x14, + 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x12, 0x22, 0x0a, 0x0d, 0x6c, 0x6f, 0x67, 0x5f, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x6c, 0x6f, 0x67, + 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x6c, 0x6f, 0x67, 0x5f, + 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x50, + 0x61, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x63, + 0x72, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x72, 0x6f, 0x6e, 0x12, + 0x20, 0x0a, 0x0c, 0x72, 0x75, 0x6e, 0x5f, 0x6f, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x72, 0x75, 0x6e, 0x4f, 0x6e, 0x53, 0x74, 0x61, 0x72, + 0x74, 0x12, 0x1e, 0x0a, 0x0b, 0x72, 0x75, 0x6e, 0x5f, 0x6f, 0x6e, 0x5f, 0x73, 0x74, 0x6f, 0x70, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x72, 0x75, 0x6e, 0x4f, 0x6e, 0x53, 0x74, 0x6f, + 0x70, 0x12, 0x2c, 0x0a, 0x12, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, + 0x73, 0x5f, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x73, + 0x74, 0x61, 0x72, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, + 0x33, 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x74, 0x69, 0x6d, + 0x65, 0x6f, 0x75, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, + 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, 0x22, 0x86, 0x04, 0x0a, 0x16, 0x57, 0x6f, 0x72, 0x6b, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x12, 0x45, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, + 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, + 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x52, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x54, 0x0a, 0x0b, 0x64, 0x65, 0x73, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x32, + 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, + 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x1a, + 0x85, 0x01, 0x0a, 0x06, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x3d, 0x0a, 0x0c, 0x63, 0x6f, + 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0b, 0x63, 0x6f, + 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x67, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x61, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x1a, 0xc6, 0x01, 0x0a, 0x0b, 0x44, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, + 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, + 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, + 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x06, + 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x12, 0x35, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x33, 0x0a, 0x07, 0x74, + 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, + 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, + 0x22, 0xea, 0x06, 0x0a, 0x08, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, + 0x08, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x07, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x67, 0x65, 0x6e, + 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x67, + 0x65, 0x6e, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x6f, 0x77, 0x6e, 0x65, 0x72, + 0x5f, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0d, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x21, + 0x0a, 0x0c, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x0e, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, + 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x28, 0x0a, 0x10, 0x67, 0x69, 0x74, 0x5f, + 0x61, 0x75, 0x74, 0x68, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0d, 0x52, 0x0e, 0x67, 0x69, 0x74, 0x41, 0x75, 0x74, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x73, 0x12, 0x67, 0x0a, 0x15, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, + 0x74, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, - 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, - 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x1a, 0x85, 0x01, 0x0a, 0x06, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x3d, 0x0a, - 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, - 0x0b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x10, 0x0a, 0x03, - 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x61, 0x67, 0x65, 0x12, 0x14, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x1a, 0xc6, 0x01, 0x0a, 0x0b, 0x44, - 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, - 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, - 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, - 0x16, 0x0a, 0x06, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x12, 0x35, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, - 0x76, 0x61, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x33, - 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x74, 0x69, 0x6d, 0x65, - 0x6f, 0x75, 0x74, 0x22, 0xea, 0x06, 0x0a, 0x08, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, - 0x12, 0x19, 0x0a, 0x08, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x07, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x61, - 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x09, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x6f, 0x77, - 0x6e, 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0d, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0d, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, - 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, - 0x64, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x28, 0x0a, 0x10, 0x67, - 0x69, 0x74, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x67, 0x69, 0x74, 0x41, 0x75, 0x74, 0x68, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x73, 0x12, 0x67, 0x0a, 0x15, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, - 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x18, 0x03, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, - 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x2e, 0x45, - 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, - 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x14, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, - 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x12, 0x1c, - 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x32, 0x0a, 0x16, - 0x76, 0x73, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x70, 0x72, 0x6f, - 0x78, 0x79, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x76, 0x73, - 0x43, 0x6f, 0x64, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x55, 0x72, 0x69, - 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x6f, 0x74, 0x64, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x6f, 0x74, 0x64, 0x50, 0x61, 0x74, 0x68, 0x12, 0x3c, 0x0a, - 0x1a, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x5f, - 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x18, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, - 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x32, 0x0a, 0x15, 0x64, - 0x65, 0x72, 0x70, 0x5f, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x5f, 0x77, 0x65, 0x62, 0x73, 0x6f, 0x63, - 0x6b, 0x65, 0x74, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x64, 0x65, 0x72, 0x70, - 0x46, 0x6f, 0x72, 0x63, 0x65, 0x57, 0x65, 0x62, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x12, - 0x34, 0x0a, 0x08, 0x64, 0x65, 0x72, 0x70, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x09, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, - 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x44, 0x45, 0x52, 0x50, 0x4d, 0x61, 0x70, 0x52, 0x07, 0x64, 0x65, - 0x72, 0x70, 0x4d, 0x61, 0x70, 0x12, 0x3e, 0x0a, 0x07, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x73, - 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, + 0x76, 0x32, 0x2e, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x2e, 0x45, 0x6e, 0x76, 0x69, + 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x14, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, + 0x6e, 0x74, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x64, + 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, + 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x32, 0x0a, 0x16, 0x76, 0x73, 0x5f, + 0x63, 0x6f, 0x64, 0x65, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x5f, + 0x75, 0x72, 0x69, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x76, 0x73, 0x43, 0x6f, 0x64, + 0x65, 0x50, 0x6f, 0x72, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x55, 0x72, 0x69, 0x12, 0x1b, 0x0a, + 0x09, 0x6d, 0x6f, 0x74, 0x64, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x6d, 0x6f, 0x74, 0x64, 0x50, 0x61, 0x74, 0x68, 0x12, 0x3c, 0x0a, 0x1a, 0x64, 0x69, + 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x5f, 0x63, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x18, + 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x32, 0x0a, 0x15, 0x64, 0x65, 0x72, 0x70, + 0x5f, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x5f, 0x77, 0x65, 0x62, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, + 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x64, 0x65, 0x72, 0x70, 0x46, 0x6f, 0x72, + 0x63, 0x65, 0x57, 0x65, 0x62, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x12, 0x34, 0x0a, 0x08, + 0x64, 0x65, 0x72, 0x70, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, + 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, + 0x32, 0x2e, 0x44, 0x45, 0x52, 0x50, 0x4d, 0x61, 0x70, 0x52, 0x07, 0x64, 0x65, 0x72, 0x70, 0x4d, + 0x61, 0x70, 0x12, 0x3e, 0x0a, 0x07, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x73, 0x18, 0x0a, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, + 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, + 0x65, 0x6e, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x52, 0x07, 0x73, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x73, 0x12, 0x30, 0x0a, 0x04, 0x61, 0x70, 0x70, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x1c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, + 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x70, 0x70, 0x52, 0x04, + 0x61, 0x70, 0x70, 0x73, 0x12, 0x4e, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x18, 0x0c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x52, 0x07, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x73, 0x12, 0x30, 0x0a, 0x04, 0x61, 0x70, 0x70, 0x73, 0x18, 0x0b, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x70, - 0x70, 0x52, 0x04, 0x61, 0x70, 0x70, 0x73, 0x12, 0x4e, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x63, 0x6f, 0x64, 0x65, - 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x2e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x6d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x47, 0x0a, 0x19, 0x45, 0x6e, 0x76, 0x69, 0x72, - 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, - 0x22, 0x14, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x6e, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, - 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, - 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x62, - 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, - 0x64, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x22, 0x19, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x22, 0xb3, 0x07, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x5f, 0x0a, 0x14, 0x63, - 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x5f, 0x62, 0x79, 0x5f, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, + 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x44, + 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x1a, 0x47, 0x0a, 0x19, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, + 0x65, 0x6e, 0x74, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x14, 0x0a, + 0x12, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x22, 0x6e, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, + 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x18, + 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x62, 0x61, 0x63, 0x6b, + 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0f, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x43, 0x6f, + 0x6c, 0x6f, 0x72, 0x22, 0x19, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xb3, + 0x07, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x5f, 0x0a, 0x14, 0x63, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x5f, 0x62, 0x79, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x43, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x50, 0x72, 0x6f, 0x74, 0x6f, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x12, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x42, 0x79, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x43, + 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x3f, 0x0a, 0x1c, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x6d, 0x65, 0x64, 0x69, 0x61, 0x6e, 0x5f, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, + 0x79, 0x5f, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x01, 0x52, 0x19, 0x63, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x64, 0x69, 0x61, 0x6e, 0x4c, 0x61, 0x74, 0x65, + 0x6e, 0x63, 0x79, 0x4d, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x78, 0x5f, 0x70, 0x61, 0x63, 0x6b, + 0x65, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x72, 0x78, 0x50, 0x61, 0x63, + 0x6b, 0x65, 0x74, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x72, 0x78, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x72, 0x78, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, + 0x1d, 0x0a, 0x0a, 0x74, 0x78, 0x5f, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x78, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x12, 0x19, + 0x0a, 0x08, 0x74, 0x78, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x07, 0x74, 0x78, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x30, 0x0a, 0x14, 0x73, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x76, 0x73, 0x63, 0x6f, 0x64, + 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x56, 0x73, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x36, 0x0a, 0x17, 0x73, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x6a, 0x65, 0x74, + 0x62, 0x72, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x15, 0x73, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x4a, 0x65, 0x74, 0x62, 0x72, 0x61, + 0x69, 0x6e, 0x73, 0x12, 0x43, 0x0a, 0x1e, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6e, + 0x67, 0x5f, 0x70, 0x74, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x1b, 0x73, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x74, 0x79, 0x12, 0x2a, 0x0a, 0x11, 0x73, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x73, 0x73, 0x68, 0x18, 0x0b, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x0f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, + 0x74, 0x53, 0x73, 0x68, 0x12, 0x36, 0x0a, 0x07, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x18, + 0x0c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, + 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x4d, 0x65, 0x74, + 0x72, 0x69, 0x63, 0x52, 0x07, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x1a, 0x45, 0x0a, 0x17, + 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x50, 0x72, 0x6f, + 0x74, 0x6f, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, + 0x02, 0x38, 0x01, 0x1a, 0x8e, 0x02, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x12, 0x12, + 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x21, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, + 0x32, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x2e, 0x54, + 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x01, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, + 0x3a, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, + 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x2e, 0x4c, 0x61, + 0x62, 0x65, 0x6c, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x1a, 0x31, 0x0a, 0x05, 0x4c, + 0x61, 0x62, 0x65, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x34, + 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x10, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, + 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, + 0x43, 0x4f, 0x55, 0x4e, 0x54, 0x45, 0x52, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x47, 0x41, 0x55, + 0x47, 0x45, 0x10, 0x02, 0x22, 0x41, 0x0a, 0x12, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, + 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x05, 0x73, 0x74, + 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, - 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x50, 0x72, - 0x6f, 0x74, 0x6f, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x12, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x29, 0x0a, 0x10, - 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x3f, 0x0a, 0x1c, 0x63, 0x6f, 0x6e, 0x6e, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x65, 0x64, 0x69, 0x61, 0x6e, 0x5f, 0x6c, 0x61, 0x74, - 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x01, 0x52, 0x19, 0x63, - 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x64, 0x69, 0x61, 0x6e, 0x4c, - 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x4d, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x78, 0x5f, 0x70, - 0x61, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x72, 0x78, - 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x72, 0x78, 0x5f, 0x62, 0x79, - 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x72, 0x78, 0x42, 0x79, 0x74, - 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x78, 0x5f, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x73, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x78, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, - 0x73, 0x12, 0x19, 0x0a, 0x08, 0x74, 0x78, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x07, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x07, 0x74, 0x78, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x30, 0x0a, 0x14, - 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x76, 0x73, - 0x63, 0x6f, 0x64, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x73, 0x65, 0x73, 0x73, - 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x56, 0x73, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x36, - 0x0a, 0x17, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, - 0x6a, 0x65, 0x74, 0x62, 0x72, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x15, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x4a, 0x65, 0x74, - 0x62, 0x72, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x43, 0x0a, 0x1e, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, - 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x70, 0x74, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x1b, - 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x63, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x74, 0x79, 0x12, 0x2a, 0x0a, 0x11, 0x73, - 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x73, 0x73, 0x68, - 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, - 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x73, 0x68, 0x12, 0x36, 0x0a, 0x07, 0x6d, 0x65, 0x74, 0x72, 0x69, - 0x63, 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, - 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x2e, - 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x52, 0x07, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x1a, - 0x45, 0x0a, 0x17, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, - 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x8e, 0x02, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, - 0x63, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, - 0x63, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x01, 0x52, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x12, 0x3a, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, - 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, - 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x1a, 0x31, - 0x0a, 0x05, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x22, 0x34, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x10, 0x54, 0x59, 0x50, - 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, - 0x0b, 0x0a, 0x07, 0x43, 0x4f, 0x55, 0x4e, 0x54, 0x45, 0x52, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, - 0x47, 0x41, 0x55, 0x47, 0x45, 0x10, 0x02, 0x22, 0x41, 0x0a, 0x12, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2b, 0x0a, - 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x63, - 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, - 0x61, 0x74, 0x73, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x22, 0x59, 0x0a, 0x13, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x42, 0x0a, 0x0f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x76, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0e, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x74, - 0x65, 0x72, 0x76, 0x61, 0x6c, 0x22, 0xae, 0x02, 0x0a, 0x09, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, - 0x63, 0x6c, 0x65, 0x12, 0x35, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, - 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x2e, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x68, - 0x61, 0x6e, 0x67, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6e, - 0x67, 0x65, 0x64, 0x41, 0x74, 0x22, 0xae, 0x01, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, - 0x15, 0x0a, 0x11, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, - 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, - 0x44, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x53, 0x54, 0x41, 0x52, 0x54, 0x49, 0x4e, 0x47, 0x10, - 0x02, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x54, 0x41, 0x52, 0x54, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, - 0x55, 0x54, 0x10, 0x03, 0x12, 0x0f, 0x0a, 0x0b, 0x53, 0x54, 0x41, 0x52, 0x54, 0x5f, 0x45, 0x52, - 0x52, 0x4f, 0x52, 0x10, 0x04, 0x12, 0x09, 0x0a, 0x05, 0x52, 0x45, 0x41, 0x44, 0x59, 0x10, 0x05, - 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x48, 0x55, 0x54, 0x54, 0x49, 0x4e, 0x47, 0x5f, 0x44, 0x4f, 0x57, - 0x4e, 0x10, 0x06, 0x12, 0x14, 0x0a, 0x10, 0x53, 0x48, 0x55, 0x54, 0x44, 0x4f, 0x57, 0x4e, 0x5f, - 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, 0x07, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x48, 0x55, - 0x54, 0x44, 0x4f, 0x57, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x08, 0x12, 0x07, 0x0a, - 0x03, 0x4f, 0x46, 0x46, 0x10, 0x09, 0x22, 0x51, 0x0a, 0x16, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x37, 0x0a, 0x09, 0x6c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x52, 0x09, - 0x6c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x22, 0xc4, 0x01, 0x0a, 0x1b, 0x42, 0x61, - 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, - 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x52, 0x0a, 0x07, 0x75, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x63, 0x6f, 0x64, - 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, - 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x52, 0x07, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x1a, 0x51, 0x0a, - 0x0c, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, - 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, 0x12, 0x31, 0x0a, - 0x06, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, - 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x41, - 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x06, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, - 0x22, 0x1e, 0x0a, 0x1c, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, - 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0xe8, 0x01, 0x0a, 0x07, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x18, 0x0a, 0x07, - 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2d, 0x0a, 0x12, 0x65, 0x78, 0x70, 0x61, 0x6e, 0x64, - 0x65, 0x64, 0x5f, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x11, 0x65, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x65, 0x64, 0x44, 0x69, 0x72, 0x65, - 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x41, 0x0a, 0x0a, 0x73, 0x75, 0x62, 0x73, 0x79, 0x73, 0x74, - 0x65, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x63, 0x6f, 0x64, 0x65, - 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, - 0x75, 0x70, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x0a, 0x73, 0x75, - 0x62, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x51, 0x0a, 0x09, 0x53, 0x75, 0x62, 0x73, - 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x55, 0x42, 0x53, 0x59, 0x53, 0x54, - 0x45, 0x4d, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, - 0x12, 0x0a, 0x0a, 0x06, 0x45, 0x4e, 0x56, 0x42, 0x4f, 0x58, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, - 0x45, 0x4e, 0x56, 0x42, 0x55, 0x49, 0x4c, 0x44, 0x45, 0x52, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09, - 0x45, 0x58, 0x45, 0x43, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x03, 0x22, 0x49, 0x0a, 0x14, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x07, 0x73, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, - 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x52, 0x07, 0x73, - 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x22, 0x63, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x03, 0x6b, 0x65, 0x79, 0x12, 0x45, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, - 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, - 0x67, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x52, 0x65, 0x73, - 0x75, 0x6c, 0x74, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x52, 0x0a, 0x1a, 0x42, - 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x08, 0x6d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x63, 0x6f, - 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, - 0x1d, 0x0a, 0x1b, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xde, - 0x01, 0x0a, 0x03, 0x4c, 0x6f, 0x67, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x64, 0x5f, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, - 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x2f, 0x0a, 0x05, 0x6c, 0x65, 0x76, - 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, - 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x6f, 0x67, 0x2e, 0x4c, 0x65, - 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x53, 0x0a, 0x05, 0x4c, 0x65, - 0x76, 0x65, 0x6c, 0x12, 0x15, 0x0a, 0x11, 0x4c, 0x45, 0x56, 0x45, 0x4c, 0x5f, 0x55, 0x4e, 0x53, - 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, - 0x41, 0x43, 0x45, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x02, - 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, - 0x52, 0x4e, 0x10, 0x04, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x05, 0x22, - 0x65, 0x0a, 0x16, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, - 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x0d, 0x6c, 0x6f, 0x67, - 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, - 0x52, 0x0b, 0x6c, 0x6f, 0x67, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x27, 0x0a, - 0x04, 0x6c, 0x6f, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x6f, - 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x6f, 0x67, - 0x52, 0x04, 0x6c, 0x6f, 0x67, 0x73, 0x22, 0x47, 0x0a, 0x17, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x2c, 0x0a, 0x12, 0x6c, 0x6f, 0x67, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x5f, 0x65, - 0x78, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x6c, - 0x6f, 0x67, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x45, 0x78, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x22, - 0x1f, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x22, 0x71, 0x0a, 0x1e, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x14, 0x61, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x5f, 0x62, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x1c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, - 0x32, 0x2e, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x13, - 0x61, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, - 0x65, 0x72, 0x73, 0x22, 0x6d, 0x0a, 0x0c, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x18, 0x0a, - 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, - 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x62, 0x61, 0x63, 0x6b, 0x67, - 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0f, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x43, 0x6f, 0x6c, - 0x6f, 0x72, 0x22, 0x56, 0x0a, 0x24, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, - 0x67, 0x65, 0x6e, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, - 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x06, 0x74, 0x69, - 0x6d, 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x64, - 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x69, 0x6d, 0x69, - 0x6e, 0x67, 0x52, 0x06, 0x74, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x22, 0x27, 0x0a, 0x25, 0x57, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0xfd, 0x02, 0x0a, 0x06, 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x12, 0x1b, - 0x0a, 0x09, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x08, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x49, 0x64, 0x12, 0x30, 0x0a, 0x05, 0x73, - 0x74, 0x61, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x2c, 0x0a, - 0x03, 0x65, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x65, - 0x78, 0x69, 0x74, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, - 0x65, 0x78, 0x69, 0x74, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x32, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x67, - 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, - 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x2e, - 0x53, 0x74, 0x61, 0x67, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x12, 0x35, 0x0a, 0x06, - 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1d, 0x2e, 0x63, - 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x69, - 0x6d, 0x69, 0x6e, 0x67, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x22, 0x26, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x67, 0x65, 0x12, 0x09, 0x0a, 0x05, - 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x54, 0x4f, 0x50, 0x10, - 0x01, 0x12, 0x08, 0x0a, 0x04, 0x43, 0x52, 0x4f, 0x4e, 0x10, 0x02, 0x22, 0x46, 0x0a, 0x06, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x06, 0x0a, 0x02, 0x4f, 0x4b, 0x10, 0x00, 0x12, 0x10, 0x0a, - 0x0c, 0x45, 0x58, 0x49, 0x54, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x10, 0x01, 0x12, - 0x0d, 0x0a, 0x09, 0x54, 0x49, 0x4d, 0x45, 0x44, 0x5f, 0x4f, 0x55, 0x54, 0x10, 0x02, 0x12, 0x13, - 0x0a, 0x0f, 0x50, 0x49, 0x50, 0x45, 0x53, 0x5f, 0x4c, 0x45, 0x46, 0x54, 0x5f, 0x4f, 0x50, 0x45, - 0x4e, 0x10, 0x03, 0x2a, 0x63, 0x0a, 0x09, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, - 0x12, 0x1a, 0x0a, 0x16, 0x41, 0x50, 0x50, 0x5f, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x5f, 0x55, - 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, - 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, 0x45, 0x44, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x49, 0x4e, - 0x49, 0x54, 0x49, 0x41, 0x4c, 0x49, 0x5a, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, - 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x03, 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e, 0x48, - 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x04, 0x32, 0xef, 0x07, 0x0a, 0x05, 0x41, 0x67, 0x65, - 0x6e, 0x74, 0x12, 0x4b, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, - 0x74, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, - 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, - 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, - 0x5a, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, - 0x6e, 0x65, 0x72, 0x12, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, - 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x63, - 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x56, 0x0a, 0x0b, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, - 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, + 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x22, 0x59, 0x0a, 0x13, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x42, + 0x0a, 0x0f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, + 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x0e, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, + 0x61, 0x6c, 0x22, 0xae, 0x02, 0x0a, 0x09, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, + 0x12, 0x35, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x1f, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, + 0x2e, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, + 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x67, + 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, + 0x41, 0x74, 0x22, 0xae, 0x01, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x15, 0x0a, 0x11, + 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, + 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, + 0x12, 0x0c, 0x0a, 0x08, 0x53, 0x54, 0x41, 0x52, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x11, + 0x0a, 0x0d, 0x53, 0x54, 0x41, 0x52, 0x54, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, + 0x03, 0x12, 0x0f, 0x0a, 0x0b, 0x53, 0x54, 0x41, 0x52, 0x54, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, + 0x10, 0x04, 0x12, 0x09, 0x0a, 0x05, 0x52, 0x45, 0x41, 0x44, 0x59, 0x10, 0x05, 0x12, 0x11, 0x0a, + 0x0d, 0x53, 0x48, 0x55, 0x54, 0x54, 0x49, 0x4e, 0x47, 0x5f, 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x06, + 0x12, 0x14, 0x0a, 0x10, 0x53, 0x48, 0x55, 0x54, 0x44, 0x4f, 0x57, 0x4e, 0x5f, 0x54, 0x49, 0x4d, + 0x45, 0x4f, 0x55, 0x54, 0x10, 0x07, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x48, 0x55, 0x54, 0x44, 0x4f, + 0x57, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x08, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x46, + 0x46, 0x10, 0x09, 0x22, 0x51, 0x0a, 0x16, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x66, + 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x37, 0x0a, + 0x09, 0x6c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, + 0x32, 0x2e, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x52, 0x09, 0x6c, 0x69, 0x66, + 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x22, 0xc4, 0x01, 0x0a, 0x1b, 0x42, 0x61, 0x74, 0x63, 0x68, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x52, 0x0a, 0x07, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x52, 0x07, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x1a, 0x51, 0x0a, 0x0c, 0x48, 0x65, + 0x61, 0x6c, 0x74, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, 0x12, 0x31, 0x0a, 0x06, 0x68, 0x65, + 0x61, 0x6c, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x41, 0x70, 0x70, 0x48, + 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x06, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x22, 0x1e, 0x0a, + 0x1c, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, + 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xe8, 0x01, + 0x0a, 0x07, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x2d, 0x0a, 0x12, 0x65, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x65, 0x64, 0x5f, + 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x11, 0x65, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x65, 0x64, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, + 0x72, 0x79, 0x12, 0x41, 0x0a, 0x0a, 0x73, 0x75, 0x62, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x73, + 0x18, 0x03, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x2e, + 0x53, 0x75, 0x62, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x0a, 0x73, 0x75, 0x62, 0x73, 0x79, + 0x73, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x51, 0x0a, 0x09, 0x53, 0x75, 0x62, 0x73, 0x79, 0x73, 0x74, + 0x65, 0x6d, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x55, 0x42, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, 0x5f, + 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0a, 0x0a, + 0x06, 0x45, 0x4e, 0x56, 0x42, 0x4f, 0x58, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x45, 0x4e, 0x56, + 0x42, 0x55, 0x49, 0x4c, 0x44, 0x45, 0x52, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09, 0x45, 0x58, 0x45, + 0x43, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x03, 0x22, 0x49, 0x0a, 0x14, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x31, 0x0a, 0x07, 0x73, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, + 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x52, 0x07, 0x73, 0x74, 0x61, 0x72, + 0x74, 0x75, 0x70, 0x22, 0x63, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, + 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, + 0x79, 0x12, 0x45, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, + 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, + 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, + 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x52, 0x0a, 0x1a, 0x42, 0x61, 0x74, 0x63, + 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x1d, 0x0a, 0x1b, + 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xde, 0x01, 0x0a, 0x03, + 0x4c, 0x6f, 0x67, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, + 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x16, + 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x2f, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, + 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x6f, 0x67, 0x2e, 0x4c, 0x65, 0x76, 0x65, 0x6c, + 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x53, 0x0a, 0x05, 0x4c, 0x65, 0x76, 0x65, 0x6c, + 0x12, 0x15, 0x0a, 0x11, 0x4c, 0x45, 0x56, 0x45, 0x4c, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, + 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, + 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x02, 0x12, 0x08, 0x0a, + 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, + 0x04, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x05, 0x22, 0x65, 0x0a, 0x16, + 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x0d, 0x6c, 0x6f, 0x67, 0x5f, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x6c, + 0x6f, 0x67, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x27, 0x0a, 0x04, 0x6c, 0x6f, + 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x6f, 0x67, 0x52, 0x04, 0x6c, + 0x6f, 0x67, 0x73, 0x22, 0x47, 0x0a, 0x17, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, + 0x0a, 0x12, 0x6c, 0x6f, 0x67, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x5f, 0x65, 0x78, 0x63, 0x65, + 0x65, 0x64, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x6c, 0x6f, 0x67, 0x4c, + 0x69, 0x6d, 0x69, 0x74, 0x45, 0x78, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x22, 0x1f, 0x0a, 0x1d, + 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, + 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x71, 0x0a, + 0x1e, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x4f, 0x0a, 0x14, 0x61, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, + 0x62, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, + 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x13, 0x61, 0x6e, 0x6e, + 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, + 0x22, 0x6d, 0x0a, 0x0c, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, + 0x6e, 0x64, 0x5f, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, + 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x22, + 0x56, 0x0a, 0x24, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, + 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x06, 0x74, 0x69, 0x6d, 0x69, 0x6e, + 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x52, + 0x06, 0x74, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x22, 0x27, 0x0a, 0x25, 0x57, 0x6f, 0x72, 0x6b, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, + 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0xfd, 0x02, 0x0a, 0x06, 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x12, 0x1b, 0x0a, 0x09, 0x73, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, + 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x49, 0x64, 0x12, 0x30, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, + 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x2c, 0x0a, 0x03, 0x65, 0x6e, + 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x65, 0x78, 0x69, 0x74, + 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x65, 0x78, 0x69, + 0x74, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x32, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, + 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x2e, 0x53, 0x74, 0x61, + 0x67, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x12, 0x35, 0x0a, 0x06, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1d, 0x2e, 0x63, 0x6f, 0x64, 0x65, + 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x69, 0x6d, 0x69, 0x6e, + 0x67, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x22, 0x26, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x67, 0x65, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x54, 0x41, + 0x52, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x54, 0x4f, 0x50, 0x10, 0x01, 0x12, 0x08, + 0x0a, 0x04, 0x43, 0x52, 0x4f, 0x4e, 0x10, 0x02, 0x22, 0x46, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x12, 0x06, 0x0a, 0x02, 0x4f, 0x4b, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x45, 0x58, + 0x49, 0x54, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, + 0x54, 0x49, 0x4d, 0x45, 0x44, 0x5f, 0x4f, 0x55, 0x54, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x50, + 0x49, 0x50, 0x45, 0x53, 0x5f, 0x4c, 0x45, 0x46, 0x54, 0x5f, 0x4f, 0x50, 0x45, 0x4e, 0x10, 0x03, + 0x22, 0x2c, 0x0a, 0x2a, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, + 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xa0, + 0x04, 0x0a, 0x2b, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, + 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5a, + 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x42, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x66, - 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, - 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69, - 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, + 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, + 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x5f, 0x0a, 0x06, 0x6d, 0x65, + 0x6d, 0x6f, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x42, 0x2e, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, + 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x48, 0x00, + 0x52, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x88, 0x01, 0x01, 0x12, 0x5c, 0x0a, 0x07, 0x76, + 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x42, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, + 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, + 0x72, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, + 0x52, 0x07, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x1a, 0x6f, 0x0a, 0x06, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x0e, 0x6e, 0x75, 0x6d, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x70, + 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x6e, 0x75, 0x6d, + 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x3e, 0x0a, 0x1b, 0x63, 0x6f, + 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, + 0x6c, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x19, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x74, 0x65, 0x72, + 0x76, 0x61, 0x6c, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x1a, 0x22, 0x0a, 0x06, 0x4d, 0x65, + 0x6d, 0x6f, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x1a, 0x36, + 0x0a, 0x06, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, + 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x6d, 0x65, 0x6d, 0x6f, 0x72, + 0x79, 0x22, 0xb3, 0x04, 0x0a, 0x23, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, + 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x5d, 0x0a, 0x0a, 0x64, 0x61, 0x74, + 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3d, 0x2e, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x50, + 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, + 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x0a, 0x64, 0x61, + 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x1a, 0xac, 0x03, 0x0a, 0x09, 0x44, 0x61, 0x74, + 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x3d, 0x0a, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, + 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, + 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x66, 0x0a, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x49, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, + 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, + 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x70, + 0x6f, 0x69, 0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x55, 0x73, 0x61, 0x67, 0x65, + 0x48, 0x00, 0x52, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x88, 0x01, 0x01, 0x12, 0x63, 0x0a, + 0x07, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x49, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, - 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x72, 0x0a, 0x15, 0x42, 0x61, 0x74, - 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, - 0x68, 0x73, 0x12, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, - 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, - 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x2c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, - 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, - 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, - 0x0d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x24, + 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, + 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x2e, 0x56, 0x6f, + 0x6c, 0x75, 0x6d, 0x65, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x07, 0x76, 0x6f, 0x6c, 0x75, 0x6d, + 0x65, 0x73, 0x1a, 0x37, 0x0a, 0x0b, 0x4d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x55, 0x73, 0x61, 0x67, + 0x65, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x04, 0x75, 0x73, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x1a, 0x4f, 0x0a, 0x0b, 0x56, + 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x6f, + 0x6c, 0x75, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x76, 0x6f, 0x6c, 0x75, + 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x04, 0x75, 0x73, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x09, 0x0a, 0x07, + 0x5f, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x22, 0x26, 0x0a, 0x24, 0x50, 0x75, 0x73, 0x68, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, + 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0xb6, 0x03, 0x0a, 0x0a, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, + 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, 0x12, 0x39, + 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x21, + 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, + 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x33, 0x0a, 0x04, 0x74, 0x79, 0x70, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x38, + 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x70, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1b, 0x0a, 0x06, 0x72, 0x65, 0x61, + 0x73, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x06, 0x72, 0x65, 0x61, + 0x73, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x22, 0x3d, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x16, 0x0a, 0x12, 0x41, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, + 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x43, 0x4f, 0x4e, 0x4e, + 0x45, 0x43, 0x54, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x44, 0x49, 0x53, 0x43, 0x4f, 0x4e, 0x4e, + 0x45, 0x43, 0x54, 0x10, 0x02, 0x22, 0x56, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, + 0x10, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, + 0x44, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x53, 0x53, 0x48, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, + 0x56, 0x53, 0x43, 0x4f, 0x44, 0x45, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09, 0x4a, 0x45, 0x54, 0x42, + 0x52, 0x41, 0x49, 0x4e, 0x53, 0x10, 0x03, 0x12, 0x14, 0x0a, 0x10, 0x52, 0x45, 0x43, 0x4f, 0x4e, + 0x4e, 0x45, 0x43, 0x54, 0x49, 0x4e, 0x47, 0x5f, 0x50, 0x54, 0x59, 0x10, 0x04, 0x42, 0x09, 0x0a, + 0x07, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x55, 0x0a, 0x17, 0x52, 0x65, 0x70, 0x6f, + 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x3a, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2a, + 0x63, 0x0a, 0x09, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x1a, 0x0a, 0x16, + 0x41, 0x50, 0x50, 0x5f, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, + 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x49, 0x53, 0x41, + 0x42, 0x4c, 0x45, 0x44, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, + 0x4c, 0x49, 0x5a, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x48, 0x45, 0x41, 0x4c, + 0x54, 0x48, 0x59, 0x10, 0x03, 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e, 0x48, 0x45, 0x41, 0x4c, 0x54, + 0x48, 0x59, 0x10, 0x04, 0x32, 0xf1, 0x0a, 0x0a, 0x05, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x4b, + 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x22, 0x2e, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, + 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x18, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, + 0x76, 0x32, 0x2e, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x5a, 0x0a, 0x10, 0x47, + 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, + 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, + 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, + 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x56, 0x0a, 0x0b, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, + 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x54, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, + 0x6c, 0x65, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, + 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, + 0x63, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x66, 0x65, + 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x72, 0x0a, 0x15, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x73, 0x12, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, - 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x6e, 0x0a, - 0x13, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x12, 0x2a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, - 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, + 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, + 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x63, 0x6f, + 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, + 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, + 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x0d, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x24, 0x2e, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x17, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, + 0x32, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x6e, 0x0a, 0x13, 0x42, 0x61, 0x74, + 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x12, 0x2a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x62, 0x0a, - 0x0f, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, - 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, - 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, - 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x77, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x2d, 0x2e, 0x63, 0x6f, - 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, - 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, - 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x63, 0x6f, 0x64, - 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x41, - 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, - 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7e, 0x0a, 0x0f, 0x53, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x34, 0x2e, - 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, - 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, - 0x65, 0x6e, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, - 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x27, 0x5a, 0x25, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, - 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, + 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x62, 0x0a, 0x0f, 0x42, 0x61, 0x74, + 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x26, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, + 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, + 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x77, 0x0a, + 0x16, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, + 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, + 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7e, 0x0a, 0x0f, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x34, 0x2e, 0x63, 0x6f, 0x64, 0x65, + 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, + 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x35, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, + 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x9e, 0x01, 0x0a, 0x23, 0x47, 0x65, 0x74, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, + 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3a, + 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, + 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, + 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3b, 0x2e, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, + 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x89, 0x01, 0x0a, 0x1c, 0x50, 0x75, 0x73, 0x68, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, + 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x33, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, + 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x50, + 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, + 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x10, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x43, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x27, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -3179,123 +4084,155 @@ func file_agent_proto_agent_proto_rawDescGZIP() []byte { return file_agent_proto_agent_proto_rawDescData } -var file_agent_proto_agent_proto_enumTypes = make([]protoimpl.EnumInfo, 9) -var file_agent_proto_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 36) +var file_agent_proto_agent_proto_enumTypes = make([]protoimpl.EnumInfo, 11) +var file_agent_proto_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 48) var file_agent_proto_agent_proto_goTypes = []interface{}{ - (AppHealth)(0), // 0: coder.agent.v2.AppHealth - (WorkspaceApp_SharingLevel)(0), // 1: coder.agent.v2.WorkspaceApp.SharingLevel - (WorkspaceApp_Health)(0), // 2: coder.agent.v2.WorkspaceApp.Health - (Stats_Metric_Type)(0), // 3: coder.agent.v2.Stats.Metric.Type - (Lifecycle_State)(0), // 4: coder.agent.v2.Lifecycle.State - (Startup_Subsystem)(0), // 5: coder.agent.v2.Startup.Subsystem - (Log_Level)(0), // 6: coder.agent.v2.Log.Level - (Timing_Stage)(0), // 7: coder.agent.v2.Timing.Stage - (Timing_Status)(0), // 8: coder.agent.v2.Timing.Status - (*WorkspaceApp)(nil), // 9: coder.agent.v2.WorkspaceApp - (*WorkspaceAgentScript)(nil), // 10: coder.agent.v2.WorkspaceAgentScript - (*WorkspaceAgentMetadata)(nil), // 11: coder.agent.v2.WorkspaceAgentMetadata - (*Manifest)(nil), // 12: coder.agent.v2.Manifest - (*GetManifestRequest)(nil), // 13: coder.agent.v2.GetManifestRequest - (*ServiceBanner)(nil), // 14: coder.agent.v2.ServiceBanner - (*GetServiceBannerRequest)(nil), // 15: coder.agent.v2.GetServiceBannerRequest - (*Stats)(nil), // 16: coder.agent.v2.Stats - (*UpdateStatsRequest)(nil), // 17: coder.agent.v2.UpdateStatsRequest - (*UpdateStatsResponse)(nil), // 18: coder.agent.v2.UpdateStatsResponse - (*Lifecycle)(nil), // 19: coder.agent.v2.Lifecycle - (*UpdateLifecycleRequest)(nil), // 20: coder.agent.v2.UpdateLifecycleRequest - (*BatchUpdateAppHealthRequest)(nil), // 21: coder.agent.v2.BatchUpdateAppHealthRequest - (*BatchUpdateAppHealthResponse)(nil), // 22: coder.agent.v2.BatchUpdateAppHealthResponse - (*Startup)(nil), // 23: coder.agent.v2.Startup - (*UpdateStartupRequest)(nil), // 24: coder.agent.v2.UpdateStartupRequest - (*Metadata)(nil), // 25: coder.agent.v2.Metadata - (*BatchUpdateMetadataRequest)(nil), // 26: coder.agent.v2.BatchUpdateMetadataRequest - (*BatchUpdateMetadataResponse)(nil), // 27: coder.agent.v2.BatchUpdateMetadataResponse - (*Log)(nil), // 28: coder.agent.v2.Log - (*BatchCreateLogsRequest)(nil), // 29: coder.agent.v2.BatchCreateLogsRequest - (*BatchCreateLogsResponse)(nil), // 30: coder.agent.v2.BatchCreateLogsResponse - (*GetAnnouncementBannersRequest)(nil), // 31: coder.agent.v2.GetAnnouncementBannersRequest - (*GetAnnouncementBannersResponse)(nil), // 32: coder.agent.v2.GetAnnouncementBannersResponse - (*BannerConfig)(nil), // 33: coder.agent.v2.BannerConfig - (*WorkspaceAgentScriptCompletedRequest)(nil), // 34: coder.agent.v2.WorkspaceAgentScriptCompletedRequest - (*WorkspaceAgentScriptCompletedResponse)(nil), // 35: coder.agent.v2.WorkspaceAgentScriptCompletedResponse - (*Timing)(nil), // 36: coder.agent.v2.Timing - (*WorkspaceApp_Healthcheck)(nil), // 37: coder.agent.v2.WorkspaceApp.Healthcheck - (*WorkspaceAgentMetadata_Result)(nil), // 38: coder.agent.v2.WorkspaceAgentMetadata.Result - (*WorkspaceAgentMetadata_Description)(nil), // 39: coder.agent.v2.WorkspaceAgentMetadata.Description - nil, // 40: coder.agent.v2.Manifest.EnvironmentVariablesEntry - nil, // 41: coder.agent.v2.Stats.ConnectionsByProtoEntry - (*Stats_Metric)(nil), // 42: coder.agent.v2.Stats.Metric - (*Stats_Metric_Label)(nil), // 43: coder.agent.v2.Stats.Metric.Label - (*BatchUpdateAppHealthRequest_HealthUpdate)(nil), // 44: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate - (*durationpb.Duration)(nil), // 45: google.protobuf.Duration - (*proto.DERPMap)(nil), // 46: coder.tailnet.v2.DERPMap - (*timestamppb.Timestamp)(nil), // 47: google.protobuf.Timestamp + (AppHealth)(0), // 0: coder.agent.v2.AppHealth + (WorkspaceApp_SharingLevel)(0), // 1: coder.agent.v2.WorkspaceApp.SharingLevel + (WorkspaceApp_Health)(0), // 2: coder.agent.v2.WorkspaceApp.Health + (Stats_Metric_Type)(0), // 3: coder.agent.v2.Stats.Metric.Type + (Lifecycle_State)(0), // 4: coder.agent.v2.Lifecycle.State + (Startup_Subsystem)(0), // 5: coder.agent.v2.Startup.Subsystem + (Log_Level)(0), // 6: coder.agent.v2.Log.Level + (Timing_Stage)(0), // 7: coder.agent.v2.Timing.Stage + (Timing_Status)(0), // 8: coder.agent.v2.Timing.Status + (Connection_Action)(0), // 9: coder.agent.v2.Connection.Action + (Connection_Type)(0), // 10: coder.agent.v2.Connection.Type + (*WorkspaceApp)(nil), // 11: coder.agent.v2.WorkspaceApp + (*WorkspaceAgentScript)(nil), // 12: coder.agent.v2.WorkspaceAgentScript + (*WorkspaceAgentMetadata)(nil), // 13: coder.agent.v2.WorkspaceAgentMetadata + (*Manifest)(nil), // 14: coder.agent.v2.Manifest + (*GetManifestRequest)(nil), // 15: coder.agent.v2.GetManifestRequest + (*ServiceBanner)(nil), // 16: coder.agent.v2.ServiceBanner + (*GetServiceBannerRequest)(nil), // 17: coder.agent.v2.GetServiceBannerRequest + (*Stats)(nil), // 18: coder.agent.v2.Stats + (*UpdateStatsRequest)(nil), // 19: coder.agent.v2.UpdateStatsRequest + (*UpdateStatsResponse)(nil), // 20: coder.agent.v2.UpdateStatsResponse + (*Lifecycle)(nil), // 21: coder.agent.v2.Lifecycle + (*UpdateLifecycleRequest)(nil), // 22: coder.agent.v2.UpdateLifecycleRequest + (*BatchUpdateAppHealthRequest)(nil), // 23: coder.agent.v2.BatchUpdateAppHealthRequest + (*BatchUpdateAppHealthResponse)(nil), // 24: coder.agent.v2.BatchUpdateAppHealthResponse + (*Startup)(nil), // 25: coder.agent.v2.Startup + (*UpdateStartupRequest)(nil), // 26: coder.agent.v2.UpdateStartupRequest + (*Metadata)(nil), // 27: coder.agent.v2.Metadata + (*BatchUpdateMetadataRequest)(nil), // 28: coder.agent.v2.BatchUpdateMetadataRequest + (*BatchUpdateMetadataResponse)(nil), // 29: coder.agent.v2.BatchUpdateMetadataResponse + (*Log)(nil), // 30: coder.agent.v2.Log + (*BatchCreateLogsRequest)(nil), // 31: coder.agent.v2.BatchCreateLogsRequest + (*BatchCreateLogsResponse)(nil), // 32: coder.agent.v2.BatchCreateLogsResponse + (*GetAnnouncementBannersRequest)(nil), // 33: coder.agent.v2.GetAnnouncementBannersRequest + (*GetAnnouncementBannersResponse)(nil), // 34: coder.agent.v2.GetAnnouncementBannersResponse + (*BannerConfig)(nil), // 35: coder.agent.v2.BannerConfig + (*WorkspaceAgentScriptCompletedRequest)(nil), // 36: coder.agent.v2.WorkspaceAgentScriptCompletedRequest + (*WorkspaceAgentScriptCompletedResponse)(nil), // 37: coder.agent.v2.WorkspaceAgentScriptCompletedResponse + (*Timing)(nil), // 38: coder.agent.v2.Timing + (*GetResourcesMonitoringConfigurationRequest)(nil), // 39: coder.agent.v2.GetResourcesMonitoringConfigurationRequest + (*GetResourcesMonitoringConfigurationResponse)(nil), // 40: coder.agent.v2.GetResourcesMonitoringConfigurationResponse + (*PushResourcesMonitoringUsageRequest)(nil), // 41: coder.agent.v2.PushResourcesMonitoringUsageRequest + (*PushResourcesMonitoringUsageResponse)(nil), // 42: coder.agent.v2.PushResourcesMonitoringUsageResponse + (*Connection)(nil), // 43: coder.agent.v2.Connection + (*ReportConnectionRequest)(nil), // 44: coder.agent.v2.ReportConnectionRequest + (*WorkspaceApp_Healthcheck)(nil), // 45: coder.agent.v2.WorkspaceApp.Healthcheck + (*WorkspaceAgentMetadata_Result)(nil), // 46: coder.agent.v2.WorkspaceAgentMetadata.Result + (*WorkspaceAgentMetadata_Description)(nil), // 47: coder.agent.v2.WorkspaceAgentMetadata.Description + nil, // 48: coder.agent.v2.Manifest.EnvironmentVariablesEntry + nil, // 49: coder.agent.v2.Stats.ConnectionsByProtoEntry + (*Stats_Metric)(nil), // 50: coder.agent.v2.Stats.Metric + (*Stats_Metric_Label)(nil), // 51: coder.agent.v2.Stats.Metric.Label + (*BatchUpdateAppHealthRequest_HealthUpdate)(nil), // 52: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate + (*GetResourcesMonitoringConfigurationResponse_Config)(nil), // 53: coder.agent.v2.GetResourcesMonitoringConfigurationResponse.Config + (*GetResourcesMonitoringConfigurationResponse_Memory)(nil), // 54: coder.agent.v2.GetResourcesMonitoringConfigurationResponse.Memory + (*GetResourcesMonitoringConfigurationResponse_Volume)(nil), // 55: coder.agent.v2.GetResourcesMonitoringConfigurationResponse.Volume + (*PushResourcesMonitoringUsageRequest_Datapoint)(nil), // 56: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint + (*PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage)(nil), // 57: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.MemoryUsage + (*PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage)(nil), // 58: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.VolumeUsage + (*durationpb.Duration)(nil), // 59: google.protobuf.Duration + (*proto.DERPMap)(nil), // 60: coder.tailnet.v2.DERPMap + (*timestamppb.Timestamp)(nil), // 61: google.protobuf.Timestamp + (*emptypb.Empty)(nil), // 62: google.protobuf.Empty } var file_agent_proto_agent_proto_depIdxs = []int32{ 1, // 0: coder.agent.v2.WorkspaceApp.sharing_level:type_name -> coder.agent.v2.WorkspaceApp.SharingLevel - 37, // 1: coder.agent.v2.WorkspaceApp.healthcheck:type_name -> coder.agent.v2.WorkspaceApp.Healthcheck + 45, // 1: coder.agent.v2.WorkspaceApp.healthcheck:type_name -> coder.agent.v2.WorkspaceApp.Healthcheck 2, // 2: coder.agent.v2.WorkspaceApp.health:type_name -> coder.agent.v2.WorkspaceApp.Health - 45, // 3: coder.agent.v2.WorkspaceAgentScript.timeout:type_name -> google.protobuf.Duration - 38, // 4: coder.agent.v2.WorkspaceAgentMetadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result - 39, // 5: coder.agent.v2.WorkspaceAgentMetadata.description:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description - 40, // 6: coder.agent.v2.Manifest.environment_variables:type_name -> coder.agent.v2.Manifest.EnvironmentVariablesEntry - 46, // 7: coder.agent.v2.Manifest.derp_map:type_name -> coder.tailnet.v2.DERPMap - 10, // 8: coder.agent.v2.Manifest.scripts:type_name -> coder.agent.v2.WorkspaceAgentScript - 9, // 9: coder.agent.v2.Manifest.apps:type_name -> coder.agent.v2.WorkspaceApp - 39, // 10: coder.agent.v2.Manifest.metadata:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description - 41, // 11: coder.agent.v2.Stats.connections_by_proto:type_name -> coder.agent.v2.Stats.ConnectionsByProtoEntry - 42, // 12: coder.agent.v2.Stats.metrics:type_name -> coder.agent.v2.Stats.Metric - 16, // 13: coder.agent.v2.UpdateStatsRequest.stats:type_name -> coder.agent.v2.Stats - 45, // 14: coder.agent.v2.UpdateStatsResponse.report_interval:type_name -> google.protobuf.Duration + 59, // 3: coder.agent.v2.WorkspaceAgentScript.timeout:type_name -> google.protobuf.Duration + 46, // 4: coder.agent.v2.WorkspaceAgentMetadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result + 47, // 5: coder.agent.v2.WorkspaceAgentMetadata.description:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description + 48, // 6: coder.agent.v2.Manifest.environment_variables:type_name -> coder.agent.v2.Manifest.EnvironmentVariablesEntry + 60, // 7: coder.agent.v2.Manifest.derp_map:type_name -> coder.tailnet.v2.DERPMap + 12, // 8: coder.agent.v2.Manifest.scripts:type_name -> coder.agent.v2.WorkspaceAgentScript + 11, // 9: coder.agent.v2.Manifest.apps:type_name -> coder.agent.v2.WorkspaceApp + 47, // 10: coder.agent.v2.Manifest.metadata:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description + 49, // 11: coder.agent.v2.Stats.connections_by_proto:type_name -> coder.agent.v2.Stats.ConnectionsByProtoEntry + 50, // 12: coder.agent.v2.Stats.metrics:type_name -> coder.agent.v2.Stats.Metric + 18, // 13: coder.agent.v2.UpdateStatsRequest.stats:type_name -> coder.agent.v2.Stats + 59, // 14: coder.agent.v2.UpdateStatsResponse.report_interval:type_name -> google.protobuf.Duration 4, // 15: coder.agent.v2.Lifecycle.state:type_name -> coder.agent.v2.Lifecycle.State - 47, // 16: coder.agent.v2.Lifecycle.changed_at:type_name -> google.protobuf.Timestamp - 19, // 17: coder.agent.v2.UpdateLifecycleRequest.lifecycle:type_name -> coder.agent.v2.Lifecycle - 44, // 18: coder.agent.v2.BatchUpdateAppHealthRequest.updates:type_name -> coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate + 61, // 16: coder.agent.v2.Lifecycle.changed_at:type_name -> google.protobuf.Timestamp + 21, // 17: coder.agent.v2.UpdateLifecycleRequest.lifecycle:type_name -> coder.agent.v2.Lifecycle + 52, // 18: coder.agent.v2.BatchUpdateAppHealthRequest.updates:type_name -> coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate 5, // 19: coder.agent.v2.Startup.subsystems:type_name -> coder.agent.v2.Startup.Subsystem - 23, // 20: coder.agent.v2.UpdateStartupRequest.startup:type_name -> coder.agent.v2.Startup - 38, // 21: coder.agent.v2.Metadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result - 25, // 22: coder.agent.v2.BatchUpdateMetadataRequest.metadata:type_name -> coder.agent.v2.Metadata - 47, // 23: coder.agent.v2.Log.created_at:type_name -> google.protobuf.Timestamp + 25, // 20: coder.agent.v2.UpdateStartupRequest.startup:type_name -> coder.agent.v2.Startup + 46, // 21: coder.agent.v2.Metadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result + 27, // 22: coder.agent.v2.BatchUpdateMetadataRequest.metadata:type_name -> coder.agent.v2.Metadata + 61, // 23: coder.agent.v2.Log.created_at:type_name -> google.protobuf.Timestamp 6, // 24: coder.agent.v2.Log.level:type_name -> coder.agent.v2.Log.Level - 28, // 25: coder.agent.v2.BatchCreateLogsRequest.logs:type_name -> coder.agent.v2.Log - 33, // 26: coder.agent.v2.GetAnnouncementBannersResponse.announcement_banners:type_name -> coder.agent.v2.BannerConfig - 36, // 27: coder.agent.v2.WorkspaceAgentScriptCompletedRequest.timing:type_name -> coder.agent.v2.Timing - 47, // 28: coder.agent.v2.Timing.start:type_name -> google.protobuf.Timestamp - 47, // 29: coder.agent.v2.Timing.end:type_name -> google.protobuf.Timestamp + 30, // 25: coder.agent.v2.BatchCreateLogsRequest.logs:type_name -> coder.agent.v2.Log + 35, // 26: coder.agent.v2.GetAnnouncementBannersResponse.announcement_banners:type_name -> coder.agent.v2.BannerConfig + 38, // 27: coder.agent.v2.WorkspaceAgentScriptCompletedRequest.timing:type_name -> coder.agent.v2.Timing + 61, // 28: coder.agent.v2.Timing.start:type_name -> google.protobuf.Timestamp + 61, // 29: coder.agent.v2.Timing.end:type_name -> google.protobuf.Timestamp 7, // 30: coder.agent.v2.Timing.stage:type_name -> coder.agent.v2.Timing.Stage 8, // 31: coder.agent.v2.Timing.status:type_name -> coder.agent.v2.Timing.Status - 45, // 32: coder.agent.v2.WorkspaceApp.Healthcheck.interval:type_name -> google.protobuf.Duration - 47, // 33: coder.agent.v2.WorkspaceAgentMetadata.Result.collected_at:type_name -> google.protobuf.Timestamp - 45, // 34: coder.agent.v2.WorkspaceAgentMetadata.Description.interval:type_name -> google.protobuf.Duration - 45, // 35: coder.agent.v2.WorkspaceAgentMetadata.Description.timeout:type_name -> google.protobuf.Duration - 3, // 36: coder.agent.v2.Stats.Metric.type:type_name -> coder.agent.v2.Stats.Metric.Type - 43, // 37: coder.agent.v2.Stats.Metric.labels:type_name -> coder.agent.v2.Stats.Metric.Label - 0, // 38: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate.health:type_name -> coder.agent.v2.AppHealth - 13, // 39: coder.agent.v2.Agent.GetManifest:input_type -> coder.agent.v2.GetManifestRequest - 15, // 40: coder.agent.v2.Agent.GetServiceBanner:input_type -> coder.agent.v2.GetServiceBannerRequest - 17, // 41: coder.agent.v2.Agent.UpdateStats:input_type -> coder.agent.v2.UpdateStatsRequest - 20, // 42: coder.agent.v2.Agent.UpdateLifecycle:input_type -> coder.agent.v2.UpdateLifecycleRequest - 21, // 43: coder.agent.v2.Agent.BatchUpdateAppHealths:input_type -> coder.agent.v2.BatchUpdateAppHealthRequest - 24, // 44: coder.agent.v2.Agent.UpdateStartup:input_type -> coder.agent.v2.UpdateStartupRequest - 26, // 45: coder.agent.v2.Agent.BatchUpdateMetadata:input_type -> coder.agent.v2.BatchUpdateMetadataRequest - 29, // 46: coder.agent.v2.Agent.BatchCreateLogs:input_type -> coder.agent.v2.BatchCreateLogsRequest - 31, // 47: coder.agent.v2.Agent.GetAnnouncementBanners:input_type -> coder.agent.v2.GetAnnouncementBannersRequest - 34, // 48: coder.agent.v2.Agent.ScriptCompleted:input_type -> coder.agent.v2.WorkspaceAgentScriptCompletedRequest - 12, // 49: coder.agent.v2.Agent.GetManifest:output_type -> coder.agent.v2.Manifest - 14, // 50: coder.agent.v2.Agent.GetServiceBanner:output_type -> coder.agent.v2.ServiceBanner - 18, // 51: coder.agent.v2.Agent.UpdateStats:output_type -> coder.agent.v2.UpdateStatsResponse - 19, // 52: coder.agent.v2.Agent.UpdateLifecycle:output_type -> coder.agent.v2.Lifecycle - 22, // 53: coder.agent.v2.Agent.BatchUpdateAppHealths:output_type -> coder.agent.v2.BatchUpdateAppHealthResponse - 23, // 54: coder.agent.v2.Agent.UpdateStartup:output_type -> coder.agent.v2.Startup - 27, // 55: coder.agent.v2.Agent.BatchUpdateMetadata:output_type -> coder.agent.v2.BatchUpdateMetadataResponse - 30, // 56: coder.agent.v2.Agent.BatchCreateLogs:output_type -> coder.agent.v2.BatchCreateLogsResponse - 32, // 57: coder.agent.v2.Agent.GetAnnouncementBanners:output_type -> coder.agent.v2.GetAnnouncementBannersResponse - 35, // 58: coder.agent.v2.Agent.ScriptCompleted:output_type -> coder.agent.v2.WorkspaceAgentScriptCompletedResponse - 49, // [49:59] is the sub-list for method output_type - 39, // [39:49] is the sub-list for method input_type - 39, // [39:39] is the sub-list for extension type_name - 39, // [39:39] is the sub-list for extension extendee - 0, // [0:39] is the sub-list for field type_name + 53, // 32: coder.agent.v2.GetResourcesMonitoringConfigurationResponse.config:type_name -> coder.agent.v2.GetResourcesMonitoringConfigurationResponse.Config + 54, // 33: coder.agent.v2.GetResourcesMonitoringConfigurationResponse.memory:type_name -> coder.agent.v2.GetResourcesMonitoringConfigurationResponse.Memory + 55, // 34: coder.agent.v2.GetResourcesMonitoringConfigurationResponse.volumes:type_name -> coder.agent.v2.GetResourcesMonitoringConfigurationResponse.Volume + 56, // 35: coder.agent.v2.PushResourcesMonitoringUsageRequest.datapoints:type_name -> coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint + 9, // 36: coder.agent.v2.Connection.action:type_name -> coder.agent.v2.Connection.Action + 10, // 37: coder.agent.v2.Connection.type:type_name -> coder.agent.v2.Connection.Type + 61, // 38: coder.agent.v2.Connection.timestamp:type_name -> google.protobuf.Timestamp + 43, // 39: coder.agent.v2.ReportConnectionRequest.connection:type_name -> coder.agent.v2.Connection + 59, // 40: coder.agent.v2.WorkspaceApp.Healthcheck.interval:type_name -> google.protobuf.Duration + 61, // 41: coder.agent.v2.WorkspaceAgentMetadata.Result.collected_at:type_name -> google.protobuf.Timestamp + 59, // 42: coder.agent.v2.WorkspaceAgentMetadata.Description.interval:type_name -> google.protobuf.Duration + 59, // 43: coder.agent.v2.WorkspaceAgentMetadata.Description.timeout:type_name -> google.protobuf.Duration + 3, // 44: coder.agent.v2.Stats.Metric.type:type_name -> coder.agent.v2.Stats.Metric.Type + 51, // 45: coder.agent.v2.Stats.Metric.labels:type_name -> coder.agent.v2.Stats.Metric.Label + 0, // 46: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate.health:type_name -> coder.agent.v2.AppHealth + 61, // 47: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.collected_at:type_name -> google.protobuf.Timestamp + 57, // 48: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.memory:type_name -> coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.MemoryUsage + 58, // 49: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.volumes:type_name -> coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.VolumeUsage + 15, // 50: coder.agent.v2.Agent.GetManifest:input_type -> coder.agent.v2.GetManifestRequest + 17, // 51: coder.agent.v2.Agent.GetServiceBanner:input_type -> coder.agent.v2.GetServiceBannerRequest + 19, // 52: coder.agent.v2.Agent.UpdateStats:input_type -> coder.agent.v2.UpdateStatsRequest + 22, // 53: coder.agent.v2.Agent.UpdateLifecycle:input_type -> coder.agent.v2.UpdateLifecycleRequest + 23, // 54: coder.agent.v2.Agent.BatchUpdateAppHealths:input_type -> coder.agent.v2.BatchUpdateAppHealthRequest + 26, // 55: coder.agent.v2.Agent.UpdateStartup:input_type -> coder.agent.v2.UpdateStartupRequest + 28, // 56: coder.agent.v2.Agent.BatchUpdateMetadata:input_type -> coder.agent.v2.BatchUpdateMetadataRequest + 31, // 57: coder.agent.v2.Agent.BatchCreateLogs:input_type -> coder.agent.v2.BatchCreateLogsRequest + 33, // 58: coder.agent.v2.Agent.GetAnnouncementBanners:input_type -> coder.agent.v2.GetAnnouncementBannersRequest + 36, // 59: coder.agent.v2.Agent.ScriptCompleted:input_type -> coder.agent.v2.WorkspaceAgentScriptCompletedRequest + 39, // 60: coder.agent.v2.Agent.GetResourcesMonitoringConfiguration:input_type -> coder.agent.v2.GetResourcesMonitoringConfigurationRequest + 41, // 61: coder.agent.v2.Agent.PushResourcesMonitoringUsage:input_type -> coder.agent.v2.PushResourcesMonitoringUsageRequest + 44, // 62: coder.agent.v2.Agent.ReportConnection:input_type -> coder.agent.v2.ReportConnectionRequest + 14, // 63: coder.agent.v2.Agent.GetManifest:output_type -> coder.agent.v2.Manifest + 16, // 64: coder.agent.v2.Agent.GetServiceBanner:output_type -> coder.agent.v2.ServiceBanner + 20, // 65: coder.agent.v2.Agent.UpdateStats:output_type -> coder.agent.v2.UpdateStatsResponse + 21, // 66: coder.agent.v2.Agent.UpdateLifecycle:output_type -> coder.agent.v2.Lifecycle + 24, // 67: coder.agent.v2.Agent.BatchUpdateAppHealths:output_type -> coder.agent.v2.BatchUpdateAppHealthResponse + 25, // 68: coder.agent.v2.Agent.UpdateStartup:output_type -> coder.agent.v2.Startup + 29, // 69: coder.agent.v2.Agent.BatchUpdateMetadata:output_type -> coder.agent.v2.BatchUpdateMetadataResponse + 32, // 70: coder.agent.v2.Agent.BatchCreateLogs:output_type -> coder.agent.v2.BatchCreateLogsResponse + 34, // 71: coder.agent.v2.Agent.GetAnnouncementBanners:output_type -> coder.agent.v2.GetAnnouncementBannersResponse + 37, // 72: coder.agent.v2.Agent.ScriptCompleted:output_type -> coder.agent.v2.WorkspaceAgentScriptCompletedResponse + 40, // 73: coder.agent.v2.Agent.GetResourcesMonitoringConfiguration:output_type -> coder.agent.v2.GetResourcesMonitoringConfigurationResponse + 42, // 74: coder.agent.v2.Agent.PushResourcesMonitoringUsage:output_type -> coder.agent.v2.PushResourcesMonitoringUsageResponse + 62, // 75: coder.agent.v2.Agent.ReportConnection:output_type -> google.protobuf.Empty + 63, // [63:76] is the sub-list for method output_type + 50, // [50:63] is the sub-list for method input_type + 50, // [50:50] is the sub-list for extension type_name + 50, // [50:50] is the sub-list for extension extendee + 0, // [0:50] is the sub-list for field type_name } func init() { file_agent_proto_agent_proto_init() } @@ -3641,7 +4578,7 @@ func file_agent_proto_agent_proto_init() { } } file_agent_proto_agent_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*WorkspaceApp_Healthcheck); i { + switch v := v.(*GetResourcesMonitoringConfigurationRequest); i { case 0: return &v.state case 1: @@ -3653,7 +4590,7 @@ func file_agent_proto_agent_proto_init() { } } file_agent_proto_agent_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*WorkspaceAgentMetadata_Result); i { + switch v := v.(*GetResourcesMonitoringConfigurationResponse); i { case 0: return &v.state case 1: @@ -3665,7 +4602,31 @@ func file_agent_proto_agent_proto_init() { } } file_agent_proto_agent_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*WorkspaceAgentMetadata_Description); i { + switch v := v.(*PushResourcesMonitoringUsageRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_agent_proto_agent_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PushResourcesMonitoringUsageResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_agent_proto_agent_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Connection); i { case 0: return &v.state case 1: @@ -3677,7 +4638,7 @@ func file_agent_proto_agent_proto_init() { } } file_agent_proto_agent_proto_msgTypes[33].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Stats_Metric); i { + switch v := v.(*ReportConnectionRequest); i { case 0: return &v.state case 1: @@ -3689,7 +4650,7 @@ func file_agent_proto_agent_proto_init() { } } file_agent_proto_agent_proto_msgTypes[34].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Stats_Metric_Label); i { + switch v := v.(*WorkspaceApp_Healthcheck); i { case 0: return &v.state case 1: @@ -3701,6 +4662,54 @@ func file_agent_proto_agent_proto_init() { } } file_agent_proto_agent_proto_msgTypes[35].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*WorkspaceAgentMetadata_Result); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_agent_proto_agent_proto_msgTypes[36].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*WorkspaceAgentMetadata_Description); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_agent_proto_agent_proto_msgTypes[39].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Stats_Metric); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_agent_proto_agent_proto_msgTypes[40].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Stats_Metric_Label); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_agent_proto_agent_proto_msgTypes[41].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*BatchUpdateAppHealthRequest_HealthUpdate); i { case 0: return &v.state @@ -3712,14 +4721,89 @@ func file_agent_proto_agent_proto_init() { return nil } } + file_agent_proto_agent_proto_msgTypes[42].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetResourcesMonitoringConfigurationResponse_Config); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_agent_proto_agent_proto_msgTypes[43].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetResourcesMonitoringConfigurationResponse_Memory); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_agent_proto_agent_proto_msgTypes[44].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetResourcesMonitoringConfigurationResponse_Volume); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_agent_proto_agent_proto_msgTypes[45].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PushResourcesMonitoringUsageRequest_Datapoint); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_agent_proto_agent_proto_msgTypes[46].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_agent_proto_agent_proto_msgTypes[47].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } + file_agent_proto_agent_proto_msgTypes[29].OneofWrappers = []interface{}{} + file_agent_proto_agent_proto_msgTypes[32].OneofWrappers = []interface{}{} + file_agent_proto_agent_proto_msgTypes[45].OneofWrappers = []interface{}{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_agent_proto_agent_proto_rawDesc, - NumEnums: 9, - NumMessages: 36, + NumEnums: 11, + NumMessages: 48, NumExtensions: 0, NumServices: 1, }, diff --git a/agent/proto/agent.proto b/agent/proto/agent.proto index f307066fcbfdf..1e59c109ea4d7 100644 --- a/agent/proto/agent.proto +++ b/agent/proto/agent.proto @@ -6,6 +6,7 @@ package coder.agent.v2; import "tailnet/proto/tailnet.proto"; import "google/protobuf/timestamp.proto"; import "google/protobuf/duration.proto"; +import "google/protobuf/empty.proto"; message WorkspaceApp { bytes id = 1; @@ -295,6 +296,78 @@ message Timing { Status status = 6; } +message GetResourcesMonitoringConfigurationRequest { +} + +message GetResourcesMonitoringConfigurationResponse { + message Config { + int32 num_datapoints = 1; + int32 collection_interval_seconds = 2; + } + Config config = 1; + + message Memory { + bool enabled = 1; + } + optional Memory memory = 2; + + message Volume { + bool enabled = 1; + string path = 2; + } + repeated Volume volumes = 3; +} + +message PushResourcesMonitoringUsageRequest { + message Datapoint { + message MemoryUsage { + int64 used = 1; + int64 total = 2; + } + message VolumeUsage { + string volume = 1; + int64 used = 2; + int64 total = 3; + } + + google.protobuf.Timestamp collected_at = 1; + optional MemoryUsage memory = 2; + repeated VolumeUsage volumes = 3; + + } + repeated Datapoint datapoints = 1; +} + +message PushResourcesMonitoringUsageResponse { +} + +message Connection { + enum Action { + ACTION_UNSPECIFIED = 0; + CONNECT = 1; + DISCONNECT = 2; + } + enum Type { + TYPE_UNSPECIFIED = 0; + SSH = 1; + VSCODE = 2; + JETBRAINS = 3; + RECONNECTING_PTY = 4; + } + + bytes id = 1; + Action action = 2; + Type type = 3; + google.protobuf.Timestamp timestamp = 4; + string ip = 5; + int32 status_code = 6; + optional string reason = 7; +} + +message ReportConnectionRequest { + Connection connection = 1; +} + service Agent { rpc GetManifest(GetManifestRequest) returns (Manifest); rpc GetServiceBanner(GetServiceBannerRequest) returns (ServiceBanner); @@ -306,4 +379,7 @@ service Agent { rpc BatchCreateLogs(BatchCreateLogsRequest) returns (BatchCreateLogsResponse); rpc GetAnnouncementBanners(GetAnnouncementBannersRequest) returns (GetAnnouncementBannersResponse); rpc ScriptCompleted(WorkspaceAgentScriptCompletedRequest) returns (WorkspaceAgentScriptCompletedResponse); + rpc GetResourcesMonitoringConfiguration(GetResourcesMonitoringConfigurationRequest) returns (GetResourcesMonitoringConfigurationResponse); + rpc PushResourcesMonitoringUsage(PushResourcesMonitoringUsageRequest) returns (PushResourcesMonitoringUsageResponse); + rpc ReportConnection(ReportConnectionRequest) returns (google.protobuf.Empty); } diff --git a/agent/proto/agent_drpc.pb.go b/agent/proto/agent_drpc.pb.go index 9f7e21c96248c..a9dd8cda726e0 100644 --- a/agent/proto/agent_drpc.pb.go +++ b/agent/proto/agent_drpc.pb.go @@ -9,6 +9,7 @@ import ( errors "errors" protojson "google.golang.org/protobuf/encoding/protojson" proto "google.golang.org/protobuf/proto" + emptypb "google.golang.org/protobuf/types/known/emptypb" drpc "storj.io/drpc" drpcerr "storj.io/drpc/drpcerr" ) @@ -48,6 +49,9 @@ type DRPCAgentClient interface { BatchCreateLogs(ctx context.Context, in *BatchCreateLogsRequest) (*BatchCreateLogsResponse, error) GetAnnouncementBanners(ctx context.Context, in *GetAnnouncementBannersRequest) (*GetAnnouncementBannersResponse, error) ScriptCompleted(ctx context.Context, in *WorkspaceAgentScriptCompletedRequest) (*WorkspaceAgentScriptCompletedResponse, error) + GetResourcesMonitoringConfiguration(ctx context.Context, in *GetResourcesMonitoringConfigurationRequest) (*GetResourcesMonitoringConfigurationResponse, error) + PushResourcesMonitoringUsage(ctx context.Context, in *PushResourcesMonitoringUsageRequest) (*PushResourcesMonitoringUsageResponse, error) + ReportConnection(ctx context.Context, in *ReportConnectionRequest) (*emptypb.Empty, error) } type drpcAgentClient struct { @@ -150,6 +154,33 @@ func (c *drpcAgentClient) ScriptCompleted(ctx context.Context, in *WorkspaceAgen return out, nil } +func (c *drpcAgentClient) GetResourcesMonitoringConfiguration(ctx context.Context, in *GetResourcesMonitoringConfigurationRequest) (*GetResourcesMonitoringConfigurationResponse, error) { + out := new(GetResourcesMonitoringConfigurationResponse) + err := c.cc.Invoke(ctx, "/coder.agent.v2.Agent/GetResourcesMonitoringConfiguration", drpcEncoding_File_agent_proto_agent_proto{}, in, out) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *drpcAgentClient) PushResourcesMonitoringUsage(ctx context.Context, in *PushResourcesMonitoringUsageRequest) (*PushResourcesMonitoringUsageResponse, error) { + out := new(PushResourcesMonitoringUsageResponse) + err := c.cc.Invoke(ctx, "/coder.agent.v2.Agent/PushResourcesMonitoringUsage", drpcEncoding_File_agent_proto_agent_proto{}, in, out) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *drpcAgentClient) ReportConnection(ctx context.Context, in *ReportConnectionRequest) (*emptypb.Empty, error) { + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, "/coder.agent.v2.Agent/ReportConnection", drpcEncoding_File_agent_proto_agent_proto{}, in, out) + if err != nil { + return nil, err + } + return out, nil +} + type DRPCAgentServer interface { GetManifest(context.Context, *GetManifestRequest) (*Manifest, error) GetServiceBanner(context.Context, *GetServiceBannerRequest) (*ServiceBanner, error) @@ -161,6 +192,9 @@ type DRPCAgentServer interface { BatchCreateLogs(context.Context, *BatchCreateLogsRequest) (*BatchCreateLogsResponse, error) GetAnnouncementBanners(context.Context, *GetAnnouncementBannersRequest) (*GetAnnouncementBannersResponse, error) ScriptCompleted(context.Context, *WorkspaceAgentScriptCompletedRequest) (*WorkspaceAgentScriptCompletedResponse, error) + GetResourcesMonitoringConfiguration(context.Context, *GetResourcesMonitoringConfigurationRequest) (*GetResourcesMonitoringConfigurationResponse, error) + PushResourcesMonitoringUsage(context.Context, *PushResourcesMonitoringUsageRequest) (*PushResourcesMonitoringUsageResponse, error) + ReportConnection(context.Context, *ReportConnectionRequest) (*emptypb.Empty, error) } type DRPCAgentUnimplementedServer struct{} @@ -205,9 +239,21 @@ func (s *DRPCAgentUnimplementedServer) ScriptCompleted(context.Context, *Workspa return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) } +func (s *DRPCAgentUnimplementedServer) GetResourcesMonitoringConfiguration(context.Context, *GetResourcesMonitoringConfigurationRequest) (*GetResourcesMonitoringConfigurationResponse, error) { + return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) +} + +func (s *DRPCAgentUnimplementedServer) PushResourcesMonitoringUsage(context.Context, *PushResourcesMonitoringUsageRequest) (*PushResourcesMonitoringUsageResponse, error) { + return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) +} + +func (s *DRPCAgentUnimplementedServer) ReportConnection(context.Context, *ReportConnectionRequest) (*emptypb.Empty, error) { + return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) +} + type DRPCAgentDescription struct{} -func (DRPCAgentDescription) NumMethods() int { return 10 } +func (DRPCAgentDescription) NumMethods() int { return 13 } func (DRPCAgentDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) { switch n { @@ -301,6 +347,33 @@ func (DRPCAgentDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, in1.(*WorkspaceAgentScriptCompletedRequest), ) }, DRPCAgentServer.ScriptCompleted, true + case 10: + return "/coder.agent.v2.Agent/GetResourcesMonitoringConfiguration", drpcEncoding_File_agent_proto_agent_proto{}, + func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { + return srv.(DRPCAgentServer). + GetResourcesMonitoringConfiguration( + ctx, + in1.(*GetResourcesMonitoringConfigurationRequest), + ) + }, DRPCAgentServer.GetResourcesMonitoringConfiguration, true + case 11: + return "/coder.agent.v2.Agent/PushResourcesMonitoringUsage", drpcEncoding_File_agent_proto_agent_proto{}, + func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { + return srv.(DRPCAgentServer). + PushResourcesMonitoringUsage( + ctx, + in1.(*PushResourcesMonitoringUsageRequest), + ) + }, DRPCAgentServer.PushResourcesMonitoringUsage, true + case 12: + return "/coder.agent.v2.Agent/ReportConnection", drpcEncoding_File_agent_proto_agent_proto{}, + func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { + return srv.(DRPCAgentServer). + ReportConnection( + ctx, + in1.(*ReportConnectionRequest), + ) + }, DRPCAgentServer.ReportConnection, true default: return "", nil, nil, nil, false } @@ -469,3 +542,51 @@ func (x *drpcAgent_ScriptCompletedStream) SendAndClose(m *WorkspaceAgentScriptCo } return x.CloseSend() } + +type DRPCAgent_GetResourcesMonitoringConfigurationStream interface { + drpc.Stream + SendAndClose(*GetResourcesMonitoringConfigurationResponse) error +} + +type drpcAgent_GetResourcesMonitoringConfigurationStream struct { + drpc.Stream +} + +func (x *drpcAgent_GetResourcesMonitoringConfigurationStream) SendAndClose(m *GetResourcesMonitoringConfigurationResponse) error { + if err := x.MsgSend(m, drpcEncoding_File_agent_proto_agent_proto{}); err != nil { + return err + } + return x.CloseSend() +} + +type DRPCAgent_PushResourcesMonitoringUsageStream interface { + drpc.Stream + SendAndClose(*PushResourcesMonitoringUsageResponse) error +} + +type drpcAgent_PushResourcesMonitoringUsageStream struct { + drpc.Stream +} + +func (x *drpcAgent_PushResourcesMonitoringUsageStream) SendAndClose(m *PushResourcesMonitoringUsageResponse) error { + if err := x.MsgSend(m, drpcEncoding_File_agent_proto_agent_proto{}); err != nil { + return err + } + return x.CloseSend() +} + +type DRPCAgent_ReportConnectionStream interface { + drpc.Stream + SendAndClose(*emptypb.Empty) error +} + +type drpcAgent_ReportConnectionStream struct { + drpc.Stream +} + +func (x *drpcAgent_ReportConnectionStream) SendAndClose(m *emptypb.Empty) error { + if err := x.MsgSend(m, drpcEncoding_File_agent_proto_agent_proto{}); err != nil { + return err + } + return x.CloseSend() +} diff --git a/agent/proto/agent_drpc_old.go b/agent/proto/agent_drpc_old.go index f46afaba42596..63b666a259c5c 100644 --- a/agent/proto/agent_drpc_old.go +++ b/agent/proto/agent_drpc_old.go @@ -3,6 +3,7 @@ package proto import ( "context" + emptypb "google.golang.org/protobuf/types/known/emptypb" "storj.io/drpc" ) @@ -40,3 +41,12 @@ type DRPCAgentClient23 interface { DRPCAgentClient22 ScriptCompleted(ctx context.Context, in *WorkspaceAgentScriptCompletedRequest) (*WorkspaceAgentScriptCompletedResponse, error) } + +// DRPCAgentClient24 is the Agent API at v2.4. It adds the GetResourcesMonitoringConfiguration, +// PushResourcesMonitoringUsage and ReportConnection RPCs. Compatible with Coder v2.19+ +type DRPCAgentClient24 interface { + DRPCAgentClient23 + GetResourcesMonitoringConfiguration(ctx context.Context, in *GetResourcesMonitoringConfigurationRequest) (*GetResourcesMonitoringConfigurationResponse, error) + PushResourcesMonitoringUsage(ctx context.Context, in *PushResourcesMonitoringUsageRequest) (*PushResourcesMonitoringUsageResponse, error) + ReportConnection(ctx context.Context, in *ReportConnectionRequest) (*emptypb.Empty, error) +} diff --git a/agent/proto/resourcesmonitor/fetcher.go b/agent/proto/resourcesmonitor/fetcher.go new file mode 100644 index 0000000000000..495a249fe9198 --- /dev/null +++ b/agent/proto/resourcesmonitor/fetcher.go @@ -0,0 +1,49 @@ +package resourcesmonitor + +import ( + "golang.org/x/xerrors" + + "github.com/coder/coder/v2/cli/clistat" +) + +type Fetcher interface { + FetchMemory() (total int64, used int64, err error) + FetchVolume(volume string) (total int64, used int64, err error) +} + +type fetcher struct { + *clistat.Statter +} + +//nolint:revive +func NewFetcher(f *clistat.Statter) *fetcher { + return &fetcher{ + f, + } +} + +func (f *fetcher) FetchMemory() (total int64, used int64, err error) { + mem, err := f.HostMemory(clistat.PrefixDefault) + if err != nil { + return 0, 0, xerrors.Errorf("failed to fetch memory: %w", err) + } + + if mem.Total == nil { + return 0, 0, xerrors.New("memory total is nil - can not fetch memory") + } + + return int64(*mem.Total), int64(mem.Used), nil +} + +func (f *fetcher) FetchVolume(volume string) (total int64, used int64, err error) { + vol, err := f.Disk(clistat.PrefixDefault, volume) + if err != nil { + return 0, 0, err + } + + if vol.Total == nil { + return 0, 0, xerrors.New("volume total is nil - can not fetch volume") + } + + return int64(*vol.Total), int64(vol.Used), nil +} diff --git a/agent/proto/resourcesmonitor/queue.go b/agent/proto/resourcesmonitor/queue.go new file mode 100644 index 0000000000000..9f463509f2094 --- /dev/null +++ b/agent/proto/resourcesmonitor/queue.go @@ -0,0 +1,85 @@ +package resourcesmonitor + +import ( + "time" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/coder/coder/v2/agent/proto" +) + +type Datapoint struct { + CollectedAt time.Time + Memory *MemoryDatapoint + Volumes []*VolumeDatapoint +} + +type MemoryDatapoint struct { + Total int64 + Used int64 +} + +type VolumeDatapoint struct { + Path string + Total int64 + Used int64 +} + +// Queue represents a FIFO queue with a fixed size +type Queue struct { + items []Datapoint + size int +} + +// newQueue creates a new Queue with the given size +func NewQueue(size int) *Queue { + return &Queue{ + items: make([]Datapoint, 0, size), + size: size, + } +} + +// Push adds a new item to the queue +func (q *Queue) Push(item Datapoint) { + if len(q.items) >= q.size { + // Remove the first item (FIFO) + q.items = q.items[1:] + } + q.items = append(q.items, item) +} + +func (q *Queue) IsFull() bool { + return len(q.items) == q.size +} + +func (q *Queue) Items() []Datapoint { + return q.items +} + +func (q *Queue) ItemsAsProto() []*proto.PushResourcesMonitoringUsageRequest_Datapoint { + items := make([]*proto.PushResourcesMonitoringUsageRequest_Datapoint, 0, len(q.items)) + + for _, item := range q.items { + protoItem := &proto.PushResourcesMonitoringUsageRequest_Datapoint{ + CollectedAt: timestamppb.New(item.CollectedAt), + } + if item.Memory != nil { + protoItem.Memory = &proto.PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage{ + Total: item.Memory.Total, + Used: item.Memory.Used, + } + } + + for _, volume := range item.Volumes { + protoItem.Volumes = append(protoItem.Volumes, &proto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ + Volume: volume.Path, + Total: volume.Total, + Used: volume.Used, + }) + } + + items = append(items, protoItem) + } + + return items +} diff --git a/agent/proto/resourcesmonitor/queue_test.go b/agent/proto/resourcesmonitor/queue_test.go new file mode 100644 index 0000000000000..a3a8fbc0d0a3a --- /dev/null +++ b/agent/proto/resourcesmonitor/queue_test.go @@ -0,0 +1,92 @@ +package resourcesmonitor_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/agent/proto/resourcesmonitor" +) + +func TestResourceMonitorQueue(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + pushCount int + expected []resourcesmonitor.Datapoint + }{ + { + name: "Push zero", + pushCount: 0, + expected: []resourcesmonitor.Datapoint{}, + }, + { + name: "Push less than capacity", + pushCount: 3, + expected: []resourcesmonitor.Datapoint{ + {Memory: &resourcesmonitor.MemoryDatapoint{Total: 1, Used: 1}}, + {Memory: &resourcesmonitor.MemoryDatapoint{Total: 2, Used: 2}}, + {Memory: &resourcesmonitor.MemoryDatapoint{Total: 3, Used: 3}}, + }, + }, + { + name: "Push exactly capacity", + pushCount: 20, + expected: func() []resourcesmonitor.Datapoint { + var result []resourcesmonitor.Datapoint + for i := 1; i <= 20; i++ { + result = append(result, resourcesmonitor.Datapoint{ + Memory: &resourcesmonitor.MemoryDatapoint{ + Total: int64(i), + Used: int64(i), + }, + }) + } + return result + }(), + }, + { + name: "Push more than capacity", + pushCount: 25, + expected: func() []resourcesmonitor.Datapoint { + var result []resourcesmonitor.Datapoint + for i := 6; i <= 25; i++ { + result = append(result, resourcesmonitor.Datapoint{ + Memory: &resourcesmonitor.MemoryDatapoint{ + Total: int64(i), + Used: int64(i), + }, + }) + } + return result + }(), + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + queue := resourcesmonitor.NewQueue(20) + for i := 1; i <= tt.pushCount; i++ { + queue.Push(resourcesmonitor.Datapoint{ + Memory: &resourcesmonitor.MemoryDatapoint{ + Total: int64(i), + Used: int64(i), + }, + }) + } + + if tt.pushCount < 20 { + require.False(t, queue.IsFull()) + } else { + require.True(t, queue.IsFull()) + require.Equal(t, 20, len(queue.Items())) + } + + require.EqualValues(t, tt.expected, queue.Items()) + }) + } +} diff --git a/agent/proto/resourcesmonitor/resources_monitor.go b/agent/proto/resourcesmonitor/resources_monitor.go new file mode 100644 index 0000000000000..7dea49614c072 --- /dev/null +++ b/agent/proto/resourcesmonitor/resources_monitor.go @@ -0,0 +1,93 @@ +package resourcesmonitor + +import ( + "context" + "time" + + "cdr.dev/slog" + "github.com/coder/coder/v2/agent/proto" + "github.com/coder/quartz" +) + +type monitor struct { + logger slog.Logger + clock quartz.Clock + config *proto.GetResourcesMonitoringConfigurationResponse + resourcesFetcher Fetcher + datapointsPusher datapointsPusher + queue *Queue +} + +//nolint:revive +func NewResourcesMonitor(logger slog.Logger, clock quartz.Clock, config *proto.GetResourcesMonitoringConfigurationResponse, resourcesFetcher Fetcher, datapointsPusher datapointsPusher) *monitor { + return &monitor{ + logger: logger, + clock: clock, + config: config, + resourcesFetcher: resourcesFetcher, + datapointsPusher: datapointsPusher, + queue: NewQueue(int(config.Config.NumDatapoints)), + } +} + +type datapointsPusher interface { + PushResourcesMonitoringUsage(ctx context.Context, req *proto.PushResourcesMonitoringUsageRequest) (*proto.PushResourcesMonitoringUsageResponse, error) +} + +func (m *monitor) Start(ctx context.Context) error { + m.clock.TickerFunc(ctx, time.Duration(m.config.Config.CollectionIntervalSeconds)*time.Second, func() error { + datapoint := Datapoint{ + CollectedAt: m.clock.Now(), + Volumes: make([]*VolumeDatapoint, 0, len(m.config.Volumes)), + } + + if m.config.Memory != nil && m.config.Memory.Enabled { + memTotal, memUsed, err := m.resourcesFetcher.FetchMemory() + if err != nil { + m.logger.Error(ctx, "failed to fetch memory", slog.Error(err)) + } else { + datapoint.Memory = &MemoryDatapoint{ + Total: memTotal, + Used: memUsed, + } + } + } + + for _, volume := range m.config.Volumes { + if !volume.Enabled { + continue + } + + volTotal, volUsed, err := m.resourcesFetcher.FetchVolume(volume.Path) + if err != nil { + m.logger.Error(ctx, "failed to fetch volume", slog.Error(err)) + continue + } + + datapoint.Volumes = append(datapoint.Volumes, &VolumeDatapoint{ + Path: volume.Path, + Total: volTotal, + Used: volUsed, + }) + } + + m.queue.Push(datapoint) + + if m.queue.IsFull() { + _, err := m.datapointsPusher.PushResourcesMonitoringUsage(ctx, &proto.PushResourcesMonitoringUsageRequest{ + Datapoints: m.queue.ItemsAsProto(), + }) + if err != nil { + // We don't want to stop the monitoring if we fail to push the datapoints + // to the server. We just log the error and continue. + // The queue will anyway remove the oldest datapoint and add the new one. + m.logger.Error(ctx, "failed to push resources monitoring usage", slog.Error(err)) + return nil + } + } + + return nil + }, "resources_monitor") + + return nil +} diff --git a/agent/proto/resourcesmonitor/resources_monitor_test.go b/agent/proto/resourcesmonitor/resources_monitor_test.go new file mode 100644 index 0000000000000..ddf3522ecea30 --- /dev/null +++ b/agent/proto/resourcesmonitor/resources_monitor_test.go @@ -0,0 +1,235 @@ +package resourcesmonitor_test + +import ( + "context" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "cdr.dev/slog" + "cdr.dev/slog/sloggers/sloghuman" + "github.com/coder/coder/v2/agent/proto" + "github.com/coder/coder/v2/agent/proto/resourcesmonitor" + "github.com/coder/quartz" +) + +type datapointsPusherMock struct { + PushResourcesMonitoringUsageFunc func(ctx context.Context, req *proto.PushResourcesMonitoringUsageRequest) (*proto.PushResourcesMonitoringUsageResponse, error) +} + +func (d *datapointsPusherMock) PushResourcesMonitoringUsage(ctx context.Context, req *proto.PushResourcesMonitoringUsageRequest) (*proto.PushResourcesMonitoringUsageResponse, error) { + return d.PushResourcesMonitoringUsageFunc(ctx, req) +} + +type fetcher struct { + totalMemory int64 + usedMemory int64 + totalVolume int64 + usedVolume int64 + + errMemory error + errVolume error +} + +func (r *fetcher) FetchMemory() (total int64, used int64, err error) { + return r.totalMemory, r.usedMemory, r.errMemory +} + +func (r *fetcher) FetchVolume(_ string) (total int64, used int64, err error) { + return r.totalVolume, r.usedVolume, r.errVolume +} + +func TestPushResourcesMonitoringWithConfig(t *testing.T) { + t.Parallel() + tests := []struct { + name string + config *proto.GetResourcesMonitoringConfigurationResponse + datapointsPusher func(ctx context.Context, req *proto.PushResourcesMonitoringUsageRequest) (*proto.PushResourcesMonitoringUsageResponse, error) + fetcher resourcesmonitor.Fetcher + numTicks int + }{ + { + name: "SuccessfulMonitoring", + config: &proto.GetResourcesMonitoringConfigurationResponse{ + Config: &proto.GetResourcesMonitoringConfigurationResponse_Config{ + NumDatapoints: 20, + CollectionIntervalSeconds: 1, + }, + Volumes: []*proto.GetResourcesMonitoringConfigurationResponse_Volume{ + { + Enabled: true, + Path: "/", + }, + }, + }, + datapointsPusher: func(_ context.Context, _ *proto.PushResourcesMonitoringUsageRequest) (*proto.PushResourcesMonitoringUsageResponse, error) { + return &proto.PushResourcesMonitoringUsageResponse{}, nil + }, + fetcher: &fetcher{ + totalMemory: 16000, + usedMemory: 8000, + totalVolume: 100000, + usedVolume: 50000, + }, + numTicks: 20, + }, + { + name: "SuccessfulMonitoringLongRun", + config: &proto.GetResourcesMonitoringConfigurationResponse{ + Config: &proto.GetResourcesMonitoringConfigurationResponse_Config{ + NumDatapoints: 20, + CollectionIntervalSeconds: 1, + }, + Volumes: []*proto.GetResourcesMonitoringConfigurationResponse_Volume{ + { + Enabled: true, + Path: "/", + }, + }, + }, + datapointsPusher: func(_ context.Context, _ *proto.PushResourcesMonitoringUsageRequest) (*proto.PushResourcesMonitoringUsageResponse, error) { + return &proto.PushResourcesMonitoringUsageResponse{}, nil + }, + fetcher: &fetcher{ + totalMemory: 16000, + usedMemory: 8000, + totalVolume: 100000, + usedVolume: 50000, + }, + numTicks: 60, + }, + { + // We want to make sure that even if the datapointsPusher fails, the monitoring continues. + name: "ErrorPushingDatapoints", + config: &proto.GetResourcesMonitoringConfigurationResponse{ + Config: &proto.GetResourcesMonitoringConfigurationResponse_Config{ + NumDatapoints: 20, + CollectionIntervalSeconds: 1, + }, + Volumes: []*proto.GetResourcesMonitoringConfigurationResponse_Volume{ + { + Enabled: true, + Path: "/", + }, + }, + }, + datapointsPusher: func(_ context.Context, _ *proto.PushResourcesMonitoringUsageRequest) (*proto.PushResourcesMonitoringUsageResponse, error) { + return nil, assert.AnError + }, + fetcher: &fetcher{ + totalMemory: 16000, + usedMemory: 8000, + totalVolume: 100000, + usedVolume: 50000, + }, + numTicks: 60, + }, + { + // If one of the resources fails to be fetched, the datapoints still should be pushed with the other resources. + name: "ErrorFetchingMemory", + config: &proto.GetResourcesMonitoringConfigurationResponse{ + Config: &proto.GetResourcesMonitoringConfigurationResponse_Config{ + NumDatapoints: 20, + CollectionIntervalSeconds: 1, + }, + Volumes: []*proto.GetResourcesMonitoringConfigurationResponse_Volume{ + { + Enabled: true, + Path: "/", + }, + }, + }, + datapointsPusher: func(_ context.Context, req *proto.PushResourcesMonitoringUsageRequest) (*proto.PushResourcesMonitoringUsageResponse, error) { + require.Len(t, req.Datapoints, 20) + require.Nil(t, req.Datapoints[0].Memory) + require.NotNil(t, req.Datapoints[0].Volumes) + require.Equal(t, &proto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ + Volume: "/", + Total: 100000, + Used: 50000, + }, req.Datapoints[0].Volumes[0]) + + return &proto.PushResourcesMonitoringUsageResponse{}, nil + }, + fetcher: &fetcher{ + totalMemory: 0, + usedMemory: 0, + errMemory: assert.AnError, + totalVolume: 100000, + usedVolume: 50000, + }, + numTicks: 20, + }, + { + // If one of the resources fails to be fetched, the datapoints still should be pushed with the other resources. + name: "ErrorFetchingVolume", + config: &proto.GetResourcesMonitoringConfigurationResponse{ + Config: &proto.GetResourcesMonitoringConfigurationResponse_Config{ + NumDatapoints: 20, + CollectionIntervalSeconds: 1, + }, + Volumes: []*proto.GetResourcesMonitoringConfigurationResponse_Volume{ + { + Enabled: true, + Path: "/", + }, + }, + }, + datapointsPusher: func(_ context.Context, req *proto.PushResourcesMonitoringUsageRequest) (*proto.PushResourcesMonitoringUsageResponse, error) { + require.Len(t, req.Datapoints, 20) + require.Len(t, req.Datapoints[0].Volumes, 0) + + return &proto.PushResourcesMonitoringUsageResponse{}, nil + }, + fetcher: &fetcher{ + totalMemory: 16000, + usedMemory: 8000, + totalVolume: 0, + usedVolume: 0, + errVolume: assert.AnError, + }, + numTicks: 20, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var ( + logger = slog.Make(sloghuman.Sink(os.Stdout)) + clk = quartz.NewMock(t) + counterCalls = 0 + ) + + datapointsPusher := func(ctx context.Context, req *proto.PushResourcesMonitoringUsageRequest) (*proto.PushResourcesMonitoringUsageResponse, error) { + counterCalls++ + return tt.datapointsPusher(ctx, req) + } + + pusher := &datapointsPusherMock{ + PushResourcesMonitoringUsageFunc: datapointsPusher, + } + + monitor := resourcesmonitor.NewResourcesMonitor(logger, clk, tt.config, tt.fetcher, pusher) + require.NoError(t, monitor.Start(ctx)) + + for i := 0; i < tt.numTicks; i++ { + _, waiter := clk.AdvanceNext() + require.NoError(t, waiter.Wait(ctx)) + } + + // expectedCalls is computed with the following logic : + // We have one call per tick, once reached the ${config.NumDatapoints}. + expectedCalls := tt.numTicks - int(tt.config.Config.NumDatapoints) + 1 + require.Equal(t, expectedCalls, counterCalls) + cancel() + }) + } +} diff --git a/agent/reconnectingpty/server.go b/agent/reconnectingpty/server.go index d48c7abec9353..465667c616180 100644 --- a/agent/reconnectingpty/server.go +++ b/agent/reconnectingpty/server.go @@ -159,7 +159,7 @@ func (s *Server) handleConn(ctx context.Context, logger slog.Logger, conn net.Co }() // Empty command will default to the users shell! - cmd, err := s.commandCreator.CreateCommand(ctx, msg.Command, nil) + cmd, err := s.commandCreator.CreateCommand(ctx, msg.Command, nil, nil) if err != nil { s.errorsTotal.WithLabelValues("create_command").Add(1) return xerrors.Errorf("create command: %w", err) diff --git a/agent/usershell/usershell_darwin.go b/agent/usershell/usershell_darwin.go index 0f5be08f82631..0d1c38b0a602c 100644 --- a/agent/usershell/usershell_darwin.go +++ b/agent/usershell/usershell_darwin.go @@ -17,7 +17,7 @@ func Get(username string) (string, error) { return "", xerrors.Errorf("username is nonlocal path: %s", username) } //nolint: gosec // input checked above - out, _ := exec.Command("dscl", ".", "-read", filepath.Join("/Users", username), "UserShell").Output() + out, _ := exec.Command("dscl", ".", "-read", filepath.Join("/Users", username), "UserShell").Output() //nolint:gocritic s, ok := strings.CutPrefix(string(out), "UserShell: ") if ok { return strings.TrimSpace(s), nil diff --git a/buildinfo/resources/.gitignore b/buildinfo/resources/.gitignore new file mode 100644 index 0000000000000..40679b193bdf9 --- /dev/null +++ b/buildinfo/resources/.gitignore @@ -0,0 +1 @@ +*.syso diff --git a/buildinfo/resources/resources.go b/buildinfo/resources/resources.go new file mode 100644 index 0000000000000..cd1e3e70af2b7 --- /dev/null +++ b/buildinfo/resources/resources.go @@ -0,0 +1,8 @@ +// This package is used for embedding .syso resource files into the binary +// during build and does not contain any code. During build, .syso files will be +// dropped in this directory and then removed after the build completes. +// +// This package must be imported by all binaries for this to work. +// +// See build_go.sh for more details. +package resources diff --git a/cli/agent.go b/cli/agent.go index fc96aa6d323c3..e8a46a84e071c 100644 --- a/cli/agent.go +++ b/cli/agent.go @@ -25,6 +25,7 @@ import ( "cdr.dev/slog/sloggers/slogjson" "cdr.dev/slog/sloggers/slogstackdriver" "github.com/coder/coder/v2/agent" + "github.com/coder/coder/v2/agent/agentcontainers" "github.com/coder/coder/v2/agent/agentexec" "github.com/coder/coder/v2/agent/agentssh" "github.com/coder/coder/v2/agent/reaper" @@ -37,21 +38,22 @@ import ( func (r *RootCmd) workspaceAgent() *serpent.Command { var ( - auth string - logDir string - scriptDataDir string - pprofAddress string - noReap bool - sshMaxTimeout time.Duration - tailnetListenPort int64 - prometheusAddress string - debugAddress string - slogHumanPath string - slogJSONPath string - slogStackdriverPath string - blockFileTransfer bool - agentHeaderCommand string - agentHeader []string + auth string + logDir string + scriptDataDir string + pprofAddress string + noReap bool + sshMaxTimeout time.Duration + tailnetListenPort int64 + prometheusAddress string + debugAddress string + slogHumanPath string + slogJSONPath string + slogStackdriverPath string + blockFileTransfer bool + agentHeaderCommand string + agentHeader []string + devcontainersEnabled bool ) cmd := &serpent.Command{ Use: "agent", @@ -314,6 +316,15 @@ func (r *RootCmd) workspaceAgent() *serpent.Command { return xerrors.Errorf("create agent execer: %w", err) } + var containerLister agentcontainers.Lister + if !devcontainersEnabled { + logger.Info(ctx, "agent devcontainer detection not enabled") + containerLister = &agentcontainers.NoopLister{} + } else { + logger.Info(ctx, "agent devcontainer detection enabled") + containerLister = agentcontainers.NewDocker(execer) + } + agnt := agent.New(agent.Options{ Client: client, Logger: logger, @@ -339,6 +350,7 @@ func (r *RootCmd) workspaceAgent() *serpent.Command { PrometheusRegistry: prometheusRegistry, BlockFileTransfer: blockFileTransfer, Execer: execer, + ContainerLister: containerLister, }) promHandler := agent.PrometheusMetricsHandler(prometheusRegistry, logger) @@ -461,6 +473,13 @@ func (r *RootCmd) workspaceAgent() *serpent.Command { Description: fmt.Sprintf("Block file transfer using known applications: %s.", strings.Join(agentssh.BlockedFileTransferCommands, ",")), Value: serpent.BoolOf(&blockFileTransfer), }, + { + Flag: "devcontainers-enable", + Default: "false", + Env: "CODER_AGENT_DEVCONTAINERS_ENABLE", + Description: "Allow the agent to automatically detect running devcontainers.", + Value: serpent.BoolOf(&devcontainersEnabled), + }, } return cmd diff --git a/cli/clilog/clilog_test.go b/cli/clilog/clilog_test.go index 9069c08aa4a16..c861f65b9131b 100644 --- a/cli/clilog/clilog_test.go +++ b/cli/clilog/clilog_test.go @@ -181,7 +181,7 @@ func assertLogs(t testing.TB, path string, expected ...string) { logs := strings.Split(strings.TrimSpace(string(data)), "\n") if !assert.Len(t, logs, len(expected)) { - t.Logf(string(data)) + t.Log(string(data)) t.FailNow() } for i, log := range logs { @@ -202,7 +202,7 @@ func assertLogsJSON(t testing.TB, path string, levelExpected ...string) { logs := strings.Split(strings.TrimSpace(string(data)), "\n") if !assert.Len(t, logs, len(levelExpected)/2) { - t.Logf(string(data)) + t.Log(string(data)) t.FailNow() } for i, log := range logs { diff --git a/cli/cliui/agent.go b/cli/cliui/agent.go index f2c1378eecb7a..3bb6fee7be769 100644 --- a/cli/cliui/agent.go +++ b/cli/cliui/agent.go @@ -120,7 +120,7 @@ func Agent(ctx context.Context, writer io.Writer, agentID uuid.UUID, opts AgentO if agent.Status == codersdk.WorkspaceAgentTimeout { now := time.Now() sw.Log(now, codersdk.LogLevelInfo, "The workspace agent is having trouble connecting, wait for it to connect or restart your workspace.") - sw.Log(now, codersdk.LogLevelInfo, troubleshootingMessage(agent, fmt.Sprintf("%s/templates#agent-connection-issues", opts.DocsURL))) + sw.Log(now, codersdk.LogLevelInfo, troubleshootingMessage(agent, fmt.Sprintf("%s/admin/templates/troubleshooting#agent-connection-issues", opts.DocsURL))) for agent.Status == codersdk.WorkspaceAgentTimeout { if agent, err = fetch(); err != nil { return xerrors.Errorf("fetch: %w", err) @@ -225,13 +225,13 @@ func Agent(ctx context.Context, writer io.Writer, agentID uuid.UUID, opts AgentO sw.Fail(stage, safeDuration(sw, agent.ReadyAt, agent.StartedAt)) // Use zero time (omitted) to separate these from the startup logs. sw.Log(time.Time{}, codersdk.LogLevelWarn, "Warning: A startup script exited with an error and your workspace may be incomplete.") - sw.Log(time.Time{}, codersdk.LogLevelWarn, troubleshootingMessage(agent, fmt.Sprintf("%s/templates#startup-script-exited-with-an-error", opts.DocsURL))) + sw.Log(time.Time{}, codersdk.LogLevelWarn, troubleshootingMessage(agent, fmt.Sprintf("%s/admin/templates/troubleshooting#startup-script-exited-with-an-error", opts.DocsURL))) default: switch { case agent.LifecycleState.Starting(): // Use zero time (omitted) to separate these from the startup logs. sw.Log(time.Time{}, codersdk.LogLevelWarn, "Notice: The startup scripts are still running and your workspace may be incomplete.") - sw.Log(time.Time{}, codersdk.LogLevelWarn, troubleshootingMessage(agent, fmt.Sprintf("%s/templates#your-workspace-may-be-incomplete", opts.DocsURL))) + sw.Log(time.Time{}, codersdk.LogLevelWarn, troubleshootingMessage(agent, fmt.Sprintf("%s/admin/templates/troubleshooting#your-workspace-may-be-incomplete", opts.DocsURL))) // Note: We don't complete or fail the stage here, it's // intentionally left open to indicate this stage didn't // complete. @@ -253,7 +253,7 @@ func Agent(ctx context.Context, writer io.Writer, agentID uuid.UUID, opts AgentO stage := "The workspace agent lost connection" sw.Start(stage) sw.Log(time.Now(), codersdk.LogLevelWarn, "Wait for it to reconnect or restart your workspace.") - sw.Log(time.Now(), codersdk.LogLevelWarn, troubleshootingMessage(agent, fmt.Sprintf("%s/templates#agent-connection-issues", opts.DocsURL))) + sw.Log(time.Now(), codersdk.LogLevelWarn, troubleshootingMessage(agent, fmt.Sprintf("%s/admin/templates/troubleshooting#agent-connection-issues", opts.DocsURL))) disconnectedAt := agent.DisconnectedAt for agent.Status == codersdk.WorkspaceAgentDisconnected { diff --git a/cli/cliui/output.go b/cli/cliui/output.go index b875e19d154c3..65f6171c2c962 100644 --- a/cli/cliui/output.go +++ b/cli/cliui/output.go @@ -83,6 +83,12 @@ func (f *OutputFormatter) Format(ctx context.Context, data any) (string, error) return "", xerrors.Errorf("unknown output format %q", f.formatID) } +// FormatID will return the ID of the format selected by `--output`. +// If no flag is present, it returns the 'default' formatter. +func (f *OutputFormatter) FormatID() string { + return f.formatID +} + type tableFormat struct { defaultColumns []string allColumns []string diff --git a/cli/cliui/resources.go b/cli/cliui/resources.go index a9204c968c10a..25277645ce96a 100644 --- a/cli/cliui/resources.go +++ b/cli/cliui/resources.go @@ -5,7 +5,9 @@ import ( "io" "sort" "strconv" + "strings" + "github.com/google/uuid" "github.com/jedib0t/go-pretty/v6/table" "golang.org/x/mod/semver" @@ -14,12 +16,19 @@ import ( "github.com/coder/pretty" ) +var ( + pipeMid = "├" + pipeEnd = "└" +) + type WorkspaceResourcesOptions struct { WorkspaceName string HideAgentState bool HideAccess bool Title string ServerVersion string + ListeningPorts map[uuid.UUID]codersdk.WorkspaceAgentListeningPortsResponse + Devcontainers map[uuid.UUID]codersdk.WorkspaceAgentListContainersResponse } // WorkspaceResources displays the connection status and tree-view of provided resources. @@ -86,32 +95,13 @@ func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource }) // Display all agents associated with the resource. for index, agent := range resource.Agents { - pipe := "├" - if index == len(resource.Agents)-1 { - pipe = "└" - } - row := table.Row{ - // These tree from a resource! - fmt.Sprintf("%s─ %s (%s, %s)", pipe, agent.Name, agent.OperatingSystem, agent.Architecture), + tableWriter.AppendRow(renderAgentRow(agent, index, totalAgents, options)) + for _, row := range renderListeningPorts(options, agent.ID, index, totalAgents) { + tableWriter.AppendRow(row) } - if !options.HideAgentState { - var agentStatus, agentHealth, agentVersion string - if !options.HideAgentState { - agentStatus = renderAgentStatus(agent) - agentHealth = renderAgentHealth(agent) - agentVersion = renderAgentVersion(agent.Version, options.ServerVersion) - } - row = append(row, agentStatus, agentHealth, agentVersion) + for _, row := range renderDevcontainers(options, agent.ID, index, totalAgents) { + tableWriter.AppendRow(row) } - if !options.HideAccess { - sshCommand := "coder ssh " + options.WorkspaceName - if totalAgents > 1 { - sshCommand += "." + agent.Name - } - sshCommand = pretty.Sprint(DefaultStyles.Code, sshCommand) - row = append(row, sshCommand) - } - tableWriter.AppendRow(row) } tableWriter.AppendSeparator() } @@ -119,6 +109,102 @@ func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource return err } +func renderAgentRow(agent codersdk.WorkspaceAgent, index, totalAgents int, options WorkspaceResourcesOptions) table.Row { + row := table.Row{ + // These tree from a resource! + fmt.Sprintf("%s─ %s (%s, %s)", renderPipe(index, totalAgents), agent.Name, agent.OperatingSystem, agent.Architecture), + } + if !options.HideAgentState { + var agentStatus, agentHealth, agentVersion string + if !options.HideAgentState { + agentStatus = renderAgentStatus(agent) + agentHealth = renderAgentHealth(agent) + agentVersion = renderAgentVersion(agent.Version, options.ServerVersion) + } + row = append(row, agentStatus, agentHealth, agentVersion) + } + if !options.HideAccess { + sshCommand := "coder ssh " + options.WorkspaceName + if totalAgents > 1 { + sshCommand += "." + agent.Name + } + sshCommand = pretty.Sprint(DefaultStyles.Code, sshCommand) + row = append(row, sshCommand) + } + return row +} + +func renderListeningPorts(wro WorkspaceResourcesOptions, agentID uuid.UUID, idx, total int) []table.Row { + var rows []table.Row + if wro.ListeningPorts == nil { + return []table.Row{} + } + lp, ok := wro.ListeningPorts[agentID] + if !ok || len(lp.Ports) == 0 { + return []table.Row{} + } + rows = append(rows, table.Row{ + fmt.Sprintf(" %s─ Open Ports", renderPipe(idx, total)), + }) + for idx, port := range lp.Ports { + rows = append(rows, renderPortRow(port, idx, len(lp.Ports))) + } + return rows +} + +func renderPortRow(port codersdk.WorkspaceAgentListeningPort, idx, total int) table.Row { + var sb strings.Builder + _, _ = sb.WriteString(" ") + _, _ = sb.WriteString(renderPipe(idx, total)) + _, _ = sb.WriteString("─ ") + _, _ = sb.WriteString(pretty.Sprintf(DefaultStyles.Code, "%5d/%s", port.Port, port.Network)) + if port.ProcessName != "" { + _, _ = sb.WriteString(pretty.Sprintf(DefaultStyles.Keyword, " [%s]", port.ProcessName)) + } + return table.Row{sb.String()} +} + +func renderDevcontainers(wro WorkspaceResourcesOptions, agentID uuid.UUID, index, totalAgents int) []table.Row { + var rows []table.Row + if wro.Devcontainers == nil { + return []table.Row{} + } + dc, ok := wro.Devcontainers[agentID] + if !ok || len(dc.Containers) == 0 { + return []table.Row{} + } + rows = append(rows, table.Row{ + fmt.Sprintf(" %s─ %s", renderPipe(index, totalAgents), "Devcontainers"), + }) + for idx, container := range dc.Containers { + rows = append(rows, renderDevcontainerRow(container, idx, len(dc.Containers))) + } + return rows +} + +func renderDevcontainerRow(container codersdk.WorkspaceAgentDevcontainer, index, total int) table.Row { + var row table.Row + var sb strings.Builder + _, _ = sb.WriteString(" ") + _, _ = sb.WriteString(renderPipe(index, total)) + _, _ = sb.WriteString("─ ") + _, _ = sb.WriteString(pretty.Sprintf(DefaultStyles.Code, "%s", container.FriendlyName)) + row = append(row, sb.String()) + sb.Reset() + if container.Running { + _, _ = sb.WriteString(pretty.Sprintf(DefaultStyles.Keyword, "(%s)", container.Status)) + } else { + _, _ = sb.WriteString(pretty.Sprintf(DefaultStyles.Error, "(%s)", container.Status)) + } + row = append(row, sb.String()) + sb.Reset() + // "health" is not applicable here. + row = append(row, sb.String()) + _, _ = sb.WriteString(container.Image) + row = append(row, sb.String()) + return row +} + func renderAgentStatus(agent codersdk.WorkspaceAgent) string { switch agent.Status { case codersdk.WorkspaceAgentConnecting: @@ -163,3 +249,10 @@ func renderAgentVersion(agentVersion, serverVersion string) string { } return pretty.Sprint(DefaultStyles.Keyword, agentVersion) } + +func renderPipe(idx, total int) string { + if idx == total-1 { + return pipeEnd + } + return pipeMid +} diff --git a/cli/gitauth/vscode.go b/cli/gitauth/vscode.go index ce3c64081bb53..fbd22651929b1 100644 --- a/cli/gitauth/vscode.go +++ b/cli/gitauth/vscode.go @@ -32,6 +32,14 @@ func OverrideVSCodeConfigs(fs afero.Fs) error { filepath.Join(xdg.DataHome, "code-server", "Machine", "settings.json"), // vscode-remote's default configuration path. filepath.Join(home, ".vscode-server", "data", "Machine", "settings.json"), + // vscode-insiders' default configuration path. + filepath.Join(home, ".vscode-insiders-server", "data", "Machine", "settings.json"), + // cursor default configuration path. + filepath.Join(home, ".cursor-server", "data", "Machine", "settings.json"), + // windsurf default configuration path. + filepath.Join(home, ".windsurf-server", "data", "Machine", "settings.json"), + // vscodium default configuration path. + filepath.Join(home, ".vscodium-server", "data", "Machine", "settings.json"), } { _, err := fs.Stat(configPath) if err != nil { diff --git a/cli/gitssh.go b/cli/gitssh.go index f427e43812841..4a83ace678a3b 100644 --- a/cli/gitssh.go +++ b/cli/gitssh.go @@ -91,7 +91,7 @@ func (r *RootCmd) gitssh() *serpent.Command { if xerrors.As(err, &exitErr) && exitErr.ExitCode() == 255 { _, _ = fmt.Fprintln(inv.Stderr, "\n"+pretty.Sprintf( - cliui.DefaultStyles.Wrap, + cliui.DefaultStyles.Wrap, "%s", "Coder authenticates with "+pretty.Sprint(cliui.DefaultStyles.Field, "git")+ " using the public key below. All clones with SSH are authenticated automatically 🪄.")+"\n", ) diff --git a/cli/list.go b/cli/list.go index 1a578c887371b..083d32c6e8fa1 100644 --- a/cli/list.go +++ b/cli/list.go @@ -112,7 +112,7 @@ func (r *RootCmd) list() *serpent.Command { return err } - if len(res) == 0 { + if len(res) == 0 && formatter.FormatID() != cliui.JSONFormat().ID() { pretty.Fprintf(inv.Stderr, cliui.DefaultStyles.Prompt, "No workspaces found! Create one:\n") _, _ = fmt.Fprintln(inv.Stderr) _, _ = fmt.Fprintln(inv.Stderr, " "+pretty.Sprint(cliui.DefaultStyles.Code, "coder create <name>")) diff --git a/cli/list_test.go b/cli/list_test.go index 37f2f36f79278..a70c70babf437 100644 --- a/cli/list_test.go +++ b/cli/list_test.go @@ -74,4 +74,30 @@ func TestList(t *testing.T) { require.NoError(t, json.Unmarshal(out.Bytes(), &workspaces)) require.Len(t, workspaces, 1) }) + + t.Run("NoWorkspacesJSON", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + owner := coderdtest.CreateFirstUser(t, client) + member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) + + inv, root := clitest.New(t, "list", "--output=json") + clitest.SetupConfig(t, member, root) + + ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancelFunc() + + stdout := bytes.NewBuffer(nil) + stderr := bytes.NewBuffer(nil) + inv.Stdout = stdout + inv.Stderr = stderr + err := inv.WithContext(ctx).Run() + require.NoError(t, err) + + var workspaces []codersdk.Workspace + require.NoError(t, json.Unmarshal(stdout.Bytes(), &workspaces)) + require.Len(t, workspaces, 0) + + require.Len(t, stderr.Bytes(), 0) + }) } diff --git a/cli/login.go b/cli/login.go index 591cf66e62418..e7a1d0eb8eb13 100644 --- a/cli/login.go +++ b/cli/login.go @@ -209,7 +209,7 @@ func (r *RootCmd) login() *serpent.Command { // nolint: nestif if !hasFirstUser { - _, _ = fmt.Fprintf(inv.Stdout, Caret+"Your Coder deployment hasn't been set up!\n") + _, _ = fmt.Fprint(inv.Stdout, Caret+"Your Coder deployment hasn't been set up!\n") if username == "" { if !isTTYIn(inv) { diff --git a/cli/logout.go b/cli/logout.go index 290422f492f49..6540003650919 100644 --- a/cli/logout.go +++ b/cli/logout.go @@ -68,7 +68,7 @@ func (r *RootCmd) logout() *serpent.Command { errorString := strings.TrimRight(errorStringBuilder.String(), "\n") return xerrors.New("Failed to log out.\n" + errorString) } - _, _ = fmt.Fprintf(inv.Stdout, Caret+"You are no longer logged in. You can log in using 'coder login <url>'.\n") + _, _ = fmt.Fprint(inv.Stdout, Caret+"You are no longer logged in. You can log in using 'coder login <url>'.\n") return nil }, } diff --git a/cli/notifications.go b/cli/notifications.go index 055a4bfa65e3b..1769ef3aa154a 100644 --- a/cli/notifications.go +++ b/cli/notifications.go @@ -23,6 +23,10 @@ func (r *RootCmd) notifications() *serpent.Command { Description: "Resume Coder notifications", Command: "coder notifications resume", }, + Example{ + Description: "Send a test notification. Administrators can use this to verify the notification target settings.", + Command: "coder notifications test", + }, ), Aliases: []string{"notification"}, Handler: func(inv *serpent.Invocation) error { @@ -31,6 +35,7 @@ func (r *RootCmd) notifications() *serpent.Command { Children: []*serpent.Command{ r.pauseNotifications(), r.resumeNotifications(), + r.testNotifications(), }, } return cmd @@ -83,3 +88,24 @@ func (r *RootCmd) resumeNotifications() *serpent.Command { } return cmd } + +func (r *RootCmd) testNotifications() *serpent.Command { + client := new(codersdk.Client) + cmd := &serpent.Command{ + Use: "test", + Short: "Send a test notification", + Middleware: serpent.Chain( + serpent.RequireNArgs(0), + r.InitClient(client), + ), + Handler: func(inv *serpent.Invocation) error { + if err := client.PostTestNotification(inv.Context()); err != nil { + return xerrors.Errorf("unable to post test notification: %w", err) + } + + _, _ = fmt.Fprintln(inv.Stderr, "A test notification has been sent. If you don't receive the notification, check Coder's logs for any errors.") + return nil + }, + } + return cmd +} diff --git a/cli/notifications_test.go b/cli/notifications_test.go index 9d775c6f5842b..5164657c6c1fb 100644 --- a/cli/notifications_test.go +++ b/cli/notifications_test.go @@ -12,6 +12,8 @@ import ( "github.com/coder/coder/v2/cli/clitest" "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/notifications" + "github.com/coder/coder/v2/coderd/notifications/notificationstest" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/testutil" ) @@ -109,3 +111,59 @@ func TestPauseNotifications_RegularUser(t *testing.T) { require.NoError(t, err) require.False(t, settings.NotifierPaused) // still running } + +func TestNotificationsTest(t *testing.T) { + t.Parallel() + + t.Run("OwnerCanSendTestNotification", func(t *testing.T) { + t.Parallel() + + notifyEnq := ¬ificationstest.FakeEnqueuer{} + + // Given: An owner user. + ownerClient := coderdtest.New(t, &coderdtest.Options{ + DeploymentValues: coderdtest.DeploymentValues(t), + NotificationsEnqueuer: notifyEnq, + }) + _ = coderdtest.CreateFirstUser(t, ownerClient) + + // When: The owner user attempts to send the test notification. + inv, root := clitest.New(t, "notifications", "test") + clitest.SetupConfig(t, ownerClient, root) + + // Then: we expect a notification to be sent. + err := inv.Run() + require.NoError(t, err) + + sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateTestNotification)) + require.Len(t, sent, 1) + }) + + t.Run("MemberCannotSendTestNotification", func(t *testing.T) { + t.Parallel() + + notifyEnq := ¬ificationstest.FakeEnqueuer{} + + // Given: A member user. + ownerClient := coderdtest.New(t, &coderdtest.Options{ + DeploymentValues: coderdtest.DeploymentValues(t), + NotificationsEnqueuer: notifyEnq, + }) + ownerUser := coderdtest.CreateFirstUser(t, ownerClient) + memberClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, ownerUser.OrganizationID) + + // When: The member user attempts to send the test notification. + inv, root := clitest.New(t, "notifications", "test") + clitest.SetupConfig(t, memberClient, root) + + // Then: we expect an error and no notifications to be sent. + err := inv.Run() + var sdkError *codersdk.Error + require.Error(t, err) + require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error") + assert.Equal(t, http.StatusForbidden, sdkError.StatusCode()) + + sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateTestNotification)) + require.Len(t, sent, 0) + }) +} diff --git a/cli/ping.go b/cli/ping.go index 0e219d5762f86..f75ed42d26362 100644 --- a/cli/ping.go +++ b/cli/ping.go @@ -21,13 +21,14 @@ import ( "github.com/coder/pretty" + "github.com/coder/serpent" + "github.com/coder/coder/v2/cli/cliui" "github.com/coder/coder/v2/cli/cliutil" "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/healthsdk" "github.com/coder/coder/v2/codersdk/workspacesdk" - "github.com/coder/serpent" ) type pingSummary struct { @@ -86,6 +87,8 @@ func (r *RootCmd) ping() *serpent.Command { pingNum int64 pingTimeout time.Duration pingWait time.Duration + pingTimeLocal bool + pingTimeUTC bool appearanceConfig codersdk.AppearanceConfig ) @@ -159,7 +162,7 @@ func (r *RootCmd) ping() *serpent.Command { LocalNetInfo: ni, Verbose: r.verbose, PingP2P: didP2p, - TroubleshootingURL: appearanceConfig.DocsURL + "/networking/troubleshooting", + TroubleshootingURL: appearanceConfig.DocsURL + "/admin/networking/troubleshooting", } awsRanges, err := cliutil.FetchAWSIPRanges(diagCtx, cliutil.AWSIPRangesURL) @@ -217,6 +220,10 @@ func (r *RootCmd) ping() *serpent.Command { ctx, cancel := context.WithTimeout(ctx, pingTimeout) dur, p2p, pong, err = conn.Ping(ctx) + pongTime := time.Now() + if pingTimeUTC { + pongTime = pongTime.UTC() + } cancel() results.addResult(pong) if err != nil { @@ -268,7 +275,13 @@ func (r *RootCmd) ping() *serpent.Command { ) } - _, _ = fmt.Fprintf(inv.Stdout, "pong from %s %s in %s\n", + var displayTime string + if pingTimeLocal || pingTimeUTC { + displayTime = pretty.Sprintf(cliui.DefaultStyles.DateTimeStamp, "[%s] ", pongTime.Format(time.RFC3339)) + } + + _, _ = fmt.Fprintf(inv.Stdout, "%spong from %s %s in %s\n", + displayTime, pretty.Sprint(cliui.DefaultStyles.Keyword, workspaceName), via, pretty.Sprint(cliui.DefaultStyles.DateTimeStamp, dur.String()), @@ -321,6 +334,16 @@ func (r *RootCmd) ping() *serpent.Command { Description: "Specifies the number of pings to perform. By default, pings will continue until interrupted.", Value: serpent.Int64Of(&pingNum), }, + { + Flag: "time", + Description: "Show the response time of each pong in local time.", + Value: serpent.BoolOf(&pingTimeLocal), + }, + { + Flag: "utc", + Description: "Show the response time of each pong in UTC (implies --time).", + Value: serpent.BoolOf(&pingTimeUTC), + }, } return cmd } diff --git a/cli/ping_test.go b/cli/ping_test.go index bc0bb7c0e423a..ffdcee07f07de 100644 --- a/cli/ping_test.go +++ b/cli/ping_test.go @@ -69,4 +69,60 @@ func TestPing(t *testing.T) { cancel() <-cmdDone }) + + t.Run("1PingWithTime", func(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + utc bool + }{ + {name: "LocalTime"}, // --time renders the pong response time. + {name: "UTC", utc: true}, // --utc implies --time, so we expect it to also contain the pong time. + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + client, workspace, agentToken := setupWorkspaceForAgent(t) + args := []string{"ping", "-n", "1", workspace.Name, "--time"} + if tc.utc { + args = append(args, "--utc") + } + + inv, root := clitest.New(t, args...) + clitest.SetupConfig(t, client, root) + pty := ptytest.New(t) + inv.Stdin = pty.Input() + inv.Stderr = pty.Output() + inv.Stdout = pty.Output() + + _ = agenttest.New(t, client.URL, agentToken) + _ = coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + cmdDone := tGo(t, func() { + err := inv.WithContext(ctx).Run() + assert.NoError(t, err) + }) + + // RFC3339 is the format used to render the pong times. + rfc3339 := `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?` + + // Validate that dates are rendered as specified. + if tc.utc { + rfc3339 += `Z` + } else { + rfc3339 += `(?:Z|[+-]\d{2}:\d{2})` + } + + pty.ExpectRegexMatch(`\[` + rfc3339 + `\] pong from ` + workspace.Name) + cancel() + <-cmdDone + }) + } + }) } diff --git a/cli/provisioners_test.go b/cli/provisioners_test.go index 760c7f5a6ccce..30a89714ff57f 100644 --- a/cli/provisioners_test.go +++ b/cli/provisioners_test.go @@ -71,7 +71,7 @@ func TestProvisioners_Golden(t *testing.T) { }) owner := coderdtest.CreateFirstUser(t, client) templateAdminClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.ScopedRoleOrgTemplateAdmin(owner.OrganizationID)) - memberClient, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) + _, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) // Create initial resources with a running provisioner. firstProvisioner := coderdtest.NewTaggedProvisionerDaemon(t, coderdAPI, "default-provisioner", map[string]string{"owner": "", "scope": "organization"}) @@ -95,7 +95,7 @@ func TestProvisioners_Golden(t *testing.T) { Name: "provisioner-1", CreatedAt: dbtime.Now().Add(1 * time.Second), LastSeenAt: sql.NullTime{Time: coderdAPI.Clock.Now().Add(time.Hour), Valid: true}, // Stale interval can't be adjusted, keep online. - KeyID: uuid.MustParse(codersdk.ProvisionerKeyIDBuiltIn), + KeyID: codersdk.ProvisionerKeyUUIDBuiltIn, Tags: database.StringMap{"owner": "", "scope": "organization", "foo": "bar"}, }) w1 := dbgen.Workspace(t, coderdAPI.Database, database.WorkspaceTable{ @@ -122,7 +122,7 @@ func TestProvisioners_Golden(t *testing.T) { Name: "provisioner-2", CreatedAt: dbtime.Now().Add(2 * time.Second), LastSeenAt: sql.NullTime{Time: coderdAPI.Clock.Now().Add(-time.Hour), Valid: true}, - KeyID: uuid.MustParse(codersdk.ProvisionerKeyIDBuiltIn), + KeyID: codersdk.ProvisionerKeyUUIDBuiltIn, Tags: database.StringMap{"owner": "", "scope": "organization"}, }) w2 := dbgen.Workspace(t, coderdAPI.Database, database.WorkspaceTable{ @@ -168,7 +168,7 @@ func TestProvisioners_Golden(t *testing.T) { Name: "provisioner-3", CreatedAt: dbtime.Now().Add(3 * time.Second), LastSeenAt: sql.NullTime{Time: coderdAPI.Clock.Now().Add(time.Hour), Valid: true}, // Stale interval can't be adjusted, keep online. - KeyID: uuid.MustParse(codersdk.ProvisionerKeyIDBuiltIn), + KeyID: codersdk.ProvisionerKeyUUIDBuiltIn, Tags: database.StringMap{"owner": "", "scope": "organization"}, }) @@ -178,8 +178,9 @@ func TestProvisioners_Golden(t *testing.T) { t.Logf("replace[%q] = %q", id, replaceID) } - // Test provisioners list with member as members can access - // provisioner daemons. + // Test provisioners list with template admin as members are currently + // unable to access provisioner jobs. In the future (with RBAC + // changes), we may allow them to view _their_ jobs. t.Run("list", func(t *testing.T) { t.Parallel() @@ -190,7 +191,7 @@ func TestProvisioners_Golden(t *testing.T) { "--column", "id,created at,last seen at,name,version,tags,key name,status,current job id,current job status,previous job id,previous job status,organization", ) inv.Stdout = &got - clitest.SetupConfig(t, memberClient, root) + clitest.SetupConfig(t, templateAdminClient, root) err := inv.Run() require.NoError(t, err) diff --git a/cli/publickey.go b/cli/publickey.go index 03f17a4bc4952..320ed86b2c697 100644 --- a/cli/publickey.go +++ b/cli/publickey.go @@ -45,14 +45,14 @@ func (r *RootCmd) publickey() *serpent.Command { return xerrors.Errorf("create codersdk client: %w", err) } - cliui.Infof(inv.Stdout, + cliui.Info(inv.Stdout, "This is your public key for using "+pretty.Sprint(cliui.DefaultStyles.Field, "git")+" in "+ "Coder. All clones with SSH will be authenticated automatically 🪄.", ) - cliui.Infof(inv.Stdout, pretty.Sprint(cliui.DefaultStyles.Code, strings.TrimSpace(key.PublicKey))+"\n") - cliui.Infof(inv.Stdout, "Add to GitHub and GitLab:") - cliui.Infof(inv.Stdout, "> https://github.com/settings/ssh/new") - cliui.Infof(inv.Stdout, "> https://gitlab.com/-/profile/keys") + cliui.Info(inv.Stdout, pretty.Sprint(cliui.DefaultStyles.Code, strings.TrimSpace(key.PublicKey))+"\n") + cliui.Info(inv.Stdout, "Add to GitHub and GitLab:") + cliui.Info(inv.Stdout, "> https://github.com/settings/ssh/new") + cliui.Info(inv.Stdout, "> https://gitlab.com/-/profile/keys") return nil }, diff --git a/cli/root.go b/cli/root.go index 778cf2c24215f..09044ad3e28ca 100644 --- a/cli/root.go +++ b/cli/root.go @@ -1213,9 +1213,14 @@ func wrapTransportWithVersionMismatchCheck(rt http.RoundTripper, inv *serpent.In return } upgradeMessage := defaultUpgradeMessage(semver.Canonical(serverVersion)) - serverInfo, err := getBuildInfo(inv.Context()) - if err == nil && serverInfo.UpgradeMessage != "" { - upgradeMessage = serverInfo.UpgradeMessage + if serverInfo, err := getBuildInfo(inv.Context()); err == nil { + switch { + case serverInfo.UpgradeMessage != "": + upgradeMessage = serverInfo.UpgradeMessage + // The site-local `install.sh` was introduced in v2.19.0 + case serverInfo.DashboardURL != "" && semver.Compare(semver.MajorMinor(serverVersion), "v2.19") >= 0: + upgradeMessage = fmt.Sprintf("download %s with: 'curl -fsSL %s/install.sh | sh'", serverVersion, serverInfo.DashboardURL) + } } fmtWarningText := "version mismatch: client %s, server %s\n%s" fmtWarn := pretty.Sprint(cliui.DefaultStyles.Warn, fmtWarningText) diff --git a/cli/server.go b/cli/server.go index 48f049c163b3b..745794a236200 100644 --- a/cli/server.go +++ b/cli/server.go @@ -172,6 +172,17 @@ func createOIDCConfig(ctx context.Context, logger slog.Logger, vals *codersdk.De groupAllowList[group] = true } + secondaryClaimsSrc := coderd.MergedClaimsSourceUserInfo + if !vals.OIDC.IgnoreUserInfo && vals.OIDC.UserInfoFromAccessToken { + return nil, xerrors.Errorf("to use 'oidc-access-token-claims', 'oidc-ignore-userinfo' must be set to 'false'") + } + if vals.OIDC.IgnoreUserInfo { + secondaryClaimsSrc = coderd.MergedClaimsSourceNone + } + if vals.OIDC.UserInfoFromAccessToken { + secondaryClaimsSrc = coderd.MergedClaimsSourceAccessToken + } + return &coderd.OIDCConfig{ OAuth2Config: useCfg, Provider: oidcProvider, @@ -187,7 +198,7 @@ func createOIDCConfig(ctx context.Context, logger slog.Logger, vals *codersdk.De NameField: vals.OIDC.NameField.String(), EmailField: vals.OIDC.EmailField.String(), AuthURLParams: vals.OIDC.AuthURLParams.Value, - IgnoreUserInfo: vals.OIDC.IgnoreUserInfo.Value(), + SecondaryClaims: secondaryClaimsSrc, SignInText: vals.OIDC.SignInText.String(), SignupsDisabledText: vals.OIDC.SignupsDisabledText.String(), IconURL: vals.OIDC.IconURL.String(), @@ -513,7 +524,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. } accessURL := vals.AccessURL.String() - cliui.Infof(inv.Stdout, lipgloss.NewStyle(). + cliui.Info(inv.Stdout, lipgloss.NewStyle(). Border(lipgloss.DoubleBorder()). Align(lipgloss.Center). Padding(0, 3). @@ -677,24 +688,12 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. } } - if vals.OAuth2.Github.ClientSecret != "" { - options.GithubOAuth2Config, err = configureGithubOAuth2( - oauthInstrument, - vals.AccessURL.Value(), - vals.OAuth2.Github.ClientID.String(), - vals.OAuth2.Github.ClientSecret.String(), - vals.OAuth2.Github.AllowSignups.Value(), - vals.OAuth2.Github.AllowEveryone.Value(), - vals.OAuth2.Github.AllowedOrgs, - vals.OAuth2.Github.AllowedTeams, - vals.OAuth2.Github.EnterpriseBaseURL.String(), - ) - if err != nil { - return xerrors.Errorf("configure github oauth2: %w", err) - } - } - - if vals.OIDC.ClientKeyFile != "" || vals.OIDC.ClientSecret != "" { + // As OIDC clients can be confidential or public, + // we should only check for a client id being set. + // The underlying library handles the case of no + // client secrets correctly. For more details on + // client types: https://oauth.net/2/client-types/ + if vals.OIDC.ClientID != "" { if vals.OIDC.IgnoreEmailVerified { logger.Warn(ctx, "coder will not check email_verified for OIDC logins") } @@ -776,45 +775,61 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. return xerrors.Errorf("set deployment id: %w", err) } + githubOAuth2ConfigParams, err := getGithubOAuth2ConfigParams(ctx, options.Database, vals) + if err != nil { + return xerrors.Errorf("get github oauth2 config params: %w", err) + } + if githubOAuth2ConfigParams != nil { + options.GithubOAuth2Config, err = configureGithubOAuth2( + oauthInstrument, + githubOAuth2ConfigParams, + ) + if err != nil { + return xerrors.Errorf("configure github oauth2: %w", err) + } + } + options.RuntimeConfig = runtimeconfig.NewManager() // This should be output before the logs start streaming. cliui.Infof(inv.Stdout, "\n==> Logs will stream in below (press ctrl+c to gracefully exit):") - if vals.Telemetry.Enable { - vals, err := vals.WithoutSecrets() - if err != nil { - return xerrors.Errorf("remove secrets from deployment values: %w", err) - } - options.Telemetry, err = telemetry.New(telemetry.Options{ - BuiltinPostgres: builtinPostgres, - DeploymentID: deploymentID, - Database: options.Database, - Logger: logger.Named("telemetry"), - URL: vals.Telemetry.URL.Value(), - Tunnel: tunnel != nil, - DeploymentConfig: vals, - ParseLicenseJWT: func(lic *telemetry.License) error { - // This will be nil when running in AGPL-only mode. - if options.ParseLicenseClaims == nil { - return nil - } - - email, trial, err := options.ParseLicenseClaims(lic.JWT) - if err != nil { - return err - } - if email != "" { - lic.Email = &email - } - lic.Trial = &trial + deploymentConfigWithoutSecrets, err := vals.WithoutSecrets() + if err != nil { + return xerrors.Errorf("remove secrets from deployment values: %w", err) + } + telemetryReporter, err := telemetry.New(telemetry.Options{ + Disabled: !vals.Telemetry.Enable.Value(), + BuiltinPostgres: builtinPostgres, + DeploymentID: deploymentID, + Database: options.Database, + Logger: logger.Named("telemetry"), + URL: vals.Telemetry.URL.Value(), + Tunnel: tunnel != nil, + DeploymentConfig: deploymentConfigWithoutSecrets, + ParseLicenseJWT: func(lic *telemetry.License) error { + // This will be nil when running in AGPL-only mode. + if options.ParseLicenseClaims == nil { return nil - }, - }) - if err != nil { - return xerrors.Errorf("create telemetry reporter: %w", err) - } - defer options.Telemetry.Close() + } + + email, trial, err := options.ParseLicenseClaims(lic.JWT) + if err != nil { + return err + } + if email != "" { + lic.Email = &email + } + lic.Trial = &trial + return nil + }, + }) + if err != nil { + return xerrors.Errorf("create telemetry reporter: %w", err) + } + defer telemetryReporter.Close() + if vals.Telemetry.Enable.Value() { + options.Telemetry = telemetryReporter } else { logger.Warn(ctx, fmt.Sprintf(`telemetry disabled, unable to notify of security issues. Read more: %s/admin/setup/telemetry`, vals.DocsURL.String())) } @@ -931,7 +946,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. notificationReportGenerator := reports.NewReportGenerator(ctx, logger.Named("notifications.report_generator"), options.Database, options.NotificationsEnqueuer, quartz.NewReal()) defer notificationReportGenerator.Close() } else { - cliui.Info(inv.Stdout, "Notifications are currently disabled as there are no configured delivery methods. See https://coder.com/docs/admin/monitoring/notifications#delivery-methods for more details.") + logger.Debug(ctx, "notifications are currently disabled as there are no configured delivery methods. See https://coder.com/docs/admin/monitoring/notifications#delivery-methods for more details") } // Since errCh only has one buffered slot, all routines @@ -1824,23 +1839,103 @@ func configureCAPool(tlsClientCAFile string, tlsConfig *tls.Config) error { return nil } +const ( + // Client ID for https://github.com/apps/coder + GithubOAuth2DefaultProviderClientID = "Iv1.6a2b4b4aec4f4fe7" + GithubOAuth2DefaultProviderAllowEveryone = true + GithubOAuth2DefaultProviderDeviceFlow = true +) + +type githubOAuth2ConfigParams struct { + accessURL *url.URL + clientID string + clientSecret string + deviceFlow bool + allowSignups bool + allowEveryone bool + allowOrgs []string + rawTeams []string + enterpriseBaseURL string +} + +func getGithubOAuth2ConfigParams(ctx context.Context, db database.Store, vals *codersdk.DeploymentValues) (*githubOAuth2ConfigParams, error) { + params := githubOAuth2ConfigParams{ + accessURL: vals.AccessURL.Value(), + clientID: vals.OAuth2.Github.ClientID.String(), + clientSecret: vals.OAuth2.Github.ClientSecret.String(), + deviceFlow: vals.OAuth2.Github.DeviceFlow.Value(), + allowSignups: vals.OAuth2.Github.AllowSignups.Value(), + allowEveryone: vals.OAuth2.Github.AllowEveryone.Value(), + allowOrgs: vals.OAuth2.Github.AllowedOrgs.Value(), + rawTeams: vals.OAuth2.Github.AllowedTeams.Value(), + enterpriseBaseURL: vals.OAuth2.Github.EnterpriseBaseURL.String(), + } + + // If the user manually configured the GitHub OAuth2 provider, + // we won't add the default configuration. + if params.clientID != "" || params.clientSecret != "" || params.enterpriseBaseURL != "" { + return ¶ms, nil + } + + // Check if the user manually disabled the default GitHub OAuth2 provider. + if !vals.OAuth2.Github.DefaultProviderEnable.Value() { + return nil, nil //nolint:nilnil + } + + // Check if the deployment is eligible for the default GitHub OAuth2 provider. + // We want to enable it only for new deployments, and avoid enabling it + // if a deployment was upgraded from an older version. + // nolint:gocritic // Requires system privileges + defaultEligible, err := db.GetOAuth2GithubDefaultEligible(dbauthz.AsSystemRestricted(ctx)) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return nil, xerrors.Errorf("get github default eligible: %w", err) + } + defaultEligibleNotSet := errors.Is(err, sql.ErrNoRows) + + if defaultEligibleNotSet { + // nolint:gocritic // User count requires system privileges + userCount, err := db.GetUserCount(dbauthz.AsSystemRestricted(ctx)) + if err != nil { + return nil, xerrors.Errorf("get user count: %w", err) + } + // We check if a deployment is new by checking if it has any users. + defaultEligible = userCount == 0 + // nolint:gocritic // Requires system privileges + if err := db.UpsertOAuth2GithubDefaultEligible(dbauthz.AsSystemRestricted(ctx), defaultEligible); err != nil { + return nil, xerrors.Errorf("upsert github default eligible: %w", err) + } + } + + if !defaultEligible { + return nil, nil //nolint:nilnil + } + + params.clientID = GithubOAuth2DefaultProviderClientID + params.deviceFlow = GithubOAuth2DefaultProviderDeviceFlow + if len(params.allowOrgs) == 0 { + params.allowEveryone = GithubOAuth2DefaultProviderAllowEveryone + } + + return ¶ms, nil +} + //nolint:revive // Ignore flag-parameter: parameter 'allowEveryone' seems to be a control flag, avoid control coupling (revive) -func configureGithubOAuth2(instrument *promoauth.Factory, accessURL *url.URL, clientID, clientSecret string, allowSignups, allowEveryone bool, allowOrgs []string, rawTeams []string, enterpriseBaseURL string) (*coderd.GithubOAuth2Config, error) { - redirectURL, err := accessURL.Parse("/api/v2/users/oauth2/github/callback") +func configureGithubOAuth2(instrument *promoauth.Factory, params *githubOAuth2ConfigParams) (*coderd.GithubOAuth2Config, error) { + redirectURL, err := params.accessURL.Parse("/api/v2/users/oauth2/github/callback") if err != nil { return nil, xerrors.Errorf("parse github oauth callback url: %w", err) } - if allowEveryone && len(allowOrgs) > 0 { + if params.allowEveryone && len(params.allowOrgs) > 0 { return nil, xerrors.New("allow everyone and allowed orgs cannot be used together") } - if allowEveryone && len(rawTeams) > 0 { + if params.allowEveryone && len(params.rawTeams) > 0 { return nil, xerrors.New("allow everyone and allowed teams cannot be used together") } - if !allowEveryone && len(allowOrgs) == 0 { + if !params.allowEveryone && len(params.allowOrgs) == 0 { return nil, xerrors.New("allowed orgs is empty: must specify at least one org or allow everyone") } - allowTeams := make([]coderd.GithubOAuth2Team, 0, len(rawTeams)) - for _, rawTeam := range rawTeams { + allowTeams := make([]coderd.GithubOAuth2Team, 0, len(params.rawTeams)) + for _, rawTeam := range params.rawTeams { parts := strings.SplitN(rawTeam, "/", 2) if len(parts) != 2 { return nil, xerrors.Errorf("github team allowlist is formatted incorrectly. got %s; wanted <organization>/<team>", rawTeam) @@ -1852,8 +1947,8 @@ func configureGithubOAuth2(instrument *promoauth.Factory, accessURL *url.URL, cl } endpoint := xgithub.Endpoint - if enterpriseBaseURL != "" { - enterpriseURL, err := url.Parse(enterpriseBaseURL) + if params.enterpriseBaseURL != "" { + enterpriseURL, err := url.Parse(params.enterpriseBaseURL) if err != nil { return nil, xerrors.Errorf("parse enterprise base url: %w", err) } @@ -1872,8 +1967,8 @@ func configureGithubOAuth2(instrument *promoauth.Factory, accessURL *url.URL, cl } instrumentedOauth := instrument.NewGithub("github-login", &oauth2.Config{ - ClientID: clientID, - ClientSecret: clientSecret, + ClientID: params.clientID, + ClientSecret: params.clientSecret, Endpoint: endpoint, RedirectURL: redirectURL.String(), Scopes: []string{ @@ -1885,17 +1980,28 @@ func configureGithubOAuth2(instrument *promoauth.Factory, accessURL *url.URL, cl createClient := func(client *http.Client, source promoauth.Oauth2Source) (*github.Client, error) { client = instrumentedOauth.InstrumentHTTPClient(client, source) - if enterpriseBaseURL != "" { - return github.NewEnterpriseClient(enterpriseBaseURL, "", client) + if params.enterpriseBaseURL != "" { + return github.NewEnterpriseClient(params.enterpriseBaseURL, "", client) } return github.NewClient(client), nil } + var deviceAuth *externalauth.DeviceAuth + if params.deviceFlow { + deviceAuth = &externalauth.DeviceAuth{ + Config: instrumentedOauth, + ClientID: params.clientID, + TokenURL: endpoint.TokenURL, + Scopes: []string{"read:user", "read:org", "user:email"}, + CodeURL: endpoint.DeviceAuthURL, + } + } + return &coderd.GithubOAuth2Config{ OAuth2Config: instrumentedOauth, - AllowSignups: allowSignups, - AllowEveryone: allowEveryone, - AllowOrganizations: allowOrgs, + AllowSignups: params.allowSignups, + AllowEveryone: params.allowEveryone, + AllowOrganizations: params.allowOrgs, AllowTeams: allowTeams, AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) { api, err := createClient(client, promoauth.SourceGitAPIAuthUser) @@ -1934,6 +2040,20 @@ func configureGithubOAuth2(instrument *promoauth.Factory, accessURL *url.URL, cl team, _, err := api.Teams.GetTeamMembershipBySlug(ctx, org, teamSlug, username) return team, err }, + DeviceFlowEnabled: params.deviceFlow, + ExchangeDeviceCode: func(ctx context.Context, deviceCode string) (*oauth2.Token, error) { + if !params.deviceFlow { + return nil, xerrors.New("device flow is not enabled") + } + return deviceAuth.ExchangeDeviceCode(ctx, deviceCode) + }, + AuthorizeDevice: func(ctx context.Context) (*codersdk.ExternalAuthDevice, error) { + if !params.deviceFlow { + return nil, xerrors.New("device flow is not enabled") + } + return deviceAuth.AuthorizeDevice(ctx) + }, + DefaultProviderConfigured: params.clientID == GithubOAuth2DefaultProviderClientID, }, nil } @@ -2015,6 +2135,7 @@ func startBuiltinPostgres(ctx context.Context, cfg config.Root, logger slog.Logg Username("coder"). Password(pgPassword). Database("coder"). + Encoding("UTF8"). Port(uint32(pgPort)). Logger(stdlibLogger.Writer()), ) @@ -2558,6 +2679,8 @@ func parseExternalAuthProvidersFromEnv(prefix string, environ []string) ([]coder return providers, nil } +var reInvalidPortAfterHost = regexp.MustCompile(`invalid port ".+" after host`) + // If the user provides a postgres URL with a password that contains special // characters, the URL will be invalid. We need to escape the password so that // the URL parse doesn't fail at the DB connector level. @@ -2566,7 +2689,11 @@ func escapePostgresURLUserInfo(v string) (string, error) { // I wish I could use errors.Is here, but this error is not declared as a // variable in net/url. :( if err != nil { - if strings.Contains(err.Error(), "net/url: invalid userinfo") { + // Warning: The parser may also fail with an "invalid port" error if the password contains special + // characters. It does not detect invalid user information but instead incorrectly reports an invalid port. + // + // See: https://github.com/coder/coder/issues/16319 + if strings.Contains(err.Error(), "net/url: invalid userinfo") || reInvalidPortAfterHost.MatchString(err.Error()) { // If the URL is invalid, we assume it is because the password contains // special characters that need to be escaped. diff --git a/cli/server_internal_test.go b/cli/server_internal_test.go index 4bdf54f4f0583..b5417ceb04b8e 100644 --- a/cli/server_internal_test.go +++ b/cli/server_internal_test.go @@ -351,13 +351,23 @@ func TestEscapePostgresURLUserInfo(t *testing.T) { output: "", err: xerrors.New("parse postgres url: parse \"postgres://local host:5432/coder\": invalid character \" \" in host name"), }, + { + input: "postgres://coder:co?der@localhost:5432/coder", + output: "postgres://coder:co%3Fder@localhost:5432/coder", + err: nil, + }, + { + input: "postgres://coder:co#der@localhost:5432/coder", + output: "postgres://coder:co%23der@localhost:5432/coder", + err: nil, + }, } for _, tc := range testcases { tc := tc t.Run(tc.input, func(t *testing.T) { t.Parallel() o, err := escapePostgresURLUserInfo(tc.input) - require.Equal(t, tc.output, o) + assert.Equal(t, tc.output, o) if tc.err != nil { require.Error(t, err) require.EqualValues(t, tc.err.Error(), err.Error()) diff --git a/cli/server_test.go b/cli/server_test.go index fa96e192f7eb3..64ad535ea34f3 100644 --- a/cli/server_test.go +++ b/cli/server_test.go @@ -25,6 +25,7 @@ import ( "runtime" "strconv" "strings" + "sync" "sync/atomic" "testing" "time" @@ -39,10 +40,13 @@ import ( "tailscale.com/types/key" "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/buildinfo" "github.com/coder/coder/v2/cli" "github.com/coder/coder/v2/cli/clitest" "github.com/coder/coder/v2/cli/config" "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbgen" "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/coderd/database/migrations" "github.com/coder/coder/v2/coderd/httpapi" @@ -239,6 +243,218 @@ func TestServer(t *testing.T) { t.Fatalf("expected postgres URL to start with \"postgres://\", got %q", got) } }) + t.Run("SpammyLogs", func(t *testing.T) { + // The purpose of this test is to ensure we don't show excessive logs when the server starts. + t.Parallel() + inv, cfg := clitest.New(t, + "server", + "--in-memory", + "--http-address", ":0", + "--access-url", "http://localhost:3000/", + "--cache-dir", t.TempDir(), + ) + stdoutRW := syncReaderWriter{} + stderrRW := syncReaderWriter{} + inv.Stdout = io.MultiWriter(os.Stdout, &stdoutRW) + inv.Stderr = io.MultiWriter(os.Stderr, &stderrRW) + clitest.Start(t, inv) + + // Wait for startup + _ = waitAccessURL(t, cfg) + + // Wait a bit for more logs to be printed. + time.Sleep(testutil.WaitShort) + + // Lines containing these strings are printed because we're + // running the server with a test config. They wouldn't be + // normally shown to the user, so we'll ignore them. + ignoreLines := []string{ + "isn't externally reachable", + "install.sh will be unavailable", + "telemetry disabled, unable to notify of security issues", + } + + countLines := func(fullOutput string) int { + terminalWidth := 80 + linesByNewline := strings.Split(fullOutput, "\n") + countByWidth := 0 + lineLoop: + for _, line := range linesByNewline { + for _, ignoreLine := range ignoreLines { + if strings.Contains(line, ignoreLine) { + continue lineLoop + } + } + if line == "" { + // Empty lines take up one line. + countByWidth++ + } else { + countByWidth += (len(line) + terminalWidth - 1) / terminalWidth + } + } + return countByWidth + } + + stdout, err := io.ReadAll(&stdoutRW) + if err != nil { + t.Fatalf("failed to read stdout: %v", err) + } + stderr, err := io.ReadAll(&stderrRW) + if err != nil { + t.Fatalf("failed to read stderr: %v", err) + } + + numLines := countLines(string(stdout)) + countLines(string(stderr)) + require.Less(t, numLines, 20) + }) + + t.Run("OAuth2GitHubDefaultProvider", func(t *testing.T) { + type testCase struct { + name string + githubDefaultProviderEnabled string + githubClientID string + githubClientSecret string + allowedOrg string + expectGithubEnabled bool + expectGithubDefaultProviderConfigured bool + createUserPreStart bool + createUserPostRestart bool + } + + runGitHubProviderTest := func(t *testing.T, tc testCase) { + t.Parallel() + if !dbtestutil.WillUsePostgres() { + t.Skip("test requires postgres") + } + + ctx, cancelFunc := context.WithCancel(testutil.Context(t, testutil.WaitLong)) + defer cancelFunc() + + dbURL, err := dbtestutil.Open(t) + require.NoError(t, err) + db, _ := dbtestutil.NewDB(t, dbtestutil.WithURL(dbURL)) + + if tc.createUserPreStart { + _ = dbgen.User(t, db, database.User{}) + } + + args := []string{ + "server", + "--postgres-url", dbURL, + "--http-address", ":0", + "--access-url", "https://example.com", + } + if tc.githubClientID != "" { + args = append(args, fmt.Sprintf("--oauth2-github-client-id=%s", tc.githubClientID)) + } + if tc.githubClientSecret != "" { + args = append(args, fmt.Sprintf("--oauth2-github-client-secret=%s", tc.githubClientSecret)) + } + if tc.githubClientID != "" || tc.githubClientSecret != "" { + args = append(args, "--oauth2-github-allow-everyone") + } + if tc.githubDefaultProviderEnabled != "" { + args = append(args, fmt.Sprintf("--oauth2-github-default-provider-enable=%s", tc.githubDefaultProviderEnabled)) + } + if tc.allowedOrg != "" { + args = append(args, fmt.Sprintf("--oauth2-github-allowed-orgs=%s", tc.allowedOrg)) + } + inv, cfg := clitest.New(t, args...) + errChan := make(chan error, 1) + go func() { + errChan <- inv.WithContext(ctx).Run() + }() + accessURLChan := make(chan *url.URL, 1) + go func() { + accessURLChan <- waitAccessURL(t, cfg) + }() + + var accessURL *url.URL + select { + case err := <-errChan: + require.NoError(t, err) + case accessURL = <-accessURLChan: + require.NotNil(t, accessURL) + } + + client := codersdk.New(accessURL) + + authMethods, err := client.AuthMethods(ctx) + require.NoError(t, err) + require.Equal(t, tc.expectGithubEnabled, authMethods.Github.Enabled) + require.Equal(t, tc.expectGithubDefaultProviderConfigured, authMethods.Github.DefaultProviderConfigured) + + cancelFunc() + select { + case err := <-errChan: + require.NoError(t, err) + case <-time.After(testutil.WaitLong): + t.Fatal("server did not exit") + } + + if tc.createUserPostRestart { + _ = dbgen.User(t, db, database.User{}) + } + + // Ensure that it stays at that setting after the server restarts. + inv, cfg = clitest.New(t, args...) + clitest.Start(t, inv) + accessURL = waitAccessURL(t, cfg) + client = codersdk.New(accessURL) + + ctx = testutil.Context(t, testutil.WaitLong) + authMethods, err = client.AuthMethods(ctx) + require.NoError(t, err) + require.Equal(t, tc.expectGithubEnabled, authMethods.Github.Enabled) + require.Equal(t, tc.expectGithubDefaultProviderConfigured, authMethods.Github.DefaultProviderConfigured) + } + + for _, tc := range []testCase{ + { + name: "NewDeployment", + expectGithubEnabled: true, + expectGithubDefaultProviderConfigured: true, + createUserPreStart: false, + createUserPostRestart: true, + }, + { + name: "ExistingDeployment", + expectGithubEnabled: false, + expectGithubDefaultProviderConfigured: false, + createUserPreStart: true, + createUserPostRestart: false, + }, + { + name: "ManuallyDisabled", + githubDefaultProviderEnabled: "false", + expectGithubEnabled: false, + expectGithubDefaultProviderConfigured: false, + }, + { + name: "ConfiguredClientID", + githubClientID: "123", + expectGithubEnabled: true, + expectGithubDefaultProviderConfigured: false, + }, + { + name: "ConfiguredClientSecret", + githubClientSecret: "456", + expectGithubEnabled: true, + expectGithubDefaultProviderConfigured: false, + }, + { + name: "AllowedOrg", + allowedOrg: "coder", + expectGithubEnabled: true, + expectGithubDefaultProviderConfigured: true, + }, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + runGitHubProviderTest(t, tc) + }) + } + }) // Validate that a warning is printed that it may not be externally // reachable. @@ -947,36 +1163,40 @@ func TestServer(t *testing.T) { t.Run("Telemetry", func(t *testing.T) { t.Parallel() - deployment := make(chan struct{}, 64) - snapshot := make(chan *telemetry.Snapshot, 64) - r := chi.NewRouter() - r.Post("/deployment", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusAccepted) - deployment <- struct{}{} - }) - r.Post("/snapshot", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusAccepted) - ss := &telemetry.Snapshot{} - err := json.NewDecoder(r.Body).Decode(ss) - require.NoError(t, err) - snapshot <- ss - }) - server := httptest.NewServer(r) - defer server.Close() + telemetryServerURL, deployment, snapshot := mockTelemetryServer(t) - inv, _ := clitest.New(t, + inv, cfg := clitest.New(t, "server", "--in-memory", "--http-address", ":0", "--access-url", "http://example.com", "--telemetry", - "--telemetry-url", server.URL, + "--telemetry-url", telemetryServerURL.String(), "--cache-dir", t.TempDir(), ) clitest.Start(t, inv) <-deployment <-snapshot + + accessURL := waitAccessURL(t, cfg) + + ctx := testutil.Context(t, testutil.WaitMedium) + client := codersdk.New(accessURL) + body, err := client.Request(ctx, http.MethodGet, "/", nil) + require.NoError(t, err) + require.NoError(t, body.Body.Close()) + + require.Eventually(t, func() bool { + snap := <-snapshot + htmlFirstServedFound := false + for _, item := range snap.TelemetryItems { + if item.Key == string(telemetry.TelemetryItemKeyHTMLFirstServedAt) { + htmlFirstServedFound = true + } + } + return htmlFirstServedFound + }, testutil.WaitMedium, testutil.IntervalFast, "no html_first_served telemetry item") }) t.Run("Prometheus", func(t *testing.T) { t.Parallel() @@ -1990,3 +2210,167 @@ func TestServer_DisabledDERP(t *testing.T) { err = c.Connect(ctx) require.Error(t, err) } + +type runServerOpts struct { + waitForSnapshot bool + telemetryDisabled bool + waitForTelemetryDisabledCheck bool +} + +func TestServer_TelemetryDisabled_FinalReport(t *testing.T) { + t.Parallel() + + if !dbtestutil.WillUsePostgres() { + t.Skip("this test requires postgres") + } + + telemetryServerURL, deployment, snapshot := mockTelemetryServer(t) + dbConnURL, err := dbtestutil.Open(t) + require.NoError(t, err) + + cacheDir := t.TempDir() + runServer := func(t *testing.T, opts runServerOpts) (chan error, context.CancelFunc) { + ctx, cancelFunc := context.WithCancel(context.Background()) + inv, _ := clitest.New(t, + "server", + "--postgres-url", dbConnURL, + "--http-address", ":0", + "--access-url", "http://example.com", + "--telemetry="+strconv.FormatBool(!opts.telemetryDisabled), + "--telemetry-url", telemetryServerURL.String(), + "--cache-dir", cacheDir, + "--log-filter", ".*", + ) + finished := make(chan bool, 2) + errChan := make(chan error, 1) + pty := ptytest.New(t).Attach(inv) + go func() { + errChan <- inv.WithContext(ctx).Run() + finished <- true + }() + go func() { + defer func() { + finished <- true + }() + if opts.waitForSnapshot { + pty.ExpectMatchContext(testutil.Context(t, testutil.WaitLong), "submitted snapshot") + } + if opts.waitForTelemetryDisabledCheck { + pty.ExpectMatchContext(testutil.Context(t, testutil.WaitLong), "finished telemetry status check") + } + }() + <-finished + return errChan, cancelFunc + } + waitForShutdown := func(t *testing.T, errChan chan error) error { + t.Helper() + select { + case err := <-errChan: + return err + case <-time.After(testutil.WaitMedium): + t.Fatalf("timed out waiting for server to shutdown") + } + return nil + } + + errChan, cancelFunc := runServer(t, runServerOpts{telemetryDisabled: true, waitForTelemetryDisabledCheck: true}) + cancelFunc() + require.NoError(t, waitForShutdown(t, errChan)) + + // Since telemetry was disabled, we expect no deployments or snapshots. + require.Empty(t, deployment) + require.Empty(t, snapshot) + + errChan, cancelFunc = runServer(t, runServerOpts{waitForSnapshot: true}) + cancelFunc() + require.NoError(t, waitForShutdown(t, errChan)) + // we expect to see a deployment and a snapshot twice: + // 1. the first pair is sent when the server starts + // 2. the second pair is sent when the server shuts down + for i := 0; i < 2; i++ { + select { + case <-snapshot: + case <-time.After(testutil.WaitShort / 2): + t.Fatalf("timed out waiting for snapshot") + } + select { + case <-deployment: + case <-time.After(testutil.WaitShort / 2): + t.Fatalf("timed out waiting for deployment") + } + } + + errChan, cancelFunc = runServer(t, runServerOpts{telemetryDisabled: true, waitForTelemetryDisabledCheck: true}) + cancelFunc() + require.NoError(t, waitForShutdown(t, errChan)) + + // Since telemetry is disabled, we expect no deployment. We expect a snapshot + // with the telemetry disabled item. + require.Empty(t, deployment) + select { + case ss := <-snapshot: + require.Len(t, ss.TelemetryItems, 1) + require.Equal(t, string(telemetry.TelemetryItemKeyTelemetryEnabled), ss.TelemetryItems[0].Key) + require.Equal(t, "false", ss.TelemetryItems[0].Value) + case <-time.After(testutil.WaitShort / 2): + t.Fatalf("timed out waiting for snapshot") + } + + errChan, cancelFunc = runServer(t, runServerOpts{telemetryDisabled: true, waitForTelemetryDisabledCheck: true}) + cancelFunc() + require.NoError(t, waitForShutdown(t, errChan)) + // Since telemetry is disabled and we've already sent a snapshot, we expect no + // new deployments or snapshots. + require.Empty(t, deployment) + require.Empty(t, snapshot) +} + +func mockTelemetryServer(t *testing.T) (*url.URL, chan *telemetry.Deployment, chan *telemetry.Snapshot) { + t.Helper() + deployment := make(chan *telemetry.Deployment, 64) + snapshot := make(chan *telemetry.Snapshot, 64) + r := chi.NewRouter() + r.Post("/deployment", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, buildinfo.Version(), r.Header.Get(telemetry.VersionHeader)) + dd := &telemetry.Deployment{} + err := json.NewDecoder(r.Body).Decode(dd) + require.NoError(t, err) + deployment <- dd + // Ensure the header is sent only after deployment is sent + w.WriteHeader(http.StatusAccepted) + }) + r.Post("/snapshot", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, buildinfo.Version(), r.Header.Get(telemetry.VersionHeader)) + ss := &telemetry.Snapshot{} + err := json.NewDecoder(r.Body).Decode(ss) + require.NoError(t, err) + snapshot <- ss + // Ensure the header is sent only after snapshot is sent + w.WriteHeader(http.StatusAccepted) + }) + server := httptest.NewServer(r) + t.Cleanup(server.Close) + serverURL, err := url.Parse(server.URL) + require.NoError(t, err) + + return serverURL, deployment, snapshot +} + +// syncWriter provides a thread-safe io.ReadWriter implementation +type syncReaderWriter struct { + buf bytes.Buffer + mu sync.Mutex +} + +func (w *syncReaderWriter) Write(p []byte) (n int, err error) { + w.mu.Lock() + defer w.mu.Unlock() + return w.buf.Write(p) +} + +func (w *syncReaderWriter) Read(p []byte) (n int, err error) { + w.mu.Lock() + defer w.mu.Unlock() + + return w.buf.Read(p) +} diff --git a/cli/show.go b/cli/show.go index 00c50292d69c1..f2d3df3ecc3c5 100644 --- a/cli/show.go +++ b/cli/show.go @@ -1,8 +1,13 @@ package cli import ( + "sort" + "sync" + "golang.org/x/xerrors" + "github.com/google/uuid" + "github.com/coder/coder/v2/cli/cliui" "github.com/coder/coder/v2/codersdk" "github.com/coder/serpent" @@ -26,10 +31,60 @@ func (r *RootCmd) show() *serpent.Command { if err != nil { return xerrors.Errorf("get workspace: %w", err) } - return cliui.WorkspaceResources(inv.Stdout, workspace.LatestBuild.Resources, cliui.WorkspaceResourcesOptions{ + + options := cliui.WorkspaceResourcesOptions{ WorkspaceName: workspace.Name, ServerVersion: buildInfo.Version, - }) + } + if workspace.LatestBuild.Status == codersdk.WorkspaceStatusRunning { + // Get listening ports for each agent. + ports, devcontainers := fetchRuntimeResources(inv, client, workspace.LatestBuild.Resources...) + options.ListeningPorts = ports + options.Devcontainers = devcontainers + } + return cliui.WorkspaceResources(inv.Stdout, workspace.LatestBuild.Resources, options) }, } } + +func fetchRuntimeResources(inv *serpent.Invocation, client *codersdk.Client, resources ...codersdk.WorkspaceResource) (map[uuid.UUID]codersdk.WorkspaceAgentListeningPortsResponse, map[uuid.UUID]codersdk.WorkspaceAgentListContainersResponse) { + ports := make(map[uuid.UUID]codersdk.WorkspaceAgentListeningPortsResponse) + devcontainers := make(map[uuid.UUID]codersdk.WorkspaceAgentListContainersResponse) + var wg sync.WaitGroup + var mu sync.Mutex + for _, res := range resources { + for _, agent := range res.Agents { + wg.Add(1) + go func() { + defer wg.Done() + lp, err := client.WorkspaceAgentListeningPorts(inv.Context(), agent.ID) + if err != nil { + cliui.Warnf(inv.Stderr, "Failed to get listening ports for agent %s: %v", agent.Name, err) + } + sort.Slice(lp.Ports, func(i, j int) bool { + return lp.Ports[i].Port < lp.Ports[j].Port + }) + mu.Lock() + ports[agent.ID] = lp + mu.Unlock() + }() + wg.Add(1) + go func() { + defer wg.Done() + dc, err := client.WorkspaceAgentListContainers(inv.Context(), agent.ID, map[string]string{ + // Labels set by VSCode Remote Containers and @devcontainers/cli. + "devcontainer.config_file": "", + "devcontainer.local_folder": "", + }) + if err != nil { + cliui.Warnf(inv.Stderr, "Failed to get devcontainers for agent %s: %v", agent.Name, err) + } + mu.Lock() + devcontainers[agent.ID] = dc + mu.Unlock() + }() + } + } + wg.Wait() + return ports, devcontainers +} diff --git a/cli/ssh_test.go b/cli/ssh_test.go index b403f7ff83a8e..d20278bbf7ced 100644 --- a/cli/ssh_test.go +++ b/cli/ssh_test.go @@ -453,6 +453,71 @@ func TestSSH(t *testing.T) { <-cmdDone }) + t.Run("DeterministicHostKey", func(t *testing.T) { + t.Parallel() + client, workspace, agentToken := setupWorkspaceForAgent(t) + _, _ = tGoContext(t, func(ctx context.Context) { + // Run this async so the SSH command has to wait for + // the build and agent to connect! + _ = agenttest.New(t, client.URL, agentToken) + <-ctx.Done() + }) + + clientOutput, clientInput := io.Pipe() + serverOutput, serverInput := io.Pipe() + defer func() { + for _, c := range []io.Closer{clientOutput, clientInput, serverOutput, serverInput} { + _ = c.Close() + } + }() + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + inv, root := clitest.New(t, "ssh", "--stdio", workspace.Name) + clitest.SetupConfig(t, client, root) + inv.Stdin = clientOutput + inv.Stdout = serverInput + inv.Stderr = io.Discard + + cmdDone := tGo(t, func() { + err := inv.WithContext(ctx).Run() + assert.NoError(t, err) + }) + + keySeed, err := agent.WorkspaceKeySeed(workspace.ID, "dev") + assert.NoError(t, err) + + signer, err := agentssh.CoderSigner(keySeed) + assert.NoError(t, err) + + conn, channels, requests, err := ssh.NewClientConn(&stdioConn{ + Reader: serverOutput, + Writer: clientInput, + }, "", &ssh.ClientConfig{ + HostKeyCallback: ssh.FixedHostKey(signer.PublicKey()), + }) + require.NoError(t, err) + defer conn.Close() + + sshClient := ssh.NewClient(conn, channels, requests) + session, err := sshClient.NewSession() + require.NoError(t, err) + defer session.Close() + + command := "sh -c exit" + if runtime.GOOS == "windows" { + command = "cmd.exe /c exit" + } + err = session.Run(command) + require.NoError(t, err) + err = sshClient.Close() + require.NoError(t, err) + _ = clientOutput.Close() + + <-cmdDone + }) + t.Run("NetworkInfo", func(t *testing.T) { t.Parallel() client, workspace, agentToken := setupWorkspaceForAgent(t) diff --git a/cli/templateversionarchive.go b/cli/templateversionarchive.go index 10beda42b9afa..e9b41ff31dcd1 100644 --- a/cli/templateversionarchive.go +++ b/cli/templateversionarchive.go @@ -76,7 +76,7 @@ func (r *RootCmd) setArchiveTemplateVersion(archive bool) *serpent.Command { for _, version := range versions { if version.Archived == archive { _, _ = fmt.Fprintln( - inv.Stdout, fmt.Sprintf("Version "+pretty.Sprint(cliui.DefaultStyles.Keyword, version.Name)+" already "+pastVerb), + inv.Stdout, "Version "+pretty.Sprint(cliui.DefaultStyles.Keyword, version.Name)+" already "+pastVerb, ) continue } @@ -87,7 +87,7 @@ func (r *RootCmd) setArchiveTemplateVersion(archive bool) *serpent.Command { } _, _ = fmt.Fprintln( - inv.Stdout, fmt.Sprintf("Version "+pretty.Sprint(cliui.DefaultStyles.Keyword, version.Name)+" "+pastVerb+" at "+cliui.Timestamp(time.Now())), + inv.Stdout, "Version "+pretty.Sprint(cliui.DefaultStyles.Keyword, version.Name)+" "+pastVerb+" at "+cliui.Timestamp(time.Now()), ) } return nil diff --git a/cli/testdata/coder_agent_--help.golden b/cli/testdata/coder_agent_--help.golden index 3394b43a9e900..6548a2fadbe49 100644 --- a/cli/testdata/coder_agent_--help.golden +++ b/cli/testdata/coder_agent_--help.golden @@ -33,6 +33,9 @@ OPTIONS: --debug-address string, $CODER_AGENT_DEBUG_ADDRESS (default: 127.0.0.1:2113) The bind address to serve a debug HTTP server. + --devcontainers-enable bool, $CODER_AGENT_DEVCONTAINERS_ENABLE (default: false) + Allow the agent to automatically detect running devcontainers. + --log-dir string, $CODER_AGENT_LOG_DIR (default: /tmp) Specify the location for the agent log files. diff --git a/cli/testdata/coder_list_--output_json.golden b/cli/testdata/coder_list_--output_json.golden index 0ef065dd86a81..4b308a9468b6f 100644 --- a/cli/testdata/coder_list_--output_json.golden +++ b/cli/testdata/coder_list_--output_json.golden @@ -48,7 +48,14 @@ "input": { "workspace_build_id": "========[workspace build ID]========" }, - "type": "workspace_build" + "type": "workspace_build", + "metadata": { + "template_version_name": "", + "template_id": "00000000-0000-0000-0000-000000000000", + "template_name": "", + "template_display_name": "", + "template_icon": "" + } }, "reason": "initiator", "resources": [], diff --git a/cli/testdata/coder_notifications_--help.golden b/cli/testdata/coder_notifications_--help.golden index b54e98543da7b..ced45ca0da6e5 100644 --- a/cli/testdata/coder_notifications_--help.golden +++ b/cli/testdata/coder_notifications_--help.golden @@ -19,10 +19,17 @@ USAGE: - Resume Coder notifications: $ coder notifications resume + + - Send a test notification. Administrators can use this to verify the + notification + target settings.: + + $ coder notifications test SUBCOMMANDS: pause Pause notifications resume Resume notifications + test Send a test notification ——— Run `coder --help` for a list of global options. diff --git a/cli/testdata/coder_notifications_test_--help.golden b/cli/testdata/coder_notifications_test_--help.golden new file mode 100644 index 0000000000000..37c3402ba99b1 --- /dev/null +++ b/cli/testdata/coder_notifications_test_--help.golden @@ -0,0 +1,9 @@ +coder v0.0.0-devel + +USAGE: + coder notifications test + + Send a test notification + +——— +Run `coder --help` for a list of global options. diff --git a/cli/testdata/coder_ping_--help.golden b/cli/testdata/coder_ping_--help.golden index 4955e889c3651..e2e2c11e55214 100644 --- a/cli/testdata/coder_ping_--help.golden +++ b/cli/testdata/coder_ping_--help.golden @@ -10,9 +10,15 @@ OPTIONS: Specifies the number of pings to perform. By default, pings will continue until interrupted. + --time bool + Show the response time of each pong in local time. + -t, --timeout duration (default: 5s) Specifies how long to wait for a ping to complete. + --utc bool + Show the response time of each pong in UTC (implies --time). + --wait duration (default: 1s) Specifies how long to wait between pings. diff --git a/cli/testdata/coder_provisioner_jobs_list_--help.golden b/cli/testdata/coder_provisioner_jobs_list_--help.golden index 585e918c23e7b..d6eb9a7681a07 100644 --- a/cli/testdata/coder_provisioner_jobs_list_--help.golden +++ b/cli/testdata/coder_provisioner_jobs_list_--help.golden @@ -11,7 +11,7 @@ OPTIONS: -O, --org string, $CODER_ORGANIZATION Select which organization (uuid or name) to use. - -c, --column [id|created at|started at|completed at|canceled at|error|error code|status|worker id|file id|tags|queue position|queue size|organization id|template version id|workspace build id|type|available workers|organization|queue] (default: created at,id,organization,status,type,queue,tags) + -c, --column [id|created at|started at|completed at|canceled at|error|error code|status|worker id|file id|tags|queue position|queue size|organization id|template version id|workspace build id|type|available workers|template version name|template id|template name|template display name|template icon|workspace id|workspace name|organization|queue] (default: created at,id,organization,status,type,queue,tags) Columns to display in table output. -l, --limit int, $CODER_PROVISIONER_JOB_LIST_LIMIT (default: 50) diff --git a/cli/testdata/coder_provisioner_jobs_list_--output_json.golden b/cli/testdata/coder_provisioner_jobs_list_--output_json.golden index a19683573bba2..d18e07121f653 100644 --- a/cli/testdata/coder_provisioner_jobs_list_--output_json.golden +++ b/cli/testdata/coder_provisioner_jobs_list_--output_json.golden @@ -18,6 +18,13 @@ "template_version_id": "============[version ID]============" }, "type": "template_version_import", + "metadata": { + "template_version_name": "===========[version name]===========", + "template_id": "===========[template ID]============", + "template_name": "test-template", + "template_display_name": "", + "template_icon": "" + }, "organization_name": "Coder" }, { @@ -39,6 +46,15 @@ "workspace_build_id": "========[workspace build ID]========" }, "type": "workspace_build", + "metadata": { + "template_version_name": "===========[version name]===========", + "template_id": "===========[template ID]============", + "template_name": "test-template", + "template_display_name": "", + "template_icon": "", + "workspace_id": "===========[workspace ID]===========", + "workspace_name": "test-workspace" + }, "organization_name": "Coder" } ] diff --git a/cli/testdata/coder_provisioner_list_--help.golden b/cli/testdata/coder_provisioner_list_--help.golden index a9943cb9da392..111eb8315b162 100644 --- a/cli/testdata/coder_provisioner_list_--help.golden +++ b/cli/testdata/coder_provisioner_list_--help.golden @@ -11,7 +11,7 @@ OPTIONS: -O, --org string, $CODER_ORGANIZATION Select which organization (uuid or name) to use. - -c, --column [id|organization id|created at|last seen at|name|version|api version|tags|key name|status|current job id|current job status|previous job id|previous job status|organization] (default: name,organization,status,key name,created at,last seen at,version,tags) + -c, --column [id|organization id|created at|last seen at|name|version|api version|tags|key name|status|current job id|current job status|current job template name|current job template icon|current job template display name|previous job id|previous job status|previous job template name|previous job template icon|previous job template display name|organization] (default: name,organization,status,key name,created at,last seen at,version,tags) Columns to display in table output. -o, --output table|json (default: table) diff --git a/cli/testdata/coder_provisioner_list_--output_json.golden b/cli/testdata/coder_provisioner_list_--output_json.golden index bb8e2fd0d09c9..168e690f0b33a 100644 --- a/cli/testdata/coder_provisioner_list_--output_json.golden +++ b/cli/testdata/coder_provisioner_list_--output_json.golden @@ -7,7 +7,7 @@ "last_seen_at": "====[timestamp]=====", "name": "test", "version": "v0.0.0-devel", - "api_version": "1.2", + "api_version": "1.3", "provisioners": [ "echo" ], @@ -20,7 +20,10 @@ "current_job": null, "previous_job": { "id": "======[workspace build job ID]======", - "status": "succeeded" + "status": "succeeded", + "template_name": "test-template", + "template_icon": "", + "template_display_name": "" }, "organization_name": "Coder" } diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index 93d9d69517ec9..df1f982bc52fe 100644 --- a/cli/testdata/coder_server_--help.golden +++ b/cli/testdata/coder_server_--help.golden @@ -498,6 +498,12 @@ OAUTH2 / GITHUB OPTIONS: --oauth2-github-client-secret string, $CODER_OAUTH2_GITHUB_CLIENT_SECRET Client secret for Login with GitHub. + --oauth2-github-default-provider-enable bool, $CODER_OAUTH2_GITHUB_DEFAULT_PROVIDER_ENABLE (default: true) + Enable the default GitHub OAuth2 provider managed by Coder. + + --oauth2-github-device-flow bool, $CODER_OAUTH2_GITHUB_DEVICE_FLOW (default: false) + Enable device flow for Login with GitHub. + --oauth2-github-enterprise-base-url string, $CODER_OAUTH2_GITHUB_ENTERPRISE_BASE_URL Base URL of a GitHub Enterprise deployment to use for Login with GitHub. diff --git a/cli/testdata/coder_tokens_remove_--help.golden b/cli/testdata/coder_tokens_remove_--help.golden index 30440e8ef2e7c..63caab0c7e09f 100644 --- a/cli/testdata/coder_tokens_remove_--help.golden +++ b/cli/testdata/coder_tokens_remove_--help.golden @@ -1,7 +1,7 @@ coder v0.0.0-devel USAGE: - coder tokens remove <name> + coder tokens remove <name|id|token> Delete a token diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden index 96a03c5b1f05e..cffaf65cd3cef 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -262,6 +262,12 @@ oauth2: # Client ID for Login with GitHub. # (default: <unset>, type: string) clientID: "" + # Enable device flow for Login with GitHub. + # (default: false, type: bool) + deviceFlow: false + # Enable the default GitHub OAuth2 provider managed by Coder. + # (default: true, type: bool) + defaultProviderEnable: true # Organizations the user must be a member of to Login with GitHub. # (default: <unset>, type: string-array) allowedOrgs: [] @@ -326,6 +332,12 @@ oidc: # Ignore the userinfo endpoint and only use the ID token for user information. # (default: false, type: bool) ignoreUserInfo: false + # Source supplemental user claims from the 'access_token'. This assumes the token + # is a jwt signed by the same issuer as the id_token. Using this requires setting + # 'oidc-ignore-userinfo' to true. This setting is not compliant with the OIDC + # specification and is not recommended. Use at your own risk. + # (default: false, type: bool) + accessTokenClaims: false # This field must be set if using the organization sync feature. Set to the claim # to be used for organizations. # (default: <unset>, type: string) diff --git a/cli/tokens.go b/cli/tokens.go index 2488a687a0c07..d132547576d32 100644 --- a/cli/tokens.go +++ b/cli/tokens.go @@ -3,6 +3,7 @@ package cli import ( "fmt" "os" + "strings" "time" "golang.org/x/exp/slices" @@ -223,7 +224,7 @@ func (r *RootCmd) listTokens() *serpent.Command { func (r *RootCmd) removeToken() *serpent.Command { client := new(codersdk.Client) cmd := &serpent.Command{ - Use: "remove <name>", + Use: "remove <name|id|token>", Aliases: []string{"delete"}, Short: "Delete a token", Middleware: serpent.Chain( @@ -233,7 +234,12 @@ func (r *RootCmd) removeToken() *serpent.Command { Handler: func(inv *serpent.Invocation) error { token, err := client.APIKeyByName(inv.Context(), codersdk.Me, inv.Args[0]) if err != nil { - return xerrors.Errorf("fetch api key by name %s: %w", inv.Args[0], err) + // If it's a token, we need to extract the ID + maybeID := strings.Split(inv.Args[0], "-")[0] + token, err = client.APIKeyByID(inv.Context(), codersdk.Me, maybeID) + if err != nil { + return xerrors.Errorf("fetch api key by name or id: %w", err) + } } err = client.DeleteAPIKey(inv.Context(), codersdk.Me, token.ID) diff --git a/cli/tokens_test.go b/cli/tokens_test.go index 7c024f3ad1a6f..0c717bb890f9e 100644 --- a/cli/tokens_test.go +++ b/cli/tokens_test.go @@ -93,7 +93,7 @@ func TestTokens(t *testing.T) { require.Contains(t, res, secondTokenID) // Test creating a token for third user from second user's (non-admin) session - inv, root = clitest.New(t, "tokens", "create", "--name", "token-two", "--user", thirdUser.ID.String()) + inv, root = clitest.New(t, "tokens", "create", "--name", "failed-token", "--user", thirdUser.ID.String()) clitest.SetupConfig(t, secondUserClient, root) buf = new(bytes.Buffer) inv.Stdout = buf @@ -113,6 +113,7 @@ func TestTokens(t *testing.T) { require.Len(t, tokens, 1) require.Equal(t, id, tokens[0].ID) + // Delete by name inv, root = clitest.New(t, "tokens", "rm", "token-one") clitest.SetupConfig(t, client, root) buf = new(bytes.Buffer) @@ -122,4 +123,37 @@ func TestTokens(t *testing.T) { res = buf.String() require.NotEmpty(t, res) require.Contains(t, res, "deleted") + + // Delete by ID + inv, root = clitest.New(t, "tokens", "rm", secondTokenID) + clitest.SetupConfig(t, client, root) + buf = new(bytes.Buffer) + inv.Stdout = buf + err = inv.WithContext(ctx).Run() + require.NoError(t, err) + res = buf.String() + require.NotEmpty(t, res) + require.Contains(t, res, "deleted") + + // Create third token + inv, root = clitest.New(t, "tokens", "create", "--name", "token-three") + clitest.SetupConfig(t, client, root) + buf = new(bytes.Buffer) + inv.Stdout = buf + err = inv.WithContext(ctx).Run() + require.NoError(t, err) + res = buf.String() + require.NotEmpty(t, res) + fourthToken := res + + // Delete by token + inv, root = clitest.New(t, "tokens", "rm", fourthToken) + clitest.SetupConfig(t, client, root) + buf = new(bytes.Buffer) + inv.Stdout = buf + err = inv.WithContext(ctx).Run() + require.NoError(t, err) + res = buf.String() + require.NotEmpty(t, res) + require.Contains(t, res, "deleted") } diff --git a/cli/vpndaemon_windows.go b/cli/vpndaemon_windows.go index d09733817d787..227bd0fe8e0db 100644 --- a/cli/vpndaemon_windows.go +++ b/cli/vpndaemon_windows.go @@ -41,7 +41,10 @@ func (r *RootCmd) vpnDaemonRun() *serpent.Command { }, Handler: func(inv *serpent.Invocation) error { ctx := inv.Context() - logger := inv.Logger.AppendSinks(sloghuman.Sink(inv.Stderr)).Leveled(slog.LevelDebug) + sinks := []slog.Sink{ + sloghuman.Sink(inv.Stderr), + } + logger := inv.Logger.AppendSinks(sinks...).Leveled(slog.LevelDebug) if rpcReadHandleInt < 0 || rpcWriteHandleInt < 0 { return xerrors.Errorf("rpc-read-handle (%v) and rpc-write-handle (%v) must be positive", rpcReadHandleInt, rpcWriteHandleInt) @@ -60,7 +63,11 @@ func (r *RootCmd) vpnDaemonRun() *serpent.Command { defer pipe.Close() logger.Info(ctx, "starting tunnel") - tunnel, err := vpn.NewTunnel(ctx, logger, pipe, vpn.NewClient()) + tunnel, err := vpn.NewTunnel(ctx, logger, pipe, vpn.NewClient(), + vpn.UseOSNetworkingStack(), + vpn.UseAsLogger(), + vpn.UseCustomLogSinks(sinks...), + ) if err != nil { return xerrors.Errorf("create new tunnel for client: %w", err) } diff --git a/cmd/coder/main.go b/cmd/coder/main.go index 1c22d578d7160..27918798b3a12 100644 --- a/cmd/coder/main.go +++ b/cmd/coder/main.go @@ -8,6 +8,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/coder/coder/v2/agent/agentexec" + _ "github.com/coder/coder/v2/buildinfo/resources" "github.com/coder/coder/v2/cli" ) diff --git a/coderd/agentapi/api.go b/coderd/agentapi/api.go index 62fe6fad8d4de..58032c0978b8d 100644 --- a/coderd/agentapi/api.go +++ b/coderd/agentapi/api.go @@ -17,10 +17,13 @@ import ( "cdr.dev/slog" agentproto "github.com/coder/coder/v2/agent/proto" + "github.com/coder/coder/v2/coderd/agentapi/resourcesmonitor" "github.com/coder/coder/v2/coderd/appearance" + "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/pubsub" "github.com/coder/coder/v2/coderd/externalauth" + "github.com/coder/coder/v2/coderd/notifications" "github.com/coder/coder/v2/coderd/prometheusmetrics" "github.com/coder/coder/v2/coderd/tracing" "github.com/coder/coder/v2/coderd/workspacestats" @@ -29,6 +32,7 @@ import ( "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/coder/v2/tailnet" tailnetproto "github.com/coder/coder/v2/tailnet/proto" + "github.com/coder/quartz" ) // API implements the DRPC agent API interface from agent/proto. This struct is @@ -42,8 +46,10 @@ type API struct { *LifecycleAPI *AppsAPI *MetadataAPI + *ResourcesMonitoringAPI *LogsAPI *ScriptsAPI + *AuditAPI *tailnet.DRPCService mu sync.Mutex @@ -58,8 +64,11 @@ type Options struct { Ctx context.Context Log slog.Logger + Clock quartz.Clock Database database.Store + NotificationsEnqueuer notifications.Enqueuer Pubsub pubsub.Pubsub + Auditor *atomic.Pointer[audit.Auditor] DerpMapFn func() *tailcfg.DERPMap TailnetCoordinator *atomic.Pointer[tailnet.Coordinator] StatsReporter *workspacestats.Reporter @@ -81,6 +90,10 @@ type Options struct { } func New(opts Options) *API { + if opts.Clock == nil { + opts.Clock = quartz.NewReal() + } + api := &API{ opts: opts, mu: sync.Mutex{}, @@ -102,6 +115,25 @@ func New(opts Options) *API { appearanceFetcher: opts.AppearanceFetcher, } + api.ResourcesMonitoringAPI = &ResourcesMonitoringAPI{ + AgentID: opts.AgentID, + WorkspaceID: opts.WorkspaceID, + Clock: opts.Clock, + Database: opts.Database, + NotificationsEnqueuer: opts.NotificationsEnqueuer, + Debounce: 5 * time.Minute, + + Config: resourcesmonitor.Config{ + NumDatapoints: 20, + CollectionInterval: 10 * time.Second, + + Alert: resourcesmonitor.AlertConfig{ + MinimumNOKsPercent: 20, + ConsecutiveNOKsPercent: 50, + }, + }, + } + api.StatsAPI = &StatsAPI{ AgentFn: api.agent, Database: opts.Database, @@ -145,6 +177,13 @@ func New(opts Options) *API { Database: opts.Database, } + api.AuditAPI = &AuditAPI{ + AgentFn: api.agent, + Auditor: opts.Auditor, + Database: opts.Database, + Log: opts.Log, + } + api.DRPCService = &tailnet.DRPCService{ CoordPtr: opts.TailnetCoordinator, Logger: opts.Log, diff --git a/coderd/agentapi/audit.go b/coderd/agentapi/audit.go new file mode 100644 index 0000000000000..2025b2d6cd92b --- /dev/null +++ b/coderd/agentapi/audit.go @@ -0,0 +1,105 @@ +package agentapi + +import ( + "context" + "encoding/json" + "strconv" + "sync/atomic" + + "github.com/google/uuid" + "golang.org/x/xerrors" + "google.golang.org/protobuf/types/known/emptypb" + + "cdr.dev/slog" + + agentproto "github.com/coder/coder/v2/agent/proto" + "github.com/coder/coder/v2/coderd/audit" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/db2sdk" + "github.com/coder/coder/v2/codersdk/agentsdk" +) + +type AuditAPI struct { + AgentFn func(context.Context) (database.WorkspaceAgent, error) + Auditor *atomic.Pointer[audit.Auditor] + Database database.Store + Log slog.Logger +} + +func (a *AuditAPI) ReportConnection(ctx context.Context, req *agentproto.ReportConnectionRequest) (*emptypb.Empty, error) { + // We will use connection ID as request ID, typically this is the + // SSH session ID as reported by the agent. + connectionID, err := uuid.FromBytes(req.GetConnection().GetId()) + if err != nil { + return nil, xerrors.Errorf("connection id from bytes: %w", err) + } + + action, err := db2sdk.AuditActionFromAgentProtoConnectionAction(req.GetConnection().GetAction()) + if err != nil { + return nil, err + } + connectionType, err := agentsdk.ConnectionTypeFromProto(req.GetConnection().GetType()) + if err != nil { + return nil, err + } + + // Fetch contextual data for this audit event. + workspaceAgent, err := a.AgentFn(ctx) + if err != nil { + return nil, xerrors.Errorf("get agent: %w", err) + } + workspace, err := a.Database.GetWorkspaceByAgentID(ctx, workspaceAgent.ID) + if err != nil { + return nil, xerrors.Errorf("get workspace by agent id: %w", err) + } + build, err := a.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID) + if err != nil { + return nil, xerrors.Errorf("get latest workspace build by workspace id: %w", err) + } + + // We pass the below information to the Auditor so that it + // can form a friendly string for the user to view in the UI. + type additionalFields struct { + audit.AdditionalFields + + ConnectionType agentsdk.ConnectionType `json:"connection_type"` + Reason string `json:"reason,omitempty"` + } + resourceInfo := additionalFields{ + AdditionalFields: audit.AdditionalFields{ + WorkspaceID: workspace.ID, + WorkspaceName: workspace.Name, + WorkspaceOwner: workspace.OwnerUsername, + BuildNumber: strconv.FormatInt(int64(build.BuildNumber), 10), + BuildReason: database.BuildReason(string(build.Reason)), + }, + ConnectionType: connectionType, + Reason: req.GetConnection().GetReason(), + } + + riBytes, err := json.Marshal(resourceInfo) + if err != nil { + a.Log.Error(ctx, "marshal resource info for agent connection failed", slog.Error(err)) + riBytes = []byte("{}") + } + + audit.BackgroundAudit(ctx, &audit.BackgroundAuditParams[database.WorkspaceAgent]{ + Audit: *a.Auditor.Load(), + Log: a.Log, + Time: req.GetConnection().GetTimestamp().AsTime(), + OrganizationID: workspace.OrganizationID, + RequestID: connectionID, + Action: action, + New: workspaceAgent, + Old: workspaceAgent, + IP: req.GetConnection().GetIp(), + Status: int(req.GetConnection().GetStatusCode()), + AdditionalFields: riBytes, + + // It's not possible to tell which user connected. Once we have + // the capability, this may be reported by the agent. + UserID: uuid.Nil, + }) + + return &emptypb.Empty{}, nil +} diff --git a/coderd/agentapi/audit_test.go b/coderd/agentapi/audit_test.go new file mode 100644 index 0000000000000..8b4ae3ea60f77 --- /dev/null +++ b/coderd/agentapi/audit_test.go @@ -0,0 +1,179 @@ +package agentapi_test + +import ( + "context" + "encoding/json" + "net" + "sync/atomic" + "testing" + "time" + + "github.com/google/uuid" + "github.com/sqlc-dev/pqtype" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "google.golang.org/protobuf/types/known/timestamppb" + + agentproto "github.com/coder/coder/v2/agent/proto" + "github.com/coder/coder/v2/coderd/agentapi" + "github.com/coder/coder/v2/coderd/audit" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/db2sdk" + "github.com/coder/coder/v2/coderd/database/dbmock" + "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/coder/v2/codersdk/agentsdk" +) + +func TestAuditReport(t *testing.T) { + t.Parallel() + + var ( + owner = database.User{ + ID: uuid.New(), + Username: "cool-user", + } + workspace = database.Workspace{ + ID: uuid.New(), + OrganizationID: uuid.New(), + OwnerID: owner.ID, + Name: "cool-workspace", + } + build = database.WorkspaceBuild{ + ID: uuid.New(), + WorkspaceID: workspace.ID, + } + agent = database.WorkspaceAgent{ + ID: uuid.New(), + } + ) + + tests := []struct { + name string + id uuid.UUID + action *agentproto.Connection_Action + typ *agentproto.Connection_Type + time time.Time + ip string + status int32 + reason string + }{ + { + name: "SSH Connect", + id: uuid.New(), + action: agentproto.Connection_CONNECT.Enum(), + typ: agentproto.Connection_SSH.Enum(), + time: time.Now(), + ip: "127.0.0.1", + status: 200, + }, + { + name: "VS Code Connect", + id: uuid.New(), + action: agentproto.Connection_CONNECT.Enum(), + typ: agentproto.Connection_VSCODE.Enum(), + time: time.Now(), + ip: "8.8.8.8", + }, + { + name: "JetBrains Connect", + id: uuid.New(), + action: agentproto.Connection_CONNECT.Enum(), + typ: agentproto.Connection_JETBRAINS.Enum(), + time: time.Now(), + }, + { + name: "Reconnecting PTY Connect", + id: uuid.New(), + action: agentproto.Connection_CONNECT.Enum(), + typ: agentproto.Connection_RECONNECTING_PTY.Enum(), + time: time.Now(), + }, + { + name: "SSH Disconnect", + id: uuid.New(), + action: agentproto.Connection_DISCONNECT.Enum(), + typ: agentproto.Connection_SSH.Enum(), + time: time.Now(), + }, + { + name: "SSH Disconnect", + id: uuid.New(), + action: agentproto.Connection_DISCONNECT.Enum(), + typ: agentproto.Connection_SSH.Enum(), + time: time.Now(), + status: 500, + reason: "because error says so", + }, + } + //nolint:paralleltest // No longer necessary to reinitialise the variable tt. + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + mAudit := audit.NewMock() + + mDB := dbmock.NewMockStore(gomock.NewController(t)) + mDB.EXPECT().GetWorkspaceByAgentID(gomock.Any(), agent.ID).Return(workspace, nil) + mDB.EXPECT().GetLatestWorkspaceBuildByWorkspaceID(gomock.Any(), workspace.ID).Return(build, nil) + + api := &agentapi.AuditAPI{ + Auditor: asAtomicPointer[audit.Auditor](mAudit), + Database: mDB, + AgentFn: func(context.Context) (database.WorkspaceAgent, error) { + return agent, nil + }, + } + api.ReportConnection(context.Background(), &agentproto.ReportConnectionRequest{ + Connection: &agentproto.Connection{ + Id: tt.id[:], + Action: *tt.action, + Type: *tt.typ, + Timestamp: timestamppb.New(tt.time), + Ip: tt.ip, + StatusCode: tt.status, + Reason: &tt.reason, + }, + }) + + mAudit.Contains(t, database.AuditLog{ + Time: dbtime.Time(tt.time).In(time.UTC), + Action: agentProtoConnectionActionToAudit(t, *tt.action), + OrganizationID: workspace.OrganizationID, + UserID: uuid.Nil, + RequestID: tt.id, + ResourceType: database.ResourceTypeWorkspaceAgent, + ResourceID: agent.ID, + ResourceTarget: agent.Name, + Ip: pqtype.Inet{Valid: true, IPNet: net.IPNet{IP: net.ParseIP(tt.ip), Mask: net.CIDRMask(32, 32)}}, + StatusCode: tt.status, + }) + + // Check some additional fields. + var m map[string]any + err := json.Unmarshal(mAudit.AuditLogs()[0].AdditionalFields, &m) + require.NoError(t, err) + require.Equal(t, string(agentProtoConnectionTypeToSDK(t, *tt.typ)), m["connection_type"].(string)) + if tt.reason != "" { + require.Equal(t, tt.reason, m["reason"]) + } + }) + } +} + +func agentProtoConnectionActionToAudit(t *testing.T, action agentproto.Connection_Action) database.AuditAction { + a, err := db2sdk.AuditActionFromAgentProtoConnectionAction(action) + require.NoError(t, err) + return a +} + +func agentProtoConnectionTypeToSDK(t *testing.T, typ agentproto.Connection_Type) agentsdk.ConnectionType { + action, err := agentsdk.ConnectionTypeFromProto(typ) + require.NoError(t, err) + return action +} + +func asAtomicPointer[T any](v T) *atomic.Pointer[T] { + var p atomic.Pointer[T] + p.Store(&v) + return &p +} diff --git a/coderd/agentapi/resources_monitoring.go b/coderd/agentapi/resources_monitoring.go new file mode 100644 index 0000000000000..e21c9bc7581d8 --- /dev/null +++ b/coderd/agentapi/resources_monitoring.go @@ -0,0 +1,256 @@ +package agentapi + +import ( + "context" + "database/sql" + "errors" + "fmt" + "time" + + "golang.org/x/xerrors" + + "cdr.dev/slog" + + "github.com/google/uuid" + + "github.com/coder/coder/v2/agent/proto" + "github.com/coder/coder/v2/coderd/agentapi/resourcesmonitor" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbauthz" + "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/coder/v2/coderd/notifications" + "github.com/coder/quartz" +) + +type ResourcesMonitoringAPI struct { + AgentID uuid.UUID + WorkspaceID uuid.UUID + + Log slog.Logger + Clock quartz.Clock + Database database.Store + NotificationsEnqueuer notifications.Enqueuer + + Debounce time.Duration + Config resourcesmonitor.Config +} + +func (a *ResourcesMonitoringAPI) GetResourcesMonitoringConfiguration(ctx context.Context, _ *proto.GetResourcesMonitoringConfigurationRequest) (*proto.GetResourcesMonitoringConfigurationResponse, error) { + memoryMonitor, memoryErr := a.Database.FetchMemoryResourceMonitorsByAgentID(ctx, a.AgentID) + if memoryErr != nil && !errors.Is(memoryErr, sql.ErrNoRows) { + return nil, xerrors.Errorf("failed to fetch memory resource monitor: %w", memoryErr) + } + + volumeMonitors, err := a.Database.FetchVolumesResourceMonitorsByAgentID(ctx, a.AgentID) + if err != nil { + return nil, xerrors.Errorf("failed to fetch volume resource monitors: %w", err) + } + + return &proto.GetResourcesMonitoringConfigurationResponse{ + Config: &proto.GetResourcesMonitoringConfigurationResponse_Config{ + CollectionIntervalSeconds: int32(a.Config.CollectionInterval.Seconds()), + NumDatapoints: a.Config.NumDatapoints, + }, + Memory: func() *proto.GetResourcesMonitoringConfigurationResponse_Memory { + if memoryErr != nil { + return nil + } + + return &proto.GetResourcesMonitoringConfigurationResponse_Memory{ + Enabled: memoryMonitor.Enabled, + } + }(), + Volumes: func() []*proto.GetResourcesMonitoringConfigurationResponse_Volume { + volumes := make([]*proto.GetResourcesMonitoringConfigurationResponse_Volume, 0, len(volumeMonitors)) + for _, monitor := range volumeMonitors { + volumes = append(volumes, &proto.GetResourcesMonitoringConfigurationResponse_Volume{ + Enabled: monitor.Enabled, + Path: monitor.Path, + }) + } + + return volumes + }(), + }, nil +} + +func (a *ResourcesMonitoringAPI) PushResourcesMonitoringUsage(ctx context.Context, req *proto.PushResourcesMonitoringUsageRequest) (*proto.PushResourcesMonitoringUsageResponse, error) { + var err error + + if memoryErr := a.monitorMemory(ctx, req.Datapoints); memoryErr != nil { + err = errors.Join(err, xerrors.Errorf("monitor memory: %w", memoryErr)) + } + + if volumeErr := a.monitorVolumes(ctx, req.Datapoints); volumeErr != nil { + err = errors.Join(err, xerrors.Errorf("monitor volume: %w", volumeErr)) + } + + return &proto.PushResourcesMonitoringUsageResponse{}, err +} + +func (a *ResourcesMonitoringAPI) monitorMemory(ctx context.Context, datapoints []*proto.PushResourcesMonitoringUsageRequest_Datapoint) error { + monitor, err := a.Database.FetchMemoryResourceMonitorsByAgentID(ctx, a.AgentID) + if err != nil { + // It is valid for an agent to not have a memory monitor, so we + // do not want to treat it as an error. + if errors.Is(err, sql.ErrNoRows) { + return nil + } + + return xerrors.Errorf("fetch memory resource monitor: %w", err) + } + + if !monitor.Enabled { + return nil + } + + usageDatapoints := make([]*proto.PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage, 0, len(datapoints)) + for _, datapoint := range datapoints { + usageDatapoints = append(usageDatapoints, datapoint.Memory) + } + + usageStates := resourcesmonitor.CalculateMemoryUsageStates(monitor, usageDatapoints) + + oldState := monitor.State + newState := resourcesmonitor.NextState(a.Config, oldState, usageStates) + + debouncedUntil, shouldNotify := monitor.Debounce(a.Debounce, a.Clock.Now(), oldState, newState) + + //nolint:gocritic // We need to be able to update the resource monitor here. + err = a.Database.UpdateMemoryResourceMonitor(dbauthz.AsResourceMonitor(ctx), database.UpdateMemoryResourceMonitorParams{ + AgentID: a.AgentID, + State: newState, + UpdatedAt: dbtime.Time(a.Clock.Now()), + DebouncedUntil: dbtime.Time(debouncedUntil), + }) + if err != nil { + return xerrors.Errorf("update workspace monitor: %w", err) + } + + if !shouldNotify { + return nil + } + + workspace, err := a.Database.GetWorkspaceByID(ctx, a.WorkspaceID) + if err != nil { + return xerrors.Errorf("get workspace by id: %w", err) + } + + _, err = a.NotificationsEnqueuer.EnqueueWithData( + // nolint:gocritic // We need to be able to send the notification. + dbauthz.AsNotifier(ctx), + workspace.OwnerID, + notifications.TemplateWorkspaceOutOfMemory, + map[string]string{ + "workspace": workspace.Name, + "threshold": fmt.Sprintf("%d%%", monitor.Threshold), + }, + map[string]any{ + // NOTE(DanielleMaywood): + // When notifications are enqueued, they are checked to be + // unique within a single day. This means that if we attempt + // to send two OOM notifications for the same workspace on + // the same day, the enqueuer will prevent us from sending + // a second one. We are inject a timestamp to make the + // notifications appear different enough to circumvent this + // deduplication logic. + "timestamp": a.Clock.Now(), + }, + "workspace-monitor-memory", + ) + if err != nil { + return xerrors.Errorf("notify workspace OOM: %w", err) + } + + return nil +} + +func (a *ResourcesMonitoringAPI) monitorVolumes(ctx context.Context, datapoints []*proto.PushResourcesMonitoringUsageRequest_Datapoint) error { + volumeMonitors, err := a.Database.FetchVolumesResourceMonitorsByAgentID(ctx, a.AgentID) + if err != nil { + return xerrors.Errorf("get or insert volume monitor: %w", err) + } + + outOfDiskVolumes := make([]map[string]any, 0) + + for _, monitor := range volumeMonitors { + if !monitor.Enabled { + continue + } + + usageDatapoints := make([]*proto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage, 0, len(datapoints)) + for _, datapoint := range datapoints { + var usage *proto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage + + for _, volume := range datapoint.Volumes { + if volume.Volume == monitor.Path { + usage = volume + break + } + } + + usageDatapoints = append(usageDatapoints, usage) + } + + usageStates := resourcesmonitor.CalculateVolumeUsageStates(monitor, usageDatapoints) + + oldState := monitor.State + newState := resourcesmonitor.NextState(a.Config, oldState, usageStates) + + debouncedUntil, shouldNotify := monitor.Debounce(a.Debounce, a.Clock.Now(), oldState, newState) + + if shouldNotify { + outOfDiskVolumes = append(outOfDiskVolumes, map[string]any{ + "path": monitor.Path, + "threshold": fmt.Sprintf("%d%%", monitor.Threshold), + }) + } + + //nolint:gocritic // We need to be able to update the resource monitor here. + if err := a.Database.UpdateVolumeResourceMonitor(dbauthz.AsResourceMonitor(ctx), database.UpdateVolumeResourceMonitorParams{ + AgentID: a.AgentID, + Path: monitor.Path, + State: newState, + UpdatedAt: dbtime.Time(a.Clock.Now()), + DebouncedUntil: dbtime.Time(debouncedUntil), + }); err != nil { + return xerrors.Errorf("update workspace monitor: %w", err) + } + } + + if len(outOfDiskVolumes) == 0 { + return nil + } + + workspace, err := a.Database.GetWorkspaceByID(ctx, a.WorkspaceID) + if err != nil { + return xerrors.Errorf("get workspace by id: %w", err) + } + + if _, err := a.NotificationsEnqueuer.EnqueueWithData( + // nolint:gocritic // We need to be able to send the notification. + dbauthz.AsNotifier(ctx), + workspace.OwnerID, + notifications.TemplateWorkspaceOutOfDisk, + map[string]string{ + "workspace": workspace.Name, + }, + map[string]any{ + "volumes": outOfDiskVolumes, + // NOTE(DanielleMaywood): + // When notifications are enqueued, they are checked to be + // unique within a single day. This means that if we attempt + // to send two OOM notifications for the same workspace on + // the same day, the enqueuer will prevent us from sending + // a second one. We are inject a timestamp to make the + // notifications appear different enough to circumvent this + // deduplication logic. + "timestamp": a.Clock.Now(), + }, + "workspace-monitor-volumes", + ); err != nil { + return xerrors.Errorf("notify workspace OOD: %w", err) + } + + return nil +} diff --git a/coderd/agentapi/resources_monitoring_test.go b/coderd/agentapi/resources_monitoring_test.go new file mode 100644 index 0000000000000..087ccfd24e459 --- /dev/null +++ b/coderd/agentapi/resources_monitoring_test.go @@ -0,0 +1,944 @@ +package agentapi_test + +import ( + "context" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" + + agentproto "github.com/coder/coder/v2/agent/proto" + "github.com/coder/coder/v2/coderd/agentapi" + "github.com/coder/coder/v2/coderd/agentapi/resourcesmonitor" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbgen" + "github.com/coder/coder/v2/coderd/database/dbtestutil" + "github.com/coder/coder/v2/coderd/notifications" + "github.com/coder/coder/v2/coderd/notifications/notificationstest" + "github.com/coder/quartz" +) + +func resourceMonitorAPI(t *testing.T) (*agentapi.ResourcesMonitoringAPI, database.User, *quartz.Mock, *notificationstest.FakeEnqueuer) { + t.Helper() + + db, _ := dbtestutil.NewDB(t) + user := dbgen.User(t, db, database.User{}) + org := dbgen.Organization(t, db, database.Organization{}) + template := dbgen.Template(t, db, database.Template{ + OrganizationID: org.ID, + CreatedBy: user.ID, + }) + templateVersion := dbgen.TemplateVersion(t, db, database.TemplateVersion{ + TemplateID: uuid.NullUUID{Valid: true, UUID: template.ID}, + OrganizationID: org.ID, + CreatedBy: user.ID, + }) + workspace := dbgen.Workspace(t, db, database.WorkspaceTable{ + OrganizationID: org.ID, + TemplateID: template.ID, + OwnerID: user.ID, + }) + job := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{ + Type: database.ProvisionerJobTypeWorkspaceBuild, + }) + build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ + JobID: job.ID, + WorkspaceID: workspace.ID, + TemplateVersionID: templateVersion.ID, + }) + resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ + JobID: build.JobID, + }) + agent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ + ResourceID: resource.ID, + }) + + notifyEnq := ¬ificationstest.FakeEnqueuer{} + clock := quartz.NewMock(t) + + return &agentapi.ResourcesMonitoringAPI{ + AgentID: agent.ID, + WorkspaceID: workspace.ID, + Clock: clock, + Database: db, + NotificationsEnqueuer: notifyEnq, + Config: resourcesmonitor.Config{ + NumDatapoints: 20, + CollectionInterval: 10 * time.Second, + + Alert: resourcesmonitor.AlertConfig{ + MinimumNOKsPercent: 20, + ConsecutiveNOKsPercent: 50, + }, + }, + Debounce: 1 * time.Minute, + }, user, clock, notifyEnq +} + +func TestMemoryResourceMonitorDebounce(t *testing.T) { + t.Parallel() + + // This test is a bit of a long one. We're testing that + // when a monitor goes into an alert state, it doesn't + // allow another notification to occur until after the + // debounce period. + // + // 1. OK -> NOK |> sends a notification + // 2. NOK -> OK |> does nothing + // 3. OK -> NOK |> does nothing due to debounce period + // 4. NOK -> OK |> does nothing + // 5. OK -> NOK |> sends a notification as debounce period exceeded + + api, user, clock, notifyEnq := resourceMonitorAPI(t) + api.Config.Alert.ConsecutiveNOKsPercent = 100 + + // Given: A monitor in an OK state + dbgen.WorkspaceAgentMemoryResourceMonitor(t, api.Database, database.WorkspaceAgentMemoryResourceMonitor{ + AgentID: api.AgentID, + State: database.WorkspaceAgentMonitorStateOK, + Threshold: 80, + }) + + // When: The monitor is given a state that will trigger NOK + _, err := api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ + { + CollectedAt: timestamppb.New(clock.Now()), + Memory: &agentproto.PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage{ + Used: 10, + Total: 10, + }, + }, + }, + }) + require.NoError(t, err) + + // Then: We expect there to be a notification sent + sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfMemory)) + require.Len(t, sent, 1) + require.Equal(t, user.ID, sent[0].UserID) + notifyEnq.Clear() + + // When: The monitor moves to an OK state from NOK + clock.Advance(api.Debounce / 4) + _, err = api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ + { + CollectedAt: timestamppb.New(clock.Now()), + Memory: &agentproto.PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage{ + Used: 1, + Total: 10, + }, + }, + }, + }) + require.NoError(t, err) + + // Then: We expect no new notifications + sent = notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfMemory)) + require.Len(t, sent, 0) + notifyEnq.Clear() + + // When: The monitor moves back to a NOK state before the debounced time. + clock.Advance(api.Debounce / 4) + _, err = api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ + { + CollectedAt: timestamppb.New(clock.Now()), + Memory: &agentproto.PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage{ + Used: 10, + Total: 10, + }, + }, + }, + }) + require.NoError(t, err) + + // Then: We expect no new notifications (showing the debouncer working) + sent = notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfMemory)) + require.Len(t, sent, 0) + notifyEnq.Clear() + + // When: The monitor moves back to an OK state from NOK + clock.Advance(api.Debounce / 4) + _, err = api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ + { + CollectedAt: timestamppb.New(clock.Now()), + Memory: &agentproto.PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage{ + Used: 1, + Total: 10, + }, + }, + }, + }) + require.NoError(t, err) + + // Then: We still expect no new notifications + sent = notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfMemory)) + require.Len(t, sent, 0) + notifyEnq.Clear() + + // When: The monitor moves back to a NOK state after the debounce period. + clock.Advance(api.Debounce/4 + 1*time.Second) + _, err = api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ + { + CollectedAt: timestamppb.New(clock.Now()), + Memory: &agentproto.PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage{ + Used: 10, + Total: 10, + }, + }, + }, + }) + require.NoError(t, err) + + // Then: We expect a notification + sent = notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfMemory)) + require.Len(t, sent, 1) + require.Equal(t, user.ID, sent[0].UserID) +} + +func TestMemoryResourceMonitor(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + memoryUsage []int64 + memoryTotal int64 + previousState database.WorkspaceAgentMonitorState + expectState database.WorkspaceAgentMonitorState + shouldNotify bool + }{ + { + name: "WhenOK/NeverExceedsThreshold", + memoryUsage: []int64{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, + memoryTotal: 10, + previousState: database.WorkspaceAgentMonitorStateOK, + expectState: database.WorkspaceAgentMonitorStateOK, + shouldNotify: false, + }, + { + name: "WhenOK/ShouldStayInOK", + memoryUsage: []int64{9, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, + memoryTotal: 10, + previousState: database.WorkspaceAgentMonitorStateOK, + expectState: database.WorkspaceAgentMonitorStateOK, + shouldNotify: false, + }, + { + name: "WhenOK/ConsecutiveExceedsThreshold", + memoryUsage: []int64{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 8, 9, 8, 9}, + memoryTotal: 10, + previousState: database.WorkspaceAgentMonitorStateOK, + expectState: database.WorkspaceAgentMonitorStateNOK, + shouldNotify: true, + }, + { + name: "WhenOK/MinimumExceedsThreshold", + memoryUsage: []int64{2, 8, 2, 9, 2, 8, 2, 9, 2, 8, 4, 9, 1, 8, 2, 8, 9}, + memoryTotal: 10, + previousState: database.WorkspaceAgentMonitorStateOK, + expectState: database.WorkspaceAgentMonitorStateNOK, + shouldNotify: true, + }, + { + name: "WhenNOK/NeverExceedsThreshold", + memoryUsage: []int64{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, + memoryTotal: 10, + previousState: database.WorkspaceAgentMonitorStateNOK, + expectState: database.WorkspaceAgentMonitorStateOK, + shouldNotify: false, + }, + { + name: "WhenNOK/ShouldStayInNOK", + memoryUsage: []int64{9, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, + memoryTotal: 10, + previousState: database.WorkspaceAgentMonitorStateNOK, + expectState: database.WorkspaceAgentMonitorStateNOK, + shouldNotify: false, + }, + { + name: "WhenNOK/ConsecutiveExceedsThreshold", + memoryUsage: []int64{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 8, 9, 8, 9}, + memoryTotal: 10, + previousState: database.WorkspaceAgentMonitorStateNOK, + expectState: database.WorkspaceAgentMonitorStateNOK, + shouldNotify: false, + }, + { + name: "WhenNOK/MinimumExceedsThreshold", + memoryUsage: []int64{2, 8, 2, 9, 2, 8, 2, 9, 2, 8, 4, 9, 1, 8, 2, 8, 9}, + memoryTotal: 10, + previousState: database.WorkspaceAgentMonitorStateNOK, + expectState: database.WorkspaceAgentMonitorStateNOK, + shouldNotify: false, + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + api, user, clock, notifyEnq := resourceMonitorAPI(t) + + datapoints := make([]*agentproto.PushResourcesMonitoringUsageRequest_Datapoint, 0, len(tt.memoryUsage)) + collectedAt := clock.Now() + for _, usage := range tt.memoryUsage { + collectedAt = collectedAt.Add(15 * time.Second) + datapoints = append(datapoints, &agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ + CollectedAt: timestamppb.New(collectedAt), + Memory: &agentproto.PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage{ + Used: usage, + Total: tt.memoryTotal, + }, + }) + } + + dbgen.WorkspaceAgentMemoryResourceMonitor(t, api.Database, database.WorkspaceAgentMemoryResourceMonitor{ + AgentID: api.AgentID, + State: tt.previousState, + Threshold: 80, + }) + + clock.Set(collectedAt) + _, err := api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: datapoints, + }) + require.NoError(t, err) + + sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfMemory)) + if tt.shouldNotify { + require.Len(t, sent, 1) + require.Equal(t, user.ID, sent[0].UserID) + } else { + require.Len(t, sent, 0) + } + }) + } +} + +func TestMemoryResourceMonitorMissingData(t *testing.T) { + t.Parallel() + + t.Run("UnknownPreventsMovingIntoAlertState", func(t *testing.T) { + t.Parallel() + + api, _, clock, notifyEnq := resourceMonitorAPI(t) + api.Config.Alert.ConsecutiveNOKsPercent = 50 + api.Config.Alert.MinimumNOKsPercent = 100 + + // Given: A monitor in an OK state. + dbgen.WorkspaceAgentMemoryResourceMonitor(t, api.Database, database.WorkspaceAgentMemoryResourceMonitor{ + AgentID: api.AgentID, + State: database.WorkspaceAgentMonitorStateOK, + Threshold: 80, + }) + + // When: A datapoint is missing, surrounded by two NOK datapoints. + _, err := api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ + { + CollectedAt: timestamppb.New(clock.Now()), + Memory: &agentproto.PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage{ + Used: 10, + Total: 10, + }, + }, + { + CollectedAt: timestamppb.New(clock.Now().Add(10 * time.Second)), + Memory: nil, + }, + { + CollectedAt: timestamppb.New(clock.Now().Add(20 * time.Second)), + Memory: &agentproto.PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage{ + Used: 10, + Total: 10, + }, + }, + }, + }) + require.NoError(t, err) + + // Then: We expect no notifications, as this unknown prevents us knowing we should alert. + sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfMemory)) + require.Len(t, sent, 0) + + // Then: We expect the monitor to still be in an OK state. + monitor, err := api.Database.FetchMemoryResourceMonitorsByAgentID(context.Background(), api.AgentID) + require.NoError(t, err) + require.Equal(t, database.WorkspaceAgentMonitorStateOK, monitor.State) + }) + + t.Run("UnknownPreventsMovingOutOfAlertState", func(t *testing.T) { + t.Parallel() + + api, _, clock, _ := resourceMonitorAPI(t) + api.Config.Alert.ConsecutiveNOKsPercent = 50 + api.Config.Alert.MinimumNOKsPercent = 100 + + // Given: A monitor in a NOK state. + dbgen.WorkspaceAgentMemoryResourceMonitor(t, api.Database, database.WorkspaceAgentMemoryResourceMonitor{ + AgentID: api.AgentID, + State: database.WorkspaceAgentMonitorStateNOK, + Threshold: 80, + }) + + // When: A datapoint is missing, surrounded by two OK datapoints. + _, err := api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ + { + CollectedAt: timestamppb.New(clock.Now()), + Memory: &agentproto.PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage{ + Used: 1, + Total: 10, + }, + }, + { + CollectedAt: timestamppb.New(clock.Now().Add(10 * time.Second)), + Memory: nil, + }, + { + CollectedAt: timestamppb.New(clock.Now().Add(20 * time.Second)), + Memory: &agentproto.PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage{ + Used: 1, + Total: 10, + }, + }, + }, + }) + require.NoError(t, err) + + // Then: We expect the monitor to still be in a NOK state. + monitor, err := api.Database.FetchMemoryResourceMonitorsByAgentID(context.Background(), api.AgentID) + require.NoError(t, err) + require.Equal(t, database.WorkspaceAgentMonitorStateNOK, monitor.State) + }) +} + +func TestVolumeResourceMonitorDebounce(t *testing.T) { + t.Parallel() + + // This test is an even longer one. We're testing + // that the debounce logic is independent per + // volume monitor. We interleave the triggering + // of each monitor to ensure the debounce logic + // is monitor independent. + // + // First Monitor: + // 1. OK -> NOK |> sends a notification + // 2. NOK -> OK |> does nothing + // 3. OK -> NOK |> does nothing due to debounce period + // 4. NOK -> OK |> does nothing + // 5. OK -> NOK |> sends a notification as debounce period exceeded + // 6. NOK -> OK |> does nothing + // + // Second Monitor: + // 1. OK -> OK |> does nothing + // 2. OK -> NOK |> sends a notification + // 3. NOK -> OK |> does nothing + // 4. OK -> NOK |> does nothing due to debounce period + // 5. NOK -> OK |> does nothing + // 6. OK -> NOK |> sends a notification as debounce period exceeded + // + + firstVolumePath := "/home/coder" + secondVolumePath := "/dev/coder" + + api, _, clock, notifyEnq := resourceMonitorAPI(t) + + // Given: + // - First monitor in an OK state + // - Second monitor in an OK state + dbgen.WorkspaceAgentVolumeResourceMonitor(t, api.Database, database.WorkspaceAgentVolumeResourceMonitor{ + AgentID: api.AgentID, + Path: firstVolumePath, + State: database.WorkspaceAgentMonitorStateOK, + Threshold: 80, + }) + dbgen.WorkspaceAgentVolumeResourceMonitor(t, api.Database, database.WorkspaceAgentVolumeResourceMonitor{ + AgentID: api.AgentID, + Path: secondVolumePath, + State: database.WorkspaceAgentMonitorStateNOK, + Threshold: 80, + }) + + // When: + // - First monitor is in a NOK state + // - Second monitor is in an OK state + _, err := api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ + { + CollectedAt: timestamppb.New(clock.Now()), + Volumes: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ + {Volume: firstVolumePath, Used: 10, Total: 10}, + {Volume: secondVolumePath, Used: 1, Total: 10}, + }, + }, + }, + }) + require.NoError(t, err) + + // Then: + // - We expect a notification from only the first monitor + sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfDisk)) + require.Len(t, sent, 1) + volumes := requireVolumeData(t, sent[0]) + require.Len(t, volumes, 1) + require.Equal(t, firstVolumePath, volumes[0]["path"]) + notifyEnq.Clear() + + // When: + // - First monitor moves back to OK + // - Second monitor moves to NOK + clock.Advance(api.Debounce / 4) + _, err = api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ + { + CollectedAt: timestamppb.New(clock.Now()), + Volumes: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ + {Volume: firstVolumePath, Used: 1, Total: 10}, + {Volume: secondVolumePath, Used: 10, Total: 10}, + }, + }, + }, + }) + require.NoError(t, err) + + // Then: + // - We expect a notification from only the second monitor + sent = notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfDisk)) + require.Len(t, sent, 1) + volumes = requireVolumeData(t, sent[0]) + require.Len(t, volumes, 1) + require.Equal(t, secondVolumePath, volumes[0]["path"]) + notifyEnq.Clear() + + // When: + // - First monitor moves back to NOK before debounce period has ended + // - Second monitor moves back to OK + clock.Advance(api.Debounce / 4) + _, err = api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ + { + CollectedAt: timestamppb.New(clock.Now()), + Volumes: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ + {Volume: firstVolumePath, Used: 10, Total: 10}, + {Volume: secondVolumePath, Used: 1, Total: 10}, + }, + }, + }, + }) + require.NoError(t, err) + + // Then: + // - We expect no new notifications + sent = notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfDisk)) + require.Len(t, sent, 0) + notifyEnq.Clear() + + // When: + // - First monitor moves back to OK + // - Second monitor moves back to NOK + clock.Advance(api.Debounce / 4) + _, err = api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ + { + CollectedAt: timestamppb.New(clock.Now()), + Volumes: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ + {Volume: firstVolumePath, Used: 1, Total: 10}, + {Volume: secondVolumePath, Used: 10, Total: 10}, + }, + }, + }, + }) + require.NoError(t, err) + + // Then: + // - We expect no new notifications. + sent = notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfDisk)) + require.Len(t, sent, 0) + notifyEnq.Clear() + + // When: + // - First monitor moves back to a NOK state after the debounce period + // - Second monitor moves back to OK + clock.Advance(api.Debounce/4 + 1*time.Second) + _, err = api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ + { + CollectedAt: timestamppb.New(clock.Now()), + Volumes: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ + {Volume: firstVolumePath, Used: 10, Total: 10}, + {Volume: secondVolumePath, Used: 1, Total: 10}, + }, + }, + }, + }) + require.NoError(t, err) + + // Then: + // - We expect a notification from only the first monitor + sent = notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfDisk)) + require.Len(t, sent, 1) + volumes = requireVolumeData(t, sent[0]) + require.Len(t, volumes, 1) + require.Equal(t, firstVolumePath, volumes[0]["path"]) + notifyEnq.Clear() + + // When: + // - First montior moves back to OK + // - Second monitor moves back to NOK after the debounce period + clock.Advance(api.Debounce/4 + 1*time.Second) + _, err = api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ + { + CollectedAt: timestamppb.New(clock.Now()), + Volumes: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ + {Volume: firstVolumePath, Used: 1, Total: 10}, + {Volume: secondVolumePath, Used: 10, Total: 10}, + }, + }, + }, + }) + require.NoError(t, err) + + // Then: + // - We expect a notification from only the second monitor + sent = notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfDisk)) + require.Len(t, sent, 1) + volumes = requireVolumeData(t, sent[0]) + require.Len(t, volumes, 1) + require.Equal(t, secondVolumePath, volumes[0]["path"]) +} + +func TestVolumeResourceMonitor(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + volumePath string + volumeUsage []int64 + volumeTotal int64 + thresholdPercent int32 + previousState database.WorkspaceAgentMonitorState + expectState database.WorkspaceAgentMonitorState + shouldNotify bool + }{ + { + name: "WhenOK/NeverExceedsThreshold", + volumePath: "/home/coder", + volumeUsage: []int64{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, + volumeTotal: 10, + thresholdPercent: 80, + previousState: database.WorkspaceAgentMonitorStateOK, + expectState: database.WorkspaceAgentMonitorStateOK, + shouldNotify: false, + }, + { + name: "WhenOK/ShouldStayInOK", + volumePath: "/home/coder", + volumeUsage: []int64{9, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, + volumeTotal: 10, + thresholdPercent: 80, + previousState: database.WorkspaceAgentMonitorStateOK, + expectState: database.WorkspaceAgentMonitorStateOK, + shouldNotify: false, + }, + { + name: "WhenOK/ConsecutiveExceedsThreshold", + volumePath: "/home/coder", + volumeUsage: []int64{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 8, 9, 8, 9}, + volumeTotal: 10, + thresholdPercent: 80, + previousState: database.WorkspaceAgentMonitorStateOK, + expectState: database.WorkspaceAgentMonitorStateNOK, + shouldNotify: true, + }, + { + name: "WhenOK/MinimumExceedsThreshold", + volumePath: "/home/coder", + volumeUsage: []int64{2, 8, 2, 9, 2, 8, 2, 9, 2, 8, 4, 9, 1, 8, 2, 8, 9}, + volumeTotal: 10, + thresholdPercent: 80, + previousState: database.WorkspaceAgentMonitorStateOK, + expectState: database.WorkspaceAgentMonitorStateNOK, + shouldNotify: true, + }, + { + name: "WhenNOK/NeverExceedsThreshold", + volumePath: "/home/coder", + volumeUsage: []int64{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, + volumeTotal: 10, + thresholdPercent: 80, + previousState: database.WorkspaceAgentMonitorStateNOK, + expectState: database.WorkspaceAgentMonitorStateOK, + shouldNotify: false, + }, + { + name: "WhenNOK/ShouldStayInNOK", + volumePath: "/home/coder", + volumeUsage: []int64{9, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, + volumeTotal: 10, + thresholdPercent: 80, + previousState: database.WorkspaceAgentMonitorStateNOK, + expectState: database.WorkspaceAgentMonitorStateNOK, + shouldNotify: false, + }, + { + name: "WhenNOK/ConsecutiveExceedsThreshold", + volumePath: "/home/coder", + volumeUsage: []int64{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 8, 9, 8, 9}, + volumeTotal: 10, + thresholdPercent: 80, + previousState: database.WorkspaceAgentMonitorStateNOK, + expectState: database.WorkspaceAgentMonitorStateNOK, + shouldNotify: false, + }, + { + name: "WhenNOK/MinimumExceedsThreshold", + volumePath: "/home/coder", + volumeUsage: []int64{2, 8, 2, 9, 2, 8, 2, 9, 2, 8, 4, 9, 1, 8, 2, 8, 9}, + volumeTotal: 10, + thresholdPercent: 80, + previousState: database.WorkspaceAgentMonitorStateNOK, + expectState: database.WorkspaceAgentMonitorStateNOK, + shouldNotify: false, + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + api, user, clock, notifyEnq := resourceMonitorAPI(t) + + datapoints := make([]*agentproto.PushResourcesMonitoringUsageRequest_Datapoint, 0, len(tt.volumeUsage)) + collectedAt := clock.Now() + for _, volumeUsage := range tt.volumeUsage { + collectedAt = collectedAt.Add(15 * time.Second) + + volumeDatapoints := []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ + { + Volume: tt.volumePath, + Used: volumeUsage, + Total: tt.volumeTotal, + }, + } + + datapoints = append(datapoints, &agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ + CollectedAt: timestamppb.New(collectedAt), + Volumes: volumeDatapoints, + }) + } + + dbgen.WorkspaceAgentVolumeResourceMonitor(t, api.Database, database.WorkspaceAgentVolumeResourceMonitor{ + AgentID: api.AgentID, + Path: tt.volumePath, + State: tt.previousState, + Threshold: tt.thresholdPercent, + }) + + clock.Set(collectedAt) + _, err := api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: datapoints, + }) + require.NoError(t, err) + + sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfDisk)) + if tt.shouldNotify { + require.Len(t, sent, 1) + require.Equal(t, user.ID, sent[0].UserID) + } else { + require.Len(t, sent, 0) + } + }) + } +} + +func TestVolumeResourceMonitorMultiple(t *testing.T) { + t.Parallel() + + api, _, clock, notifyEnq := resourceMonitorAPI(t) + api.Config.Alert.ConsecutiveNOKsPercent = 100 + + // Given: two different volume resource monitors + dbgen.WorkspaceAgentVolumeResourceMonitor(t, api.Database, database.WorkspaceAgentVolumeResourceMonitor{ + AgentID: api.AgentID, + Path: "/home/coder", + State: database.WorkspaceAgentMonitorStateOK, + Threshold: 80, + }) + + dbgen.WorkspaceAgentVolumeResourceMonitor(t, api.Database, database.WorkspaceAgentVolumeResourceMonitor{ + AgentID: api.AgentID, + Path: "/dev/coder", + State: database.WorkspaceAgentMonitorStateOK, + Threshold: 80, + }) + + // When: both of them move to a NOK state + _, err := api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ + { + CollectedAt: timestamppb.New(clock.Now()), + Volumes: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ + { + Volume: "/home/coder", + Used: 10, + Total: 10, + }, + { + Volume: "/dev/coder", + Used: 10, + Total: 10, + }, + }, + }, + }, + }) + require.NoError(t, err) + + // Then: We expect a notification to alert with information about both + sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfDisk)) + require.Len(t, sent, 1) + + volumes := requireVolumeData(t, sent[0]) + require.Len(t, volumes, 2) + require.Equal(t, "/home/coder", volumes[0]["path"]) + require.Equal(t, "/dev/coder", volumes[1]["path"]) +} + +func TestVolumeResourceMonitorMissingData(t *testing.T) { + t.Parallel() + + t.Run("UnknownPreventsMovingIntoAlertState", func(t *testing.T) { + t.Parallel() + + volumePath := "/home/coder" + + api, _, clock, notifyEnq := resourceMonitorAPI(t) + api.Config.Alert.ConsecutiveNOKsPercent = 50 + api.Config.Alert.MinimumNOKsPercent = 100 + + // Given: A monitor in an OK state. + dbgen.WorkspaceAgentVolumeResourceMonitor(t, api.Database, database.WorkspaceAgentVolumeResourceMonitor{ + AgentID: api.AgentID, + Path: volumePath, + State: database.WorkspaceAgentMonitorStateOK, + Threshold: 80, + }) + + // When: A datapoint is missing, surrounded by two NOK datapoints. + _, err := api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ + { + CollectedAt: timestamppb.New(clock.Now()), + Volumes: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ + { + Volume: volumePath, + Used: 10, + Total: 10, + }, + }, + }, + { + CollectedAt: timestamppb.New(clock.Now().Add(10 * time.Second)), + Volumes: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{}, + }, + { + CollectedAt: timestamppb.New(clock.Now().Add(20 * time.Second)), + Volumes: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ + { + Volume: volumePath, + Used: 10, + Total: 10, + }, + }, + }, + }, + }) + require.NoError(t, err) + + // Then: We expect no notifications, as this unknown prevents us knowing we should alert. + sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfDisk)) + require.Len(t, sent, 0) + + // Then: We expect the monitor to still be in an OK state. + monitors, err := api.Database.FetchVolumesResourceMonitorsByAgentID(context.Background(), api.AgentID) + require.NoError(t, err) + require.Len(t, monitors, 1) + require.Equal(t, database.WorkspaceAgentMonitorStateOK, monitors[0].State) + }) + + t.Run("UnknownPreventsMovingOutOfAlertState", func(t *testing.T) { + t.Parallel() + + volumePath := "/home/coder" + + api, _, clock, _ := resourceMonitorAPI(t) + api.Config.Alert.ConsecutiveNOKsPercent = 50 + api.Config.Alert.MinimumNOKsPercent = 100 + + // Given: A monitor in a NOK state. + dbgen.WorkspaceAgentVolumeResourceMonitor(t, api.Database, database.WorkspaceAgentVolumeResourceMonitor{ + AgentID: api.AgentID, + Path: volumePath, + State: database.WorkspaceAgentMonitorStateNOK, + Threshold: 80, + }) + + // When: A datapoint is missing, surrounded by two OK datapoints. + _, err := api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ + { + CollectedAt: timestamppb.New(clock.Now()), + Volumes: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ + { + Volume: volumePath, + Used: 1, + Total: 10, + }, + }, + }, + { + CollectedAt: timestamppb.New(clock.Now().Add(10 * time.Second)), + Volumes: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{}, + }, + { + CollectedAt: timestamppb.New(clock.Now().Add(20 * time.Second)), + Volumes: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ + { + Volume: volumePath, + Used: 1, + Total: 10, + }, + }, + }, + }, + }) + require.NoError(t, err) + + // Then: We expect the monitor to still be in a NOK state. + monitors, err := api.Database.FetchVolumesResourceMonitorsByAgentID(context.Background(), api.AgentID) + require.NoError(t, err) + require.Len(t, monitors, 1) + require.Equal(t, database.WorkspaceAgentMonitorStateNOK, monitors[0].State) + }) +} + +func requireVolumeData(t *testing.T, notif *notificationstest.FakeNotification) []map[string]any { + t.Helper() + + volumesData := notif.Data["volumes"] + require.IsType(t, []map[string]any{}, volumesData) + + return volumesData.([]map[string]any) +} diff --git a/coderd/agentapi/resourcesmonitor/resources_monitor.go b/coderd/agentapi/resourcesmonitor/resources_monitor.go new file mode 100644 index 0000000000000..9b1749cd0abd6 --- /dev/null +++ b/coderd/agentapi/resourcesmonitor/resources_monitor.go @@ -0,0 +1,129 @@ +package resourcesmonitor + +import ( + "math" + "time" + + "github.com/coder/coder/v2/agent/proto" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/util/slice" +) + +type State int + +const ( + StateOK State = iota + StateNOK + StateUnknown +) + +type AlertConfig struct { + // What percentage of datapoints in a row are + // required to put the monitor in an alert state. + ConsecutiveNOKsPercent int + + // What percentage of datapoints in a window are + // required to put the monitor in an alert state. + MinimumNOKsPercent int +} + +type Config struct { + // How many datapoints should the agent send + NumDatapoints int32 + + // How long between each datapoint should + // collection occur. + CollectionInterval time.Duration + + Alert AlertConfig +} + +func CalculateMemoryUsageStates( + monitor database.WorkspaceAgentMemoryResourceMonitor, + datapoints []*proto.PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage, +) []State { + states := make([]State, 0, len(datapoints)) + + for _, datapoint := range datapoints { + state := StateUnknown + + if datapoint != nil { + percent := int32(float64(datapoint.Used) / float64(datapoint.Total) * 100) + + if percent < monitor.Threshold { + state = StateOK + } else { + state = StateNOK + } + } + + states = append(states, state) + } + + return states +} + +func CalculateVolumeUsageStates( + monitor database.WorkspaceAgentVolumeResourceMonitor, + datapoints []*proto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage, +) []State { + states := make([]State, 0, len(datapoints)) + + for _, datapoint := range datapoints { + state := StateUnknown + + if datapoint != nil { + percent := int32(float64(datapoint.Used) / float64(datapoint.Total) * 100) + + if percent < monitor.Threshold { + state = StateOK + } else { + state = StateNOK + } + } + + states = append(states, state) + } + + return states +} + +func NextState(c Config, oldState database.WorkspaceAgentMonitorState, states []State) database.WorkspaceAgentMonitorState { + // If there are enough consecutive NOK states, we should be in an + // alert state. + consecutiveNOKs := slice.CountConsecutive(StateNOK, states...) + if percent(consecutiveNOKs, len(states)) >= c.Alert.ConsecutiveNOKsPercent { + return database.WorkspaceAgentMonitorStateNOK + } + + // We do not explicitly handle StateUnknown because it could have + // been either StateOK or StateNOK if collection didn't fail. As + // it could be either, our best bet is to ignore it. + nokCount, okCount := 0, 0 + for _, state := range states { + switch state { + case StateOK: + okCount++ + case StateNOK: + nokCount++ + } + } + + // If there are enough NOK datapoints, we should be in an alert state. + if percent(nokCount, len(states)) >= c.Alert.MinimumNOKsPercent { + return database.WorkspaceAgentMonitorStateNOK + } + + // If all datapoints are OK, we should be in an OK state + if okCount == len(states) { + return database.WorkspaceAgentMonitorStateOK + } + + // Otherwise we stay in the same state as last. + return oldState +} + +func percent[T int](numerator, denominator T) int { + percent := float64(numerator*100) / float64(denominator) + return int(math.Round(percent)) +} diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index f16653c1c834b..125cf4faa5ba1 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -1787,6 +1787,25 @@ const docTemplate = `{ } } }, + "/notifications/test": { + "post": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "tags": [ + "Notifications" + ], + "summary": "Send a test notification", + "operationId": "send-a-test-notification", + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/oauth2-provider/apps": { "get": { "security": [ @@ -2976,6 +2995,43 @@ const docTemplate = `{ "in": "path", "required": true }, + { + "type": "integer", + "description": "Page limit", + "name": "limit", + "in": "query" + }, + { + "type": "array", + "format": "uuid", + "items": { + "type": "string" + }, + "description": "Filter results by job IDs", + "name": "ids", + "in": "query" + }, + { + "enum": [ + "pending", + "running", + "succeeded", + "canceling", + "canceled", + "failed", + "unknown", + "pending", + "running", + "succeeded", + "canceling", + "canceled", + "failed" + ], + "type": "string", + "description": "Filter results by status", + "name": "status", + "in": "query" + }, { "type": "object", "description": "Provisioner tags to filter by (JSON of the form {'tag1':'value1','tag2':'value2'})", @@ -3055,6 +3111,16 @@ const docTemplate = `{ "name": "limit", "in": "query" }, + { + "type": "array", + "format": "uuid", + "items": { + "type": "string" + }, + "description": "Filter results by job IDs", + "name": "ids", + "in": "query" + }, { "enum": [ "pending", @@ -3075,6 +3141,12 @@ const docTemplate = `{ "description": "Filter results by status", "name": "status", "in": "query" + }, + { + "type": "object", + "description": "Provisioner tags to filter by (JSON of the form {'tag1':'value1','tag2':'value2'})", + "name": "tags", + "in": "query" } ], "responses": { @@ -3438,6 +3510,100 @@ const docTemplate = `{ } } }, + "/organizations/{organization}/settings/idpsync/groups/config": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Enterprise" + ], + "summary": "Update group IdP Sync config", + "operationId": "update-group-idp-sync-config", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID or name", + "name": "organization", + "in": "path", + "required": true + }, + { + "description": "New config values", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchGroupIDPSyncConfigRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.GroupSyncSettings" + } + } + } + } + }, + "/organizations/{organization}/settings/idpsync/groups/mapping": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Enterprise" + ], + "summary": "Update group IdP Sync mapping", + "operationId": "update-group-idp-sync-mapping", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID or name", + "name": "organization", + "in": "path", + "required": true + }, + { + "description": "Description of the mappings to add and remove", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchGroupIDPSyncMappingRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.GroupSyncSettings" + } + } + } + } + }, "/organizations/{organization}/settings/idpsync/roles": { "get": { "security": [ @@ -3518,6 +3684,100 @@ const docTemplate = `{ } } }, + "/organizations/{organization}/settings/idpsync/roles/config": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Enterprise" + ], + "summary": "Update role IdP Sync config", + "operationId": "update-role-idp-sync-config", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID or name", + "name": "organization", + "in": "path", + "required": true + }, + { + "description": "New config values", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchRoleIDPSyncConfigRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.RoleSyncSettings" + } + } + } + } + }, + "/organizations/{organization}/settings/idpsync/roles/mapping": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Enterprise" + ], + "summary": "Update role IdP Sync mapping", + "operationId": "update-role-idp-sync-mapping", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID or name", + "name": "organization", + "in": "path", + "required": true + }, + { + "description": "Description of the mappings to add and remove", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchRoleIDPSyncMappingRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.RoleSyncSettings" + } + } + } + } + }, "/organizations/{organization}/templates": { "get": { "security": [ @@ -4248,6 +4508,84 @@ const docTemplate = `{ } } }, + "/settings/idpsync/organization/config": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Enterprise" + ], + "summary": "Update organization IdP Sync config", + "operationId": "update-organization-idp-sync-config", + "parameters": [ + { + "description": "New config values", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchOrganizationIDPSyncConfigRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.OrganizationSyncSettings" + } + } + } + } + }, + "/settings/idpsync/organization/mapping": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Enterprise" + ], + "summary": "Update organization IdP Sync mapping", + "operationId": "update-organization-idp-sync-mapping", + "parameters": [ + { + "description": "Description of the mappings to add and remove", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchOrganizationIDPSyncMappingRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.OrganizationSyncSettings" + } + } + } + } + }, "/tailnet": { "get": { "security": [ @@ -5339,6 +5677,44 @@ const docTemplate = `{ } } }, + "/templateversions/{templateversion}/presets": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Templates" + ], + "summary": "Get template version presets", + "operationId": "get-template-version-presets", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Template version ID", + "name": "templateversion", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.Preset" + } + } + } + } + } + }, "/templateversions/{templateversion}/resources": { "get": { "security": [ @@ -5791,6 +6167,31 @@ const docTemplate = `{ } } }, + "/users/oauth2/github/device": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Get Github device auth.", + "operationId": "get-github-device-auth", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.ExternalAuthDevice" + } + } + } + } + }, "/users/oidc/callback": { "get": { "security": [ @@ -7531,8 +7932,43 @@ const docTemplate = `{ "tags": [ "Agents" ], - "summary": "Get workspace agent by ID", - "operationId": "get-workspace-agent-by-id", + "summary": "Get workspace agent by ID", + "operationId": "get-workspace-agent-by-id", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Workspace agent ID", + "name": "workspaceagent", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.WorkspaceAgent" + } + } + } + } + }, + "/workspaceagents/{workspaceagent}/connection": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Agents" + ], + "summary": "Get connection info for workspace agent", + "operationId": "get-connection-info-for-workspace-agent", "parameters": [ { "type": "string", @@ -7547,13 +7983,13 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.WorkspaceAgent" + "$ref": "#/definitions/workspacesdk.AgentConnectionInfo" } } } } }, - "/workspaceagents/{workspaceagent}/connection": { + "/workspaceagents/{workspaceagent}/containers": { "get": { "security": [ { @@ -7566,8 +8002,8 @@ const docTemplate = `{ "tags": [ "Agents" ], - "summary": "Get connection info for workspace agent", - "operationId": "get-connection-info-for-workspace-agent", + "summary": "Get running containers for workspace agent", + "operationId": "get-running-containers-for-workspace-agent", "parameters": [ { "type": "string", @@ -7576,13 +8012,21 @@ const docTemplate = `{ "name": "workspaceagent", "in": "path", "required": true + }, + { + "type": "string", + "format": "key=value", + "description": "Labels", + "name": "label", + "in": "query", + "required": true } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/workspacesdk.AgentConnectionInfo" + "$ref": "#/definitions/codersdk.WorkspaceAgentListContainersResponse" } } } @@ -9751,7 +10195,11 @@ const docTemplate = `{ "login", "logout", "register", - "request_password_reset" + "request_password_reset", + "connect", + "disconnect", + "open", + "close" ], "x-enum-varnames": [ "AuditActionCreate", @@ -9762,7 +10210,11 @@ const docTemplate = `{ "AuditActionLogin", "AuditActionLogout", "AuditActionRegister", - "AuditActionRequestPasswordReset" + "AuditActionRequestPasswordReset", + "AuditActionConnect", + "AuditActionDisconnect", + "AuditActionOpen", + "AuditActionClose" ] }, "codersdk.AuditDiff": { @@ -9879,7 +10331,7 @@ const docTemplate = `{ "type": "object", "properties": { "github": { - "$ref": "#/definitions/codersdk.AuthMethod" + "$ref": "#/definitions/codersdk.GithubAuthMethod" }, "oidc": { "$ref": "#/definitions/codersdk.OIDCAuthMethod" @@ -10423,6 +10875,10 @@ const docTemplate = `{ "type": "string", "format": "uuid" }, + "request_id": { + "type": "string", + "format": "uuid" + }, "resource_id": { "type": "string", "format": "uuid" @@ -11388,6 +11844,7 @@ const docTemplate = `{ "format": "date-time" }, "public_key": { + "description": "PublicKey is the SSH public key in OpenSSH format.\nExample: \"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID3OmYJvT7q1cF1azbybYy0OZ9yrXfA+M6Lr4vzX5zlp\\n\"\nNote: The key includes a trailing newline (\\n).", "type": "string" }, "updated_at": { @@ -11400,6 +11857,17 @@ const docTemplate = `{ } } }, + "codersdk.GithubAuthMethod": { + "type": "object", + "properties": { + "default_provider_configured": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + } + } + }, "codersdk.Group": { "type": "object", "properties": { @@ -12062,6 +12530,12 @@ const docTemplate = `{ "client_secret": { "type": "string" }, + "default_provider_enable": { + "type": "boolean" + }, + "device_flow": { + "type": "boolean" + }, "enterprise_base_url": { "type": "string" } @@ -12209,6 +12683,7 @@ const docTemplate = `{ "type": "boolean" }, "ignore_user_info": { + "description": "IgnoreUserInfo \u0026 UserInfoFromAccessToken are mutually exclusive. Only 1\ncan be set to true. Ideally this would be an enum with 3 states, ['none',\n'userinfo', 'access_token']. However, for backward compatibility,\n` + "`" + `ignore_user_info` + "`" + ` must remain. And ` + "`" + `access_token` + "`" + ` is a niche, non-spec\ncompliant edge case. So it's use is rare, and should not be advised.", "type": "boolean" }, "issuer_url": { @@ -12241,6 +12716,10 @@ const docTemplate = `{ "skip_issuer_checks": { "type": "boolean" }, + "source_user_info_from_access_token": { + "description": "UserInfoFromAccessToken as mentioned above is an edge case. This allows\nsourcing the user_info from the access token itself instead of a user_info\nendpoint. This assumes the access token is a valid JWT with a set of claims to\nbe merged with the id_token.", + "type": "boolean" + }, "user_role_field": { "type": "string" }, @@ -12391,6 +12870,57 @@ const docTemplate = `{ } } }, + "codersdk.PatchGroupIDPSyncConfigRequest": { + "type": "object", + "properties": { + "auto_create_missing_groups": { + "type": "boolean" + }, + "field": { + "type": "string" + }, + "regex_filter": { + "$ref": "#/definitions/regexp.Regexp" + } + } + }, + "codersdk.PatchGroupIDPSyncMappingRequest": { + "type": "object", + "properties": { + "add": { + "type": "array", + "items": { + "type": "object", + "properties": { + "gets": { + "description": "The ID of the Coder resource the user should be added to", + "type": "string" + }, + "given": { + "description": "The IdP claim the user has", + "type": "string" + } + } + } + }, + "remove": { + "type": "array", + "items": { + "type": "object", + "properties": { + "gets": { + "description": "The ID of the Coder resource the user should be added to", + "type": "string" + }, + "given": { + "description": "The IdP claim the user has", + "type": "string" + } + } + } + } + } + }, "codersdk.PatchGroupRequest": { "type": "object", "properties": { @@ -12420,6 +12950,99 @@ const docTemplate = `{ } } }, + "codersdk.PatchOrganizationIDPSyncConfigRequest": { + "type": "object", + "properties": { + "assign_default": { + "type": "boolean" + }, + "field": { + "type": "string" + } + } + }, + "codersdk.PatchOrganizationIDPSyncMappingRequest": { + "type": "object", + "properties": { + "add": { + "type": "array", + "items": { + "type": "object", + "properties": { + "gets": { + "description": "The ID of the Coder resource the user should be added to", + "type": "string" + }, + "given": { + "description": "The IdP claim the user has", + "type": "string" + } + } + } + }, + "remove": { + "type": "array", + "items": { + "type": "object", + "properties": { + "gets": { + "description": "The ID of the Coder resource the user should be added to", + "type": "string" + }, + "given": { + "description": "The IdP claim the user has", + "type": "string" + } + } + } + } + } + }, + "codersdk.PatchRoleIDPSyncConfigRequest": { + "type": "object", + "properties": { + "field": { + "type": "string" + } + } + }, + "codersdk.PatchRoleIDPSyncMappingRequest": { + "type": "object", + "properties": { + "add": { + "type": "array", + "items": { + "type": "object", + "properties": { + "gets": { + "description": "The ID of the Coder resource the user should be added to", + "type": "string" + }, + "given": { + "description": "The IdP claim the user has", + "type": "string" + } + } + } + }, + "remove": { + "type": "array", + "items": { + "type": "object", + "properties": { + "gets": { + "description": "The ID of the Coder resource the user should be added to", + "type": "string" + }, + "given": { + "description": "The IdP claim the user has", + "type": "string" + } + } + } + } + } + }, "codersdk.PatchTemplateVersionRequest": { "type": "object", "properties": { @@ -12514,6 +13137,34 @@ const docTemplate = `{ } } }, + "codersdk.Preset": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.PresetParameter" + } + } + } + }, + "codersdk.PresetParameter": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, "codersdk.PrometheusConfig": { "type": "object", "properties": { @@ -12653,6 +13304,15 @@ const docTemplate = `{ "$ref": "#/definitions/codersdk.ProvisionerJobStatus" } ] + }, + "template_display_name": { + "type": "string" + }, + "template_icon": { + "type": "string" + }, + "template_name": { + "type": "string" } } }, @@ -12715,6 +13375,9 @@ const docTemplate = `{ "input": { "$ref": "#/definitions/codersdk.ProvisionerJobInput" }, + "metadata": { + "$ref": "#/definitions/codersdk.ProvisionerJobMetadata" + }, "organization_id": { "type": "string", "format": "uuid" @@ -12810,6 +13473,34 @@ const docTemplate = `{ } } }, + "codersdk.ProvisionerJobMetadata": { + "type": "object", + "properties": { + "template_display_name": { + "type": "string" + }, + "template_icon": { + "type": "string" + }, + "template_id": { + "type": "string", + "format": "uuid" + }, + "template_name": { + "type": "string" + }, + "template_version_name": { + "type": "string" + }, + "workspace_id": { + "type": "string", + "format": "uuid" + }, + "workspace_name": { + "type": "string" + } + } + }, "codersdk.ProvisionerJobStatus": { "type": "string", "enum": [ @@ -13008,6 +13699,7 @@ const docTemplate = `{ "read", "read_personal", "ssh", + "unassign", "update", "update_personal", "use", @@ -13023,6 +13715,7 @@ const docTemplate = `{ "ActionRead", "ActionReadPersonal", "ActionSSH", + "ActionUnassign", "ActionUpdate", "ActionUpdatePersonal", "ActionUse", @@ -13058,13 +13751,13 @@ const docTemplate = `{ "organization_member", "provisioner_daemon", "provisioner_jobs", - "provisioner_keys", "replicas", "system", "tailnet_coordinator", "template", "user", "workspace", + "workspace_agent_resource_monitor", "workspace_dormant", "workspace_proxy" ], @@ -13093,13 +13786,13 @@ const docTemplate = `{ "ResourceOrganizationMember", "ResourceProvisionerDaemon", "ResourceProvisionerJobs", - "ResourceProvisionerKeys", "ResourceReplicas", "ResourceSystem", "ResourceTailnetCoordinator", "ResourceTemplate", "ResourceUser", "ResourceWorkspace", + "ResourceWorkspaceAgentResourceMonitor", "ResourceWorkspaceDormant", "ResourceWorkspaceProxy" ] @@ -13303,7 +13996,9 @@ const docTemplate = `{ "notification_template", "idp_sync_settings_organization", "idp_sync_settings_group", - "idp_sync_settings_role" + "idp_sync_settings_role", + "workspace_agent", + "workspace_app" ], "x-enum-varnames": [ "ResourceTypeTemplate", @@ -13327,7 +14022,9 @@ const docTemplate = `{ "ResourceTypeNotificationTemplate", "ResourceTypeIdpSyncSettingsOrganization", "ResourceTypeIdpSyncSettingsGroup", - "ResourceTypeIdpSyncSettingsRole" + "ResourceTypeIdpSyncSettingsRole", + "ResourceTypeWorkspaceAgent", + "ResourceTypeWorkspaceApp" ] }, "codersdk.Response": { @@ -15168,6 +15865,57 @@ const docTemplate = `{ } } }, + "codersdk.WorkspaceAgentDevcontainer": { + "type": "object", + "properties": { + "created_at": { + "description": "CreatedAt is the time the container was created.", + "type": "string", + "format": "date-time" + }, + "id": { + "description": "ID is the unique identifier of the container.", + "type": "string" + }, + "image": { + "description": "Image is the name of the container image.", + "type": "string" + }, + "labels": { + "description": "Labels is a map of key-value pairs of container labels.", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "description": "FriendlyName is the human-readable name of the container.", + "type": "string" + }, + "ports": { + "description": "Ports includes ports exposed by the container.", + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.WorkspaceAgentListeningPort" + } + }, + "running": { + "description": "Running is true if the container is currently running.", + "type": "boolean" + }, + "status": { + "description": "Status is the current status of the container. This is somewhat\nimplementation-dependent, but should generally be a human-readable\nstring.", + "type": "string" + }, + "volumes": { + "description": "Volumes is a map of \"things\" mounted into the container. Again, this\nis somewhat implementation-dependent.", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, "codersdk.WorkspaceAgentHealth": { "type": "object", "properties": { @@ -15208,6 +15956,25 @@ const docTemplate = `{ "WorkspaceAgentLifecycleOff" ] }, + "codersdk.WorkspaceAgentListContainersResponse": { + "type": "object", + "properties": { + "containers": { + "description": "Containers is a list of containers visible to the workspace agent.", + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.WorkspaceAgentDevcontainer" + } + }, + "warnings": { + "description": "Warnings is a list of warnings that may have occurred during the\nprocess of listing containers. This should not include fatal errors.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, "codersdk.WorkspaceAgentListeningPort": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 7859d7ffdc5e5..104d6fd70e077 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -1554,6 +1554,23 @@ } } }, + "/notifications/test": { + "post": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "tags": ["Notifications"], + "summary": "Send a test notification", + "operationId": "send-a-test-notification", + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/oauth2-provider/apps": { "get": { "security": [ @@ -2610,6 +2627,43 @@ "in": "path", "required": true }, + { + "type": "integer", + "description": "Page limit", + "name": "limit", + "in": "query" + }, + { + "type": "array", + "format": "uuid", + "items": { + "type": "string" + }, + "description": "Filter results by job IDs", + "name": "ids", + "in": "query" + }, + { + "enum": [ + "pending", + "running", + "succeeded", + "canceling", + "canceled", + "failed", + "unknown", + "pending", + "running", + "succeeded", + "canceling", + "canceled", + "failed" + ], + "type": "string", + "description": "Filter results by status", + "name": "status", + "in": "query" + }, { "type": "object", "description": "Provisioner tags to filter by (JSON of the form {'tag1':'value1','tag2':'value2'})", @@ -2683,6 +2737,16 @@ "name": "limit", "in": "query" }, + { + "type": "array", + "format": "uuid", + "items": { + "type": "string" + }, + "description": "Filter results by job IDs", + "name": "ids", + "in": "query" + }, { "enum": [ "pending", @@ -2703,6 +2767,12 @@ "description": "Filter results by status", "name": "status", "in": "query" + }, + { + "type": "object", + "description": "Provisioner tags to filter by (JSON of the form {'tag1':'value1','tag2':'value2'})", + "name": "tags", + "in": "query" } ], "responses": { @@ -3030,6 +3100,88 @@ } } }, + "/organizations/{organization}/settings/idpsync/groups/config": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Enterprise"], + "summary": "Update group IdP Sync config", + "operationId": "update-group-idp-sync-config", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID or name", + "name": "organization", + "in": "path", + "required": true + }, + { + "description": "New config values", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchGroupIDPSyncConfigRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.GroupSyncSettings" + } + } + } + } + }, + "/organizations/{organization}/settings/idpsync/groups/mapping": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Enterprise"], + "summary": "Update group IdP Sync mapping", + "operationId": "update-group-idp-sync-mapping", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID or name", + "name": "organization", + "in": "path", + "required": true + }, + { + "description": "Description of the mappings to add and remove", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchGroupIDPSyncMappingRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.GroupSyncSettings" + } + } + } + } + }, "/organizations/{organization}/settings/idpsync/roles": { "get": { "security": [ @@ -3100,6 +3252,88 @@ } } }, + "/organizations/{organization}/settings/idpsync/roles/config": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Enterprise"], + "summary": "Update role IdP Sync config", + "operationId": "update-role-idp-sync-config", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID or name", + "name": "organization", + "in": "path", + "required": true + }, + { + "description": "New config values", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchRoleIDPSyncConfigRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.RoleSyncSettings" + } + } + } + } + }, + "/organizations/{organization}/settings/idpsync/roles/mapping": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Enterprise"], + "summary": "Update role IdP Sync mapping", + "operationId": "update-role-idp-sync-mapping", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID or name", + "name": "organization", + "in": "path", + "required": true + }, + { + "description": "Description of the mappings to add and remove", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchRoleIDPSyncMappingRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.RoleSyncSettings" + } + } + } + } + }, "/organizations/{organization}/templates": { "get": { "security": [ @@ -3744,6 +3978,72 @@ } } }, + "/settings/idpsync/organization/config": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Enterprise"], + "summary": "Update organization IdP Sync config", + "operationId": "update-organization-idp-sync-config", + "parameters": [ + { + "description": "New config values", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchOrganizationIDPSyncConfigRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.OrganizationSyncSettings" + } + } + } + } + }, + "/settings/idpsync/organization/mapping": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Enterprise"], + "summary": "Update organization IdP Sync mapping", + "operationId": "update-organization-idp-sync-mapping", + "parameters": [ + { + "description": "Description of the mappings to add and remove", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchOrganizationIDPSyncMappingRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.OrganizationSyncSettings" + } + } + } + } + }, "/tailnet": { "get": { "security": [ @@ -4721,6 +5021,40 @@ } } }, + "/templateversions/{templateversion}/presets": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Templates"], + "summary": "Get template version presets", + "operationId": "get-template-version-presets", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Template version ID", + "name": "templateversion", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.Preset" + } + } + } + } + } + }, "/templateversions/{templateversion}/resources": { "get": { "security": [ @@ -5115,6 +5449,27 @@ } } }, + "/users/oauth2/github/device": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Users"], + "summary": "Get Github device auth.", + "operationId": "get-github-device-auth", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.ExternalAuthDevice" + } + } + } + } + }, "/users/oidc/callback": { "get": { "security": [ @@ -6647,8 +7002,39 @@ ], "produces": ["application/json"], "tags": ["Agents"], - "summary": "Get workspace agent by ID", - "operationId": "get-workspace-agent-by-id", + "summary": "Get workspace agent by ID", + "operationId": "get-workspace-agent-by-id", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Workspace agent ID", + "name": "workspaceagent", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.WorkspaceAgent" + } + } + } + } + }, + "/workspaceagents/{workspaceagent}/connection": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Agents"], + "summary": "Get connection info for workspace agent", + "operationId": "get-connection-info-for-workspace-agent", "parameters": [ { "type": "string", @@ -6663,13 +7049,13 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.WorkspaceAgent" + "$ref": "#/definitions/workspacesdk.AgentConnectionInfo" } } } } }, - "/workspaceagents/{workspaceagent}/connection": { + "/workspaceagents/{workspaceagent}/containers": { "get": { "security": [ { @@ -6678,8 +7064,8 @@ ], "produces": ["application/json"], "tags": ["Agents"], - "summary": "Get connection info for workspace agent", - "operationId": "get-connection-info-for-workspace-agent", + "summary": "Get running containers for workspace agent", + "operationId": "get-running-containers-for-workspace-agent", "parameters": [ { "type": "string", @@ -6688,13 +7074,21 @@ "name": "workspaceagent", "in": "path", "required": true + }, + { + "type": "string", + "format": "key=value", + "description": "Labels", + "name": "label", + "in": "query", + "required": true } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/workspacesdk.AgentConnectionInfo" + "$ref": "#/definitions/codersdk.WorkspaceAgentListContainersResponse" } } } @@ -8659,7 +9053,11 @@ "login", "logout", "register", - "request_password_reset" + "request_password_reset", + "connect", + "disconnect", + "open", + "close" ], "x-enum-varnames": [ "AuditActionCreate", @@ -8670,7 +9068,11 @@ "AuditActionLogin", "AuditActionLogout", "AuditActionRegister", - "AuditActionRequestPasswordReset" + "AuditActionRequestPasswordReset", + "AuditActionConnect", + "AuditActionDisconnect", + "AuditActionOpen", + "AuditActionClose" ] }, "codersdk.AuditDiff": { @@ -8787,7 +9189,7 @@ "type": "object", "properties": { "github": { - "$ref": "#/definitions/codersdk.AuthMethod" + "$ref": "#/definitions/codersdk.GithubAuthMethod" }, "oidc": { "$ref": "#/definitions/codersdk.OIDCAuthMethod" @@ -9280,6 +9682,10 @@ "type": "string", "format": "uuid" }, + "request_id": { + "type": "string", + "format": "uuid" + }, "resource_id": { "type": "string", "format": "uuid" @@ -10223,6 +10629,7 @@ "format": "date-time" }, "public_key": { + "description": "PublicKey is the SSH public key in OpenSSH format.\nExample: \"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID3OmYJvT7q1cF1azbybYy0OZ9yrXfA+M6Lr4vzX5zlp\\n\"\nNote: The key includes a trailing newline (\\n).", "type": "string" }, "updated_at": { @@ -10235,6 +10642,17 @@ } } }, + "codersdk.GithubAuthMethod": { + "type": "object", + "properties": { + "default_provider_configured": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + } + } + }, "codersdk.Group": { "type": "object", "properties": { @@ -10848,6 +11266,12 @@ "client_secret": { "type": "string" }, + "default_provider_enable": { + "type": "boolean" + }, + "device_flow": { + "type": "boolean" + }, "enterprise_base_url": { "type": "string" } @@ -10995,6 +11419,7 @@ "type": "boolean" }, "ignore_user_info": { + "description": "IgnoreUserInfo \u0026 UserInfoFromAccessToken are mutually exclusive. Only 1\ncan be set to true. Ideally this would be an enum with 3 states, ['none',\n'userinfo', 'access_token']. However, for backward compatibility,\n`ignore_user_info` must remain. And `access_token` is a niche, non-spec\ncompliant edge case. So it's use is rare, and should not be advised.", "type": "boolean" }, "issuer_url": { @@ -11027,6 +11452,10 @@ "skip_issuer_checks": { "type": "boolean" }, + "source_user_info_from_access_token": { + "description": "UserInfoFromAccessToken as mentioned above is an edge case. This allows\nsourcing the user_info from the access token itself instead of a user_info\nendpoint. This assumes the access token is a valid JWT with a set of claims to\nbe merged with the id_token.", + "type": "boolean" + }, "user_role_field": { "type": "string" }, @@ -11172,6 +11601,57 @@ } } }, + "codersdk.PatchGroupIDPSyncConfigRequest": { + "type": "object", + "properties": { + "auto_create_missing_groups": { + "type": "boolean" + }, + "field": { + "type": "string" + }, + "regex_filter": { + "$ref": "#/definitions/regexp.Regexp" + } + } + }, + "codersdk.PatchGroupIDPSyncMappingRequest": { + "type": "object", + "properties": { + "add": { + "type": "array", + "items": { + "type": "object", + "properties": { + "gets": { + "description": "The ID of the Coder resource the user should be added to", + "type": "string" + }, + "given": { + "description": "The IdP claim the user has", + "type": "string" + } + } + } + }, + "remove": { + "type": "array", + "items": { + "type": "object", + "properties": { + "gets": { + "description": "The ID of the Coder resource the user should be added to", + "type": "string" + }, + "given": { + "description": "The IdP claim the user has", + "type": "string" + } + } + } + } + } + }, "codersdk.PatchGroupRequest": { "type": "object", "properties": { @@ -11201,6 +11681,99 @@ } } }, + "codersdk.PatchOrganizationIDPSyncConfigRequest": { + "type": "object", + "properties": { + "assign_default": { + "type": "boolean" + }, + "field": { + "type": "string" + } + } + }, + "codersdk.PatchOrganizationIDPSyncMappingRequest": { + "type": "object", + "properties": { + "add": { + "type": "array", + "items": { + "type": "object", + "properties": { + "gets": { + "description": "The ID of the Coder resource the user should be added to", + "type": "string" + }, + "given": { + "description": "The IdP claim the user has", + "type": "string" + } + } + } + }, + "remove": { + "type": "array", + "items": { + "type": "object", + "properties": { + "gets": { + "description": "The ID of the Coder resource the user should be added to", + "type": "string" + }, + "given": { + "description": "The IdP claim the user has", + "type": "string" + } + } + } + } + } + }, + "codersdk.PatchRoleIDPSyncConfigRequest": { + "type": "object", + "properties": { + "field": { + "type": "string" + } + } + }, + "codersdk.PatchRoleIDPSyncMappingRequest": { + "type": "object", + "properties": { + "add": { + "type": "array", + "items": { + "type": "object", + "properties": { + "gets": { + "description": "The ID of the Coder resource the user should be added to", + "type": "string" + }, + "given": { + "description": "The IdP claim the user has", + "type": "string" + } + } + } + }, + "remove": { + "type": "array", + "items": { + "type": "object", + "properties": { + "gets": { + "description": "The ID of the Coder resource the user should be added to", + "type": "string" + }, + "given": { + "description": "The IdP claim the user has", + "type": "string" + } + } + } + } + } + }, "codersdk.PatchTemplateVersionRequest": { "type": "object", "properties": { @@ -11287,6 +11860,34 @@ } } }, + "codersdk.Preset": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.PresetParameter" + } + } + } + }, + "codersdk.PresetParameter": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, "codersdk.PrometheusConfig": { "type": "object", "properties": { @@ -11422,6 +12023,15 @@ "$ref": "#/definitions/codersdk.ProvisionerJobStatus" } ] + }, + "template_display_name": { + "type": "string" + }, + "template_icon": { + "type": "string" + }, + "template_name": { + "type": "string" } } }, @@ -11478,6 +12088,9 @@ "input": { "$ref": "#/definitions/codersdk.ProvisionerJobInput" }, + "metadata": { + "$ref": "#/definitions/codersdk.ProvisionerJobMetadata" + }, "organization_id": { "type": "string", "format": "uuid" @@ -11567,6 +12180,34 @@ } } }, + "codersdk.ProvisionerJobMetadata": { + "type": "object", + "properties": { + "template_display_name": { + "type": "string" + }, + "template_icon": { + "type": "string" + }, + "template_id": { + "type": "string", + "format": "uuid" + }, + "template_name": { + "type": "string" + }, + "template_version_name": { + "type": "string" + }, + "workspace_id": { + "type": "string", + "format": "uuid" + }, + "workspace_name": { + "type": "string" + } + } + }, "codersdk.ProvisionerJobStatus": { "type": "string", "enum": [ @@ -11747,6 +12388,7 @@ "read", "read_personal", "ssh", + "unassign", "update", "update_personal", "use", @@ -11762,6 +12404,7 @@ "ActionRead", "ActionReadPersonal", "ActionSSH", + "ActionUnassign", "ActionUpdate", "ActionUpdatePersonal", "ActionUse", @@ -11797,13 +12440,13 @@ "organization_member", "provisioner_daemon", "provisioner_jobs", - "provisioner_keys", "replicas", "system", "tailnet_coordinator", "template", "user", "workspace", + "workspace_agent_resource_monitor", "workspace_dormant", "workspace_proxy" ], @@ -11832,13 +12475,13 @@ "ResourceOrganizationMember", "ResourceProvisionerDaemon", "ResourceProvisionerJobs", - "ResourceProvisionerKeys", "ResourceReplicas", "ResourceSystem", "ResourceTailnetCoordinator", "ResourceTemplate", "ResourceUser", "ResourceWorkspace", + "ResourceWorkspaceAgentResourceMonitor", "ResourceWorkspaceDormant", "ResourceWorkspaceProxy" ] @@ -12032,7 +12675,9 @@ "notification_template", "idp_sync_settings_organization", "idp_sync_settings_group", - "idp_sync_settings_role" + "idp_sync_settings_role", + "workspace_agent", + "workspace_app" ], "x-enum-varnames": [ "ResourceTypeTemplate", @@ -12056,7 +12701,9 @@ "ResourceTypeNotificationTemplate", "ResourceTypeIdpSyncSettingsOrganization", "ResourceTypeIdpSyncSettingsGroup", - "ResourceTypeIdpSyncSettingsRole" + "ResourceTypeIdpSyncSettingsRole", + "ResourceTypeWorkspaceAgent", + "ResourceTypeWorkspaceApp" ] }, "codersdk.Response": { @@ -13811,6 +14458,57 @@ } } }, + "codersdk.WorkspaceAgentDevcontainer": { + "type": "object", + "properties": { + "created_at": { + "description": "CreatedAt is the time the container was created.", + "type": "string", + "format": "date-time" + }, + "id": { + "description": "ID is the unique identifier of the container.", + "type": "string" + }, + "image": { + "description": "Image is the name of the container image.", + "type": "string" + }, + "labels": { + "description": "Labels is a map of key-value pairs of container labels.", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "description": "FriendlyName is the human-readable name of the container.", + "type": "string" + }, + "ports": { + "description": "Ports includes ports exposed by the container.", + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.WorkspaceAgentListeningPort" + } + }, + "running": { + "description": "Running is true if the container is currently running.", + "type": "boolean" + }, + "status": { + "description": "Status is the current status of the container. This is somewhat\nimplementation-dependent, but should generally be a human-readable\nstring.", + "type": "string" + }, + "volumes": { + "description": "Volumes is a map of \"things\" mounted into the container. Again, this\nis somewhat implementation-dependent.", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, "codersdk.WorkspaceAgentHealth": { "type": "object", "properties": { @@ -13851,6 +14549,25 @@ "WorkspaceAgentLifecycleOff" ] }, + "codersdk.WorkspaceAgentListContainersResponse": { + "type": "object", + "properties": { + "containers": { + "description": "Containers is a list of containers visible to the workspace agent.", + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.WorkspaceAgentDevcontainer" + } + }, + "warnings": { + "description": "Warnings is a list of warnings that may have occurred during the\nprocess of listing containers. This should not include fatal errors.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, "codersdk.WorkspaceAgentListeningPort": { "type": "object", "properties": { diff --git a/coderd/audit.go b/coderd/audit.go index f764094782a2f..72be70754c2ea 100644 --- a/coderd/audit.go +++ b/coderd/audit.go @@ -159,7 +159,7 @@ func (api *API) generateFakeAuditLog(rw http.ResponseWriter, r *http.Request) { Diff: diff, StatusCode: http.StatusOK, AdditionalFields: params.AdditionalFields, - RequestID: uuid.Nil, // no request ID to attach this to + RequestID: params.RequestID, ResourceIcon: "", OrganizationID: params.OrganizationID, }) diff --git a/coderd/audit/diff.go b/coderd/audit/diff.go index 98e47e91893cb..0a4c35814df0c 100644 --- a/coderd/audit/diff.go +++ b/coderd/audit/diff.go @@ -30,7 +30,9 @@ type Auditable interface { database.NotificationTemplate | idpsync.OrganizationSyncSettings | idpsync.GroupSyncSettings | - idpsync.RoleSyncSettings + idpsync.RoleSyncSettings | + database.WorkspaceAgent | + database.WorkspaceApp } // Map is a map of changed fields in an audited resource. It maps field names to diff --git a/coderd/audit/request.go b/coderd/audit/request.go index 05c18e32fd183..1621c91762435 100644 --- a/coderd/audit/request.go +++ b/coderd/audit/request.go @@ -9,6 +9,7 @@ import ( "net" "net/http" "strconv" + "time" "github.com/google/uuid" "github.com/sqlc-dev/pqtype" @@ -65,6 +66,7 @@ type BackgroundAuditParams[T Auditable] struct { UserID uuid.UUID RequestID uuid.UUID + Time time.Time Status int Action database.AuditAction OrganizationID uuid.UUID @@ -128,6 +130,10 @@ func ResourceTarget[T Auditable](tgt T) string { return "Organization Group Sync" case idpsync.RoleSyncSettings: return "Organization Role Sync" + case database.WorkspaceAgent: + return typed.Name + case database.WorkspaceApp: + return typed.Slug default: panic(fmt.Sprintf("unknown resource %T for ResourceTarget", tgt)) } @@ -187,6 +193,10 @@ func ResourceID[T Auditable](tgt T) uuid.UUID { return noID // Org field on audit log has org id case idpsync.RoleSyncSettings: return noID // Org field on audit log has org id + case database.WorkspaceAgent: + return typed.ID + case database.WorkspaceApp: + return typed.ID default: panic(fmt.Sprintf("unknown resource %T for ResourceID", tgt)) } @@ -238,6 +248,10 @@ func ResourceType[T Auditable](tgt T) database.ResourceType { return database.ResourceTypeIdpSyncSettingsRole case idpsync.GroupSyncSettings: return database.ResourceTypeIdpSyncSettingsGroup + case database.WorkspaceAgent: + return database.ResourceTypeWorkspaceAgent + case database.WorkspaceApp: + return database.ResourceTypeWorkspaceApp default: panic(fmt.Sprintf("unknown resource %T for ResourceType", typed)) } @@ -291,6 +305,10 @@ func ResourceRequiresOrgID[T Auditable]() bool { return true case idpsync.RoleSyncSettings: return true + case database.WorkspaceAgent: + return true + case database.WorkspaceApp: + return true default: panic(fmt.Sprintf("unknown resource %T for ResourceRequiresOrgID", tgt)) } @@ -445,13 +463,19 @@ func BackgroundAudit[T Auditable](ctx context.Context, p *BackgroundAuditParams[ diffRaw = []byte("{}") } + if p.Time.IsZero() { + p.Time = dbtime.Now() + } else { + // NOTE(mafredri): dbtime.Time does not currently enforce UTC. + p.Time = dbtime.Time(p.Time.In(time.UTC)) + } if p.AdditionalFields == nil { p.AdditionalFields = json.RawMessage("{}") } auditLog := database.AuditLog{ ID: uuid.New(), - Time: dbtime.Now(), + Time: p.Time, UserID: p.UserID, OrganizationID: requireOrgID[T](ctx, p.OrganizationID, p.Log), Ip: ip, diff --git a/coderd/audit_test.go b/coderd/audit_test.go index 922e2b359b506..18bcd78b38807 100644 --- a/coderd/audit_test.go +++ b/coderd/audit_test.go @@ -17,6 +17,8 @@ import ( "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/provisioner/echo" + "github.com/coder/coder/v2/provisionersdk/proto" ) func TestAuditLogs(t *testing.T) { @@ -30,7 +32,8 @@ func TestAuditLogs(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) err := client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ - ResourceID: user.UserID, + ResourceID: user.UserID, + OrganizationID: user.OrganizationID, }) require.NoError(t, err) @@ -54,7 +57,8 @@ func TestAuditLogs(t *testing.T) { client2, user2 := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleOwner()) err := client2.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ - ResourceID: user2.ID, + ResourceID: user2.ID, + OrganizationID: user.OrganizationID, }) require.NoError(t, err) @@ -123,6 +127,7 @@ func TestAuditLogs(t *testing.T) { ResourceType: codersdk.ResourceTypeWorkspaceBuild, ResourceID: workspace.LatestBuild.ID, AdditionalFields: wriBytes, + OrganizationID: user.OrganizationID, }) require.NoError(t, err) @@ -158,7 +163,8 @@ func TestAuditLogs(t *testing.T) { // Add an extra audit log in another organization err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ - ResourceID: owner.UserID, + ResourceID: owner.UserID, + OrganizationID: uuid.New(), }) require.NoError(t, err) @@ -229,53 +235,102 @@ func TestAuditLogsFilter(t *testing.T) { ctx = context.Background() client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) user = coderdtest.CreateFirstUser(t, client) - version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, completeWithAgentAndApp()) template = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) ) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) workspace := coderdtest.CreateWorkspace(t, client, template.ID) + workspace.LatestBuild = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) // Create two logs with "Create" err := client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ - Action: codersdk.AuditActionCreate, - ResourceType: codersdk.ResourceTypeTemplate, - ResourceID: template.ID, - Time: time.Date(2022, 8, 15, 14, 30, 45, 100, time.UTC), // 2022-8-15 14:30:45 + OrganizationID: user.OrganizationID, + Action: codersdk.AuditActionCreate, + ResourceType: codersdk.ResourceTypeTemplate, + ResourceID: template.ID, + Time: time.Date(2022, 8, 15, 14, 30, 45, 100, time.UTC), // 2022-8-15 14:30:45 }) require.NoError(t, err) err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ - Action: codersdk.AuditActionCreate, - ResourceType: codersdk.ResourceTypeUser, - ResourceID: user.UserID, - Time: time.Date(2022, 8, 16, 14, 30, 45, 100, time.UTC), // 2022-8-16 14:30:45 + OrganizationID: user.OrganizationID, + Action: codersdk.AuditActionCreate, + ResourceType: codersdk.ResourceTypeUser, + ResourceID: user.UserID, + Time: time.Date(2022, 8, 16, 14, 30, 45, 100, time.UTC), // 2022-8-16 14:30:45 }) require.NoError(t, err) // Create one log with "Delete" err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ - Action: codersdk.AuditActionDelete, - ResourceType: codersdk.ResourceTypeUser, - ResourceID: user.UserID, - Time: time.Date(2022, 8, 15, 14, 30, 45, 100, time.UTC), // 2022-8-15 14:30:45 + OrganizationID: user.OrganizationID, + Action: codersdk.AuditActionDelete, + ResourceType: codersdk.ResourceTypeUser, + ResourceID: user.UserID, + Time: time.Date(2022, 8, 15, 14, 30, 45, 100, time.UTC), // 2022-8-15 14:30:45 }) require.NoError(t, err) // Create one log with "Start" err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ - Action: codersdk.AuditActionStart, - ResourceType: codersdk.ResourceTypeWorkspaceBuild, - ResourceID: workspace.LatestBuild.ID, - Time: time.Date(2022, 8, 15, 14, 30, 45, 100, time.UTC), // 2022-8-15 14:30:45 + OrganizationID: user.OrganizationID, + Action: codersdk.AuditActionStart, + ResourceType: codersdk.ResourceTypeWorkspaceBuild, + ResourceID: workspace.LatestBuild.ID, + Time: time.Date(2022, 8, 15, 14, 30, 45, 100, time.UTC), // 2022-8-15 14:30:45 }) require.NoError(t, err) // Create one log with "Stop" err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ - Action: codersdk.AuditActionStop, - ResourceType: codersdk.ResourceTypeWorkspaceBuild, - ResourceID: workspace.LatestBuild.ID, - Time: time.Date(2022, 8, 15, 14, 30, 45, 100, time.UTC), // 2022-8-15 14:30:45 + OrganizationID: user.OrganizationID, + Action: codersdk.AuditActionStop, + ResourceType: codersdk.ResourceTypeWorkspaceBuild, + ResourceID: workspace.LatestBuild.ID, + Time: time.Date(2022, 8, 15, 14, 30, 45, 100, time.UTC), // 2022-8-15 14:30:45 + }) + require.NoError(t, err) + + // Create one log with "Connect" and "Disconect". + connectRequestID := uuid.New() + err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ + OrganizationID: user.OrganizationID, + Action: codersdk.AuditActionConnect, + RequestID: connectRequestID, + ResourceType: codersdk.ResourceTypeWorkspaceAgent, + ResourceID: workspace.LatestBuild.Resources[0].Agents[0].ID, + Time: time.Date(2022, 8, 15, 14, 30, 45, 100, time.UTC), // 2022-8-15 14:30:45 + }) + require.NoError(t, err) + + err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ + OrganizationID: user.OrganizationID, + Action: codersdk.AuditActionDisconnect, + RequestID: connectRequestID, + ResourceType: codersdk.ResourceTypeWorkspaceAgent, + ResourceID: workspace.LatestBuild.Resources[0].Agents[0].ID, + Time: time.Date(2022, 8, 15, 14, 35, 0o0, 100, time.UTC), // 2022-8-15 14:35:00 + }) + require.NoError(t, err) + + // Create one log with "Open" and "Close". + openRequestID := uuid.New() + err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ + OrganizationID: user.OrganizationID, + Action: codersdk.AuditActionOpen, + RequestID: openRequestID, + ResourceType: codersdk.ResourceTypeWorkspaceApp, + ResourceID: workspace.LatestBuild.Resources[0].Agents[0].Apps[0].ID, + Time: time.Date(2022, 8, 15, 14, 30, 45, 100, time.UTC), // 2022-8-15 14:30:45 + }) + require.NoError(t, err) + err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ + OrganizationID: user.OrganizationID, + Action: codersdk.AuditActionClose, + RequestID: openRequestID, + ResourceType: codersdk.ResourceTypeWorkspaceApp, + ResourceID: workspace.LatestBuild.Resources[0].Agents[0].Apps[0].ID, + Time: time.Date(2022, 8, 15, 14, 35, 0o0, 100, time.UTC), // 2022-8-15 14:35:00 }) require.NoError(t, err) @@ -309,12 +364,12 @@ func TestAuditLogsFilter(t *testing.T) { { Name: "FilterByEmail", SearchQuery: "email:" + coderdtest.FirstUserParams.Email, - ExpectedResult: 5, + ExpectedResult: 9, }, { Name: "FilterByUsername", SearchQuery: "username:" + coderdtest.FirstUserParams.Username, - ExpectedResult: 5, + ExpectedResult: 9, }, { Name: "FilterByResourceID", @@ -366,6 +421,36 @@ func TestAuditLogsFilter(t *testing.T) { SearchQuery: "resource_type:workspace_build action:start build_reason:initiator", ExpectedResult: 1, }, + { + Name: "FilterOnWorkspaceAgentConnect", + SearchQuery: "resource_type:workspace_agent action:connect", + ExpectedResult: 1, + }, + { + Name: "FilterOnWorkspaceAgentDisconnect", + SearchQuery: "resource_type:workspace_agent action:disconnect", + ExpectedResult: 1, + }, + { + Name: "FilterOnWorkspaceAgentConnectionRequestID", + SearchQuery: "resource_type:workspace_agent request_id:" + connectRequestID.String(), + ExpectedResult: 2, + }, + { + Name: "FilterOnWorkspaceAppOpen", + SearchQuery: "resource_type:workspace_app action:open", + ExpectedResult: 1, + }, + { + Name: "FilterOnWorkspaceAppClose", + SearchQuery: "resource_type:workspace_app action:close", + ExpectedResult: 1, + }, + { + Name: "FilterOnWorkspaceAppOpenRequestID", + SearchQuery: "resource_type:workspace_app request_id:" + openRequestID.String(), + ExpectedResult: 2, + }, } for _, testCase := range testCases { @@ -387,3 +472,63 @@ func TestAuditLogsFilter(t *testing.T) { } }) } + +func completeWithAgentAndApp() *echo.Responses { + return &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionPlan: []*proto.Response{ + { + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{ + Resources: []*proto.Resource{ + { + Type: "compute", + Name: "main", + Agents: []*proto.Agent{ + { + Name: "smith", + OperatingSystem: "linux", + Architecture: "i386", + Apps: []*proto.App{ + { + Slug: "app", + DisplayName: "App", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + ProvisionApply: []*proto.Response{ + { + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ + Resources: []*proto.Resource{ + { + Type: "compute", + Name: "main", + Agents: []*proto.Agent{ + { + Name: "smith", + OperatingSystem: "linux", + Architecture: "i386", + Apps: []*proto.App{ + { + Slug: "app", + DisplayName: "App", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } +} diff --git a/coderd/coderd.go b/coderd/coderd.go index e273b7afdb80f..1cb4c0592b66e 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -585,6 +585,8 @@ func New(options *Options) *API { AppearanceFetcher: &api.AppearanceFetcher, BuildInfo: buildInfo, Entitlements: options.Entitlements, + Telemetry: options.Telemetry, + Logger: options.Logger.Named("site"), }) api.SiteHandler.Experiments.Store(&experiments) @@ -786,6 +788,7 @@ func New(options *Options) *API { httpmw.AttachRequestID, httpmw.ExtractRealIP(api.RealIPConfig), httpmw.Logger(api.Logger), + singleSlashMW, rolestore.CustomRoleMW, prometheusMW, // Build-Version is helpful for debugging. @@ -927,6 +930,25 @@ func New(options *Options) *API { r.Route("/audit", func(r chi.Router) { r.Use( apiKeyMiddleware, + // This middleware only checks the site and orgs for the audit_log read + // permission. + // In the future if it makes sense to have this permission on the user as + // well we will need to update this middleware to include that check. + func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + if api.Authorize(r, policy.ActionRead, rbac.ResourceAuditLog) { + next.ServeHTTP(rw, r) + return + } + + if api.Authorize(r, policy.ActionRead, rbac.ResourceAuditLog.AnyOrganization()) { + next.ServeHTTP(rw, r) + return + } + + httpapi.Forbidden(rw) + }) + }, ) r.Get("/", api.auditLogs) @@ -1055,6 +1077,7 @@ func New(options *Options) *API { r.Get("/rich-parameters", api.templateVersionRichParameters) r.Get("/external-auth", api.templateVersionExternalAuth) r.Get("/variables", api.templateVersionVariables) + r.Get("/presets", api.templateVersionPresets) r.Get("/resources", api.templateVersionResources) r.Get("/logs", api.templateVersionLogs) r.Route("/dry-run", func(r chi.Router) { @@ -1083,6 +1106,7 @@ func New(options *Options) *API { r.Post("/validate-password", api.validateUserPassword) r.Post("/otp/change-password", api.postChangePasswordWithOneTimePasscode) r.Route("/oauth2", func(r chi.Router) { + r.Get("/github/device", api.userOAuth2GithubDevice) r.Route("/github", func(r chi.Router) { r.Use( httpmw.ExtractOAuth2(options.GithubOAuth2Config, options.HTTPClient, nil), @@ -1209,6 +1233,7 @@ func New(options *Options) *API { r.Get("/logs", api.workspaceAgentLogs) r.Get("/listening-ports", api.workspaceAgentListeningPorts) r.Get("/connection", api.workspaceAgentConnection) + r.Get("/containers", api.workspaceAgentListContainers) r.Get("/coordinate", api.workspaceAgentClientCoordinate) // PTY is part of workspaceAppServer. @@ -1299,7 +1324,7 @@ func New(options *Options) *API { func(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { if !api.Authorize(r, policy.ActionRead, rbac.ResourceDebugInfo) { - httpapi.ResourceNotFound(rw) + httpapi.Forbidden(rw) return } @@ -1365,6 +1390,7 @@ func New(options *Options) *API { r.Get("/system", api.systemNotificationTemplates) }) r.Get("/dispatch-methods", api.notificationDispatchMethods) + r.Post("/test", api.postTestNotification) }) r.Route("/tailnet", func(r chi.Router) { r.Use(apiKeyMiddleware) @@ -1728,3 +1754,31 @@ func ReadExperiments(log slog.Logger, raw []string) codersdk.Experiments { } return exps } + +var multipleSlashesRe = regexp.MustCompile(`/+`) + +func singleSlashMW(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + var path string + rctx := chi.RouteContext(r.Context()) + if rctx != nil && rctx.RoutePath != "" { + path = rctx.RoutePath + } else { + path = r.URL.Path + } + + // Normalize multiple slashes to a single slash + newPath := multipleSlashesRe.ReplaceAllString(path, "/") + + // Apply the cleaned path + // The approach is consistent with: https://github.com/go-chi/chi/blob/e846b8304c769c4f1a51c9de06bebfaa4576bd88/middleware/strip.go#L24-L28 + if rctx != nil { + rctx.RoutePath = newPath + } else { + r.URL.Path = newPath + } + + next.ServeHTTP(w, r) + } + return http.HandlerFunc(fn) +} diff --git a/coderd/coderd_internal_test.go b/coderd/coderd_internal_test.go new file mode 100644 index 0000000000000..34f5738bf90a0 --- /dev/null +++ b/coderd/coderd_internal_test.go @@ -0,0 +1,69 @@ +package coderd + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-chi/chi/v5" + "github.com/stretchr/testify/assert" +) + +func TestStripSlashesMW(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + inputPath string + wantPath string + }{ + {"No changes", "/api/v1/buildinfo", "/api/v1/buildinfo"}, + {"Double slashes", "/api//v2//buildinfo", "/api/v2/buildinfo"}, + {"Triple slashes", "/api///v2///buildinfo", "/api/v2/buildinfo"}, + {"Leading slashes", "///api/v2/buildinfo", "/api/v2/buildinfo"}, + {"Root path", "/", "/"}, + {"Double slashes root", "//", "/"}, + {"Only slashes", "/////", "/"}, + } + + handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + for _, tt := range tests { + tt := tt + + t.Run("chi/"+tt.name, func(t *testing.T) { + t.Parallel() + req := httptest.NewRequest("GET", tt.inputPath, nil) + rec := httptest.NewRecorder() + + // given + rctx := chi.NewRouteContext() + rctx.RoutePath = tt.inputPath + req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) + + // when + singleSlashMW(handler).ServeHTTP(rec, req) + updatedCtx := chi.RouteContext(req.Context()) + + // then + assert.Equal(t, tt.inputPath, req.URL.Path) + assert.Equal(t, tt.wantPath, updatedCtx.RoutePath) + }) + + t.Run("stdlib/"+tt.name, func(t *testing.T) { + t.Parallel() + req := httptest.NewRequest("GET", tt.inputPath, nil) + rec := httptest.NewRecorder() + + // when + singleSlashMW(handler).ServeHTTP(rec, req) + + // then + assert.Equal(t, tt.wantPath, req.URL.Path) + assert.Nil(t, chi.RouteContext(req.Context())) + }) + } +} diff --git a/coderd/coderdtest/oidctest/idp.go b/coderd/coderdtest/oidctest/idp.go index d6c7e6259f760..e0fd1bb9b0be2 100644 --- a/coderd/coderdtest/oidctest/idp.go +++ b/coderd/coderdtest/oidctest/idp.go @@ -105,6 +105,7 @@ type FakeIDP struct { // "Authorized Redirect URLs". This can be used to emulate that. hookValidRedirectURL func(redirectURL string) error hookUserInfo func(email string) (jwt.MapClaims, error) + hookAccessTokenJWT func(email string, exp time.Time) jwt.MapClaims // defaultIDClaims is if a new client connects and we didn't preset // some claims. defaultIDClaims jwt.MapClaims @@ -154,6 +155,12 @@ func WithMiddlewares(mws ...func(http.Handler) http.Handler) func(*FakeIDP) { } } +func WithAccessTokenJWTHook(hook func(email string, exp time.Time) jwt.MapClaims) func(*FakeIDP) { + return func(f *FakeIDP) { + f.hookAccessTokenJWT = hook + } +} + func WithHookWellKnown(hook func(r *http.Request, j *ProviderJSON) error) func(*FakeIDP) { return func(f *FakeIDP) { f.hookWellKnown = hook @@ -316,8 +323,7 @@ const ( func NewFakeIDP(t testing.TB, opts ...FakeIDPOpt) *FakeIDP { t.Helper() - block, _ := pem.Decode([]byte(testRSAPrivateKey)) - pkey, err := x509.ParsePKCS1PrivateKey(block.Bytes) + pkey, err := FakeIDPKey() require.NoError(t, err) idp := &FakeIDP{ @@ -676,8 +682,13 @@ func (f *FakeIDP) newCode(state string) string { // newToken enforces the access token exchanged is actually a valid access token // created by the IDP. -func (f *FakeIDP) newToken(email string, expires time.Time) string { +func (f *FakeIDP) newToken(t testing.TB, email string, expires time.Time) string { accessToken := uuid.NewString() + if f.hookAccessTokenJWT != nil { + claims := f.hookAccessTokenJWT(email, expires) + accessToken = f.encodeClaims(t, claims) + } + f.accessTokens.Store(accessToken, token{ issued: time.Now(), email: email, @@ -963,7 +974,7 @@ func (f *FakeIDP) httpHandler(t testing.TB) http.Handler { email := getEmail(claims) refreshToken := f.newRefreshTokens(email) token := map[string]interface{}{ - "access_token": f.newToken(email, exp), + "access_token": f.newToken(t, email, exp), "refresh_token": refreshToken, "token_type": "Bearer", "expires_in": int64((f.defaultExpire).Seconds()), @@ -1465,9 +1476,10 @@ func (f *FakeIDP) internalOIDCConfig(ctx context.Context, t testing.TB, scopes [ Verifier: oidc.NewVerifier(f.provider.Issuer, &oidc.StaticKeySet{ PublicKeys: []crypto.PublicKey{f.key.Public()}, }, verifierConfig), - UsernameField: "preferred_username", - EmailField: "email", - AuthURLParams: map[string]string{"access_type": "offline"}, + UsernameField: "preferred_username", + EmailField: "email", + AuthURLParams: map[string]string{"access_type": "offline"}, + SecondaryClaims: coderd.MergedClaimsSourceUserInfo, } for _, opt := range opts { @@ -1552,3 +1564,8 @@ d8h4Ht09E+f3nhTEc87mODkl7WJZpHL6V2sORfeq/eIkds+H6CJ4hy5w/bSw8tjf sz9Di8sGIaUbLZI2rd0CQQCzlVwEtRtoNCyMJTTrkgUuNufLP19RZ5FpyXxBO5/u QastnN77KfUwdj3SJt44U/uh1jAIv4oSLBr8HYUkbnI8 -----END RSA PRIVATE KEY-----` + +func FakeIDPKey() (*rsa.PrivateKey, error) { + block, _ := pem.Decode([]byte(testRSAPrivateKey)) + return x509.ParsePKCS1PrivateKey(block.Bytes) +} diff --git a/coderd/cryptokeys/rotate.go b/coderd/cryptokeys/rotate.go index 26256b4cd4c12..24e764a015dd0 100644 --- a/coderd/cryptokeys/rotate.go +++ b/coderd/cryptokeys/rotate.go @@ -152,7 +152,7 @@ func (k *rotator) rotateKeys(ctx context.Context) error { } } if validKeys == 0 { - k.logger.Info(ctx, "no valid keys detected, inserting new key", + k.logger.Debug(ctx, "no valid keys detected, inserting new key", slog.F("feature", feature), ) _, err := k.insertNewKey(ctx, tx, feature, now) @@ -194,7 +194,7 @@ func (k *rotator) insertNewKey(ctx context.Context, tx database.Store, feature d return database.CryptoKey{}, xerrors.Errorf("inserting new key: %w", err) } - k.logger.Info(ctx, "inserted new key for feature", slog.F("feature", feature)) + k.logger.Debug(ctx, "inserted new key for feature", slog.F("feature", feature)) return newKey, nil } diff --git a/coderd/database/db.go b/coderd/database/db.go index 0f923a861efb4..23ee5028e3a12 100644 --- a/coderd/database/db.go +++ b/coderd/database/db.go @@ -3,9 +3,8 @@ // Query functions are generated using sqlc. // // To modify the database schema: -// 1. Add a new migration using "create_migration.sh" in database/migrations/ -// 2. Run "make coderd/database/generate" in the root to generate models. -// 3. Add/Edit queries in "query.sql" and run "make coderd/database/generate" to create Go code. +// 1. Add a new migration using "create_migration.sh" in database/migrations/ and run "make gen" to generate models. +// 2. Add/Edit queries in "query.sql" and run "make gen" to create Go code. package database import ( diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go index 8d2a75960bd0e..2249e0c9f32ec 100644 --- a/coderd/database/db2sdk/db2sdk.go +++ b/coderd/database/db2sdk/db2sdk.go @@ -15,6 +15,7 @@ import ( "golang.org/x/xerrors" "tailscale.com/tailcfg" + agentproto "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" @@ -705,3 +706,26 @@ func TemplateRoleActions(role codersdk.TemplateRole) []policy.Action { } return []policy.Action{} } + +func AuditActionFromAgentProtoConnectionAction(action agentproto.Connection_Action) (database.AuditAction, error) { + switch action { + case agentproto.Connection_CONNECT: + return database.AuditActionConnect, nil + case agentproto.Connection_DISCONNECT: + return database.AuditActionDisconnect, nil + default: + // Also Connection_ACTION_UNSPECIFIED, no mapping. + return "", xerrors.Errorf("unknown agent connection action %q", action) + } +} + +func AgentProtoConnectionActionToAuditAction(action database.AuditAction) (agentproto.Connection_Action, error) { + switch action { + case database.AuditActionConnect: + return agentproto.Connection_CONNECT, nil + case database.AuditActionDisconnect: + return agentproto.Connection_DISCONNECT, nil + default: + return agentproto.Connection_ACTION_UNSPECIFIED, xerrors.Errorf("unknown agent connection action %q", action) + } +} diff --git a/coderd/database/db_test.go b/coderd/database/db_test.go index b4580527c843a..68b60a788fd3d 100644 --- a/coderd/database/db_test.go +++ b/coderd/database/db_test.go @@ -1,5 +1,3 @@ -//go:build linux - package database_test import ( diff --git a/coderd/database/dbauthz/customroles_test.go b/coderd/database/dbauthz/customroles_test.go index c5d40b0323185..815d6629f64f9 100644 --- a/coderd/database/dbauthz/customroles_test.go +++ b/coderd/database/dbauthz/customroles_test.go @@ -34,11 +34,12 @@ func TestInsertCustomRoles(t *testing.T) { } } - canAssignRole := rbac.Role{ + canCreateCustomRole := rbac.Role{ Identifier: rbac.RoleIdentifier{Name: "can-assign"}, DisplayName: "", Site: rbac.Permissions(map[string][]policy.Action{ - rbac.ResourceAssignRole.Type: {policy.ActionRead, policy.ActionCreate}, + rbac.ResourceAssignRole.Type: {policy.ActionRead}, + rbac.ResourceAssignOrgRole.Type: {policy.ActionRead, policy.ActionCreate}, }), } @@ -61,17 +62,15 @@ func TestInsertCustomRoles(t *testing.T) { return all } - orgID := uuid.NullUUID{ - UUID: uuid.New(), - Valid: true, - } + orgID := uuid.New() + testCases := []struct { name string subject rbac.ExpandableRoles // Perms to create on new custom role - organizationID uuid.NullUUID + organizationID uuid.UUID site []codersdk.Permission org []codersdk.Permission user []codersdk.Permission @@ -79,19 +78,21 @@ func TestInsertCustomRoles(t *testing.T) { }{ { // No roles, so no assign role - name: "no-roles", - subject: rbac.RoleIdentifiers{}, - errorContains: "forbidden", + name: "no-roles", + organizationID: orgID, + subject: rbac.RoleIdentifiers{}, + errorContains: "forbidden", }, { // This works because the new role has 0 perms - name: "empty", - subject: merge(canAssignRole), + name: "empty", + organizationID: orgID, + subject: merge(canCreateCustomRole), }, { name: "mixed-scopes", - subject: merge(canAssignRole, rbac.RoleOwner()), organizationID: orgID, + subject: merge(canCreateCustomRole, rbac.RoleOwner()), site: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{ codersdk.ResourceWorkspace: {codersdk.ActionRead}, }), @@ -101,27 +102,30 @@ func TestInsertCustomRoles(t *testing.T) { errorContains: "organization roles specify site or user permissions", }, { - name: "invalid-action", - subject: merge(canAssignRole, rbac.RoleOwner()), - site: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{ + name: "invalid-action", + organizationID: orgID, + subject: merge(canCreateCustomRole, rbac.RoleOwner()), + org: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{ // Action does not go with resource codersdk.ResourceWorkspace: {codersdk.ActionViewInsights}, }), errorContains: "invalid action", }, { - name: "invalid-resource", - subject: merge(canAssignRole, rbac.RoleOwner()), - site: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{ + name: "invalid-resource", + organizationID: orgID, + subject: merge(canCreateCustomRole, rbac.RoleOwner()), + org: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{ "foobar": {codersdk.ActionViewInsights}, }), errorContains: "invalid resource", }, { // Not allowing these at this time. - name: "negative-permission", - subject: merge(canAssignRole, rbac.RoleOwner()), - site: []codersdk.Permission{ + name: "negative-permission", + organizationID: orgID, + subject: merge(canCreateCustomRole, rbac.RoleOwner()), + org: []codersdk.Permission{ { Negate: true, ResourceType: codersdk.ResourceWorkspace, @@ -131,89 +135,69 @@ func TestInsertCustomRoles(t *testing.T) { errorContains: "no negative permissions", }, { - name: "wildcard", // not allowed - subject: merge(canAssignRole, rbac.RoleOwner()), - site: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{ + name: "wildcard", // not allowed + organizationID: orgID, + subject: merge(canCreateCustomRole, rbac.RoleOwner()), + org: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{ codersdk.ResourceWorkspace: {"*"}, }), errorContains: "no wildcard symbols", }, // escalation checks { - name: "read-workspace-escalation", - subject: merge(canAssignRole), - site: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{ + name: "read-workspace-escalation", + organizationID: orgID, + subject: merge(canCreateCustomRole), + org: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{ codersdk.ResourceWorkspace: {codersdk.ActionRead}, }), errorContains: "not allowed to grant this permission", }, { - name: "read-workspace-outside-org", - organizationID: uuid.NullUUID{ - UUID: uuid.New(), - Valid: true, - }, - subject: merge(canAssignRole, rbac.ScopedRoleOrgAdmin(orgID.UUID)), + name: "read-workspace-outside-org", + organizationID: uuid.New(), + subject: merge(canCreateCustomRole, rbac.ScopedRoleOrgAdmin(orgID)), org: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{ codersdk.ResourceWorkspace: {codersdk.ActionRead}, }), - errorContains: "forbidden", + errorContains: "not allowed to grant this permission", }, { name: "user-escalation", // These roles do not grant user perms - subject: merge(canAssignRole, rbac.ScopedRoleOrgAdmin(orgID.UUID)), + organizationID: orgID, + subject: merge(canCreateCustomRole, rbac.ScopedRoleOrgAdmin(orgID)), user: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{ codersdk.ResourceWorkspace: {codersdk.ActionRead}, }), - errorContains: "not allowed to grant this permission", + errorContains: "organization roles specify site or user permissions", }, { - name: "template-admin-escalation", - subject: merge(canAssignRole, rbac.RoleTemplateAdmin()), + name: "site-escalation", + organizationID: orgID, + subject: merge(canCreateCustomRole, rbac.RoleTemplateAdmin()), site: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{ - codersdk.ResourceWorkspace: {codersdk.ActionRead}, // ok! codersdk.ResourceDeploymentConfig: {codersdk.ActionUpdate}, // not ok! }), - user: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{ - codersdk.ResourceWorkspace: {codersdk.ActionRead}, // ok! - }), - errorContains: "deployment_config", + errorContains: "organization roles specify site or user permissions", }, // ok! { - name: "read-workspace-template-admin", - subject: merge(canAssignRole, rbac.RoleTemplateAdmin()), - site: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{ + name: "read-workspace-template-admin", + organizationID: orgID, + subject: merge(canCreateCustomRole, rbac.RoleTemplateAdmin()), + org: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{ codersdk.ResourceWorkspace: {codersdk.ActionRead}, }), }, { name: "read-workspace-in-org", - subject: merge(canAssignRole, rbac.ScopedRoleOrgAdmin(orgID.UUID)), organizationID: orgID, + subject: merge(canCreateCustomRole, rbac.ScopedRoleOrgAdmin(orgID)), org: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{ codersdk.ResourceWorkspace: {codersdk.ActionRead}, }), }, - { - name: "user-perms", - // This is weird, but is ok - subject: merge(canAssignRole, rbac.RoleMember()), - user: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{ - codersdk.ResourceWorkspace: {codersdk.ActionRead}, - }), - }, - { - name: "site+user-perms", - subject: merge(canAssignRole, rbac.RoleMember(), rbac.RoleTemplateAdmin()), - site: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{ - codersdk.ResourceWorkspace: {codersdk.ActionRead}, - }), - user: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{ - codersdk.ResourceWorkspace: {codersdk.ActionRead}, - }), - }, } for _, tc := range testCases { @@ -234,7 +218,7 @@ func TestInsertCustomRoles(t *testing.T) { _, err := az.InsertCustomRole(ctx, database.InsertCustomRoleParams{ Name: "test-role", DisplayName: "", - OrganizationID: tc.organizationID, + OrganizationID: uuid.NullUUID{UUID: tc.organizationID, Valid: true}, SitePermissions: db2sdk.List(tc.site, convertSDKPerm), OrgPermissions: db2sdk.List(tc.org, convertSDKPerm), UserPermissions: db2sdk.List(tc.user, convertSDKPerm), @@ -249,11 +233,11 @@ func TestInsertCustomRoles(t *testing.T) { LookupRoles: []database.NameOrganizationPair{ { Name: "test-role", - OrganizationID: tc.organizationID.UUID, + OrganizationID: tc.organizationID, }, }, ExcludeOrgRoles: false, - OrganizationID: uuid.UUID{}, + OrganizationID: uuid.Nil, }) require.NoError(t, err) require.Len(t, roles, 1) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 2e12cab9d33e0..faebb809f4599 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -184,6 +184,8 @@ var ( rbac.ResourceGroup.Type: {policy.ActionRead}, // Provisionerd creates notification messages rbac.ResourceNotificationMessage.Type: {policy.ActionCreate, policy.ActionRead}, + // Provisionerd creates workspaces resources monitor + rbac.ResourceWorkspaceAgentResourceMonitor.Type: {policy.ActionCreate}, }), Org: map[string][]rbac.Permission{}, User: []rbac.Permission{}, @@ -287,6 +289,24 @@ var ( Scope: rbac.ScopeAll, }.WithCachedASTValue() + subjectResourceMonitor = rbac.Subject{ + FriendlyName: "Resource Monitor", + ID: uuid.Nil.String(), + Roles: rbac.Roles([]rbac.Role{ + { + Identifier: rbac.RoleIdentifier{Name: "resourcemonitor"}, + DisplayName: "Resource Monitor", + Site: rbac.Permissions(map[string][]policy.Action{ + // The workspace monitor needs to be able to update monitors + rbac.ResourceWorkspaceAgentResourceMonitor.Type: {policy.ActionUpdate}, + }), + Org: map[string][]rbac.Permission{}, + User: []rbac.Permission{}, + }, + }), + Scope: rbac.ScopeAll, + }.WithCachedASTValue() + subjectSystemRestricted = rbac.Subject{ FriendlyName: "System", ID: uuid.Nil.String(), @@ -304,7 +324,6 @@ var ( rbac.ResourceOrganization.Type: {policy.ActionCreate, policy.ActionRead}, rbac.ResourceOrganizationMember.Type: {policy.ActionCreate, policy.ActionDelete, policy.ActionRead}, rbac.ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate}, - rbac.ResourceProvisionerKeys.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionDelete}, rbac.ResourceUser.Type: rbac.ResourceUser.AvailableActions(), rbac.ResourceWorkspaceDormant.Type: {policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceStop}, rbac.ResourceWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceStart, policy.ActionWorkspaceStop, policy.ActionSSH}, @@ -374,6 +393,12 @@ func AsNotifier(ctx context.Context) context.Context { return context.WithValue(ctx, authContextKey{}, subjectNotifier) } +// AsResourceMonitor returns a context with an actor that has permissions required for +// updating resource monitors. +func AsResourceMonitor(ctx context.Context) context.Context { + return context.WithValue(ctx, authContextKey{}, subjectResourceMonitor) +} + // AsSystemRestricted returns a context with an actor that has permissions // required for various system operations (login, logout, metrics cache). func AsSystemRestricted(ctx context.Context) context.Context { @@ -722,7 +747,7 @@ func (*querier) convertToDeploymentRoles(names []string) []rbac.RoleIdentifier { } // canAssignRoles handles assigning built in and custom roles. -func (q *querier) canAssignRoles(ctx context.Context, orgID *uuid.UUID, added, removed []rbac.RoleIdentifier) error { +func (q *querier) canAssignRoles(ctx context.Context, orgID uuid.UUID, added, removed []rbac.RoleIdentifier) error { actor, ok := ActorFromContext(ctx) if !ok { return NoActorError @@ -730,12 +755,14 @@ func (q *querier) canAssignRoles(ctx context.Context, orgID *uuid.UUID, added, r roleAssign := rbac.ResourceAssignRole shouldBeOrgRoles := false - if orgID != nil { - roleAssign = rbac.ResourceAssignOrgRole.InOrg(*orgID) + if orgID != uuid.Nil { + roleAssign = rbac.ResourceAssignOrgRole.InOrg(orgID) shouldBeOrgRoles = true } - grantedRoles := append(added, removed...) + grantedRoles := make([]rbac.RoleIdentifier, 0, len(added)+len(removed)) + grantedRoles = append(grantedRoles, added...) + grantedRoles = append(grantedRoles, removed...) customRoles := make([]rbac.RoleIdentifier, 0) // Validate that the roles being assigned are valid. for _, r := range grantedRoles { @@ -749,11 +776,11 @@ func (q *querier) canAssignRoles(ctx context.Context, orgID *uuid.UUID, added, r } if shouldBeOrgRoles { - if orgID == nil { + if orgID == uuid.Nil { return xerrors.Errorf("should never happen, orgID is nil, but trying to assign an organization role") } - if r.OrganizationID != *orgID { + if r.OrganizationID != orgID { return xerrors.Errorf("attempted to assign role from a different org, role %q to %q", r, orgID.String()) } } @@ -799,7 +826,7 @@ func (q *querier) canAssignRoles(ctx context.Context, orgID *uuid.UUID, added, r } if len(removed) > 0 { - if err := q.authorizeContext(ctx, policy.ActionDelete, roleAssign); err != nil { + if err := q.authorizeContext(ctx, policy.ActionUnassign, roleAssign); err != nil { return err } } @@ -1099,11 +1126,15 @@ func (q *querier) CleanTailnetTunnels(ctx context.Context) error { return q.db.CleanTailnetTunnels(ctx) } -// TODO: Handle org scoped lookups func (q *querier) CustomRoles(ctx context.Context, arg database.CustomRolesParams) ([]database.CustomRole, error) { - if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAssignRole); err != nil { + roleObject := rbac.ResourceAssignRole + if arg.OrganizationID != uuid.Nil { + roleObject = rbac.ResourceAssignOrgRole.InOrg(arg.OrganizationID) + } + if err := q.authorizeContext(ctx, policy.ActionRead, roleObject); err != nil { return nil, err } + return q.db.CustomRoles(ctx, arg) } @@ -1160,14 +1191,11 @@ func (q *querier) DeleteCryptoKey(ctx context.Context, arg database.DeleteCrypto } func (q *querier) DeleteCustomRole(ctx context.Context, arg database.DeleteCustomRoleParams) error { - if arg.OrganizationID.UUID != uuid.Nil { - if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceAssignOrgRole.InOrg(arg.OrganizationID.UUID)); err != nil { - return err - } - } else { - if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceAssignRole); err != nil { - return err - } + if !arg.OrganizationID.Valid || arg.OrganizationID.UUID == uuid.Nil { + return NotAuthorizedError{Err: xerrors.New("custom roles must belong to an organization")} + } + if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceAssignOrgRole.InOrg(arg.OrganizationID.UUID)); err != nil { + return err } return q.db.DeleteCustomRole(ctx, arg) @@ -1276,10 +1304,6 @@ func (q *querier) DeleteOldWorkspaceAgentStats(ctx context.Context) error { return q.db.DeleteOldWorkspaceAgentStats(ctx) } -func (q *querier) DeleteOrganization(ctx context.Context, id uuid.UUID) error { - return deleteQ(q.log, q.auth, q.db.GetOrganizationByID, q.db.DeleteOrganization)(ctx, id) -} - func (q *querier) DeleteOrganizationMember(ctx context.Context, arg database.DeleteOrganizationMemberParams) error { return deleteQ[database.OrganizationMember](q.log, q.auth, func(ctx context.Context, arg database.DeleteOrganizationMemberParams) (database.OrganizationMember, error) { member, err := database.ExpectOne(q.OrganizationMembers(ctx, database.OrganizationMembersParams(arg))) @@ -1391,6 +1415,31 @@ func (q *querier) FavoriteWorkspace(ctx context.Context, id uuid.UUID) error { return update(q.log, q.auth, fetch, q.db.FavoriteWorkspace)(ctx, id) } +func (q *querier) FetchMemoryResourceMonitorsByAgentID(ctx context.Context, agentID uuid.UUID) (database.WorkspaceAgentMemoryResourceMonitor, error) { + workspace, err := q.db.GetWorkspaceByAgentID(ctx, agentID) + if err != nil { + return database.WorkspaceAgentMemoryResourceMonitor{}, err + } + + err = q.authorizeContext(ctx, policy.ActionRead, workspace) + if err != nil { + return database.WorkspaceAgentMemoryResourceMonitor{}, err + } + + return q.db.FetchMemoryResourceMonitorsByAgentID(ctx, agentID) +} + +func (q *querier) FetchMemoryResourceMonitorsUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]database.WorkspaceAgentMemoryResourceMonitor, error) { + // Ideally, we would return a list of monitors that the user has access to. However, that check would need to + // be implemented similarly to GetWorkspaces, which is more complex than what we're doing here. Since this query + // was introduced for telemetry, we perform a simpler check. + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceWorkspaceAgentResourceMonitor); err != nil { + return nil, err + } + + return q.db.FetchMemoryResourceMonitorsUpdatedAfter(ctx, updatedAt) +} + func (q *querier) FetchNewMessageMetadata(ctx context.Context, arg database.FetchNewMessageMetadataParams) (database.FetchNewMessageMetadataRow, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceNotificationMessage); err != nil { return database.FetchNewMessageMetadataRow{}, err @@ -1398,6 +1447,31 @@ func (q *querier) FetchNewMessageMetadata(ctx context.Context, arg database.Fetc return q.db.FetchNewMessageMetadata(ctx, arg) } +func (q *querier) FetchVolumesResourceMonitorsByAgentID(ctx context.Context, agentID uuid.UUID) ([]database.WorkspaceAgentVolumeResourceMonitor, error) { + workspace, err := q.db.GetWorkspaceByAgentID(ctx, agentID) + if err != nil { + return nil, err + } + + err = q.authorizeContext(ctx, policy.ActionRead, workspace) + if err != nil { + return nil, err + } + + return q.db.FetchVolumesResourceMonitorsByAgentID(ctx, agentID) +} + +func (q *querier) FetchVolumesResourceMonitorsUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]database.WorkspaceAgentVolumeResourceMonitor, error) { + // Ideally, we would return a list of monitors that the user has access to. However, that check would need to + // be implemented similarly to GetWorkspaces, which is more complex than what we're doing here. Since this query + // was introduced for telemetry, we perform a simpler check. + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceWorkspaceAgentResourceMonitor); err != nil { + return nil, err + } + + return q.db.FetchVolumesResourceMonitorsUpdatedAfter(ctx, updatedAt) +} + func (q *querier) GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error) { return fetch(q.log, q.auth, q.db.GetAPIKeyByID)(ctx, id) } @@ -1796,6 +1870,13 @@ func (q *querier) GetNotificationsSettings(ctx context.Context) (string, error) return q.db.GetNotificationsSettings(ctx) } +func (q *querier) GetOAuth2GithubDefaultEligible(ctx context.Context) (bool, error) { + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceDeploymentConfig); err != nil { + return false, err + } + return q.db.GetOAuth2GithubDefaultEligible(ctx) +} + func (q *querier) GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (database.OAuth2ProviderApp, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceOauth2App); err != nil { return database.OAuth2ProviderApp{}, err @@ -1872,7 +1953,7 @@ func (q *querier) GetOrganizationByID(ctx context.Context, id uuid.UUID) (databa return fetch(q.log, q.auth, q.db.GetOrganizationByID)(ctx, id) } -func (q *querier) GetOrganizationByName(ctx context.Context, name string) (database.Organization, error) { +func (q *querier) GetOrganizationByName(ctx context.Context, name database.GetOrganizationByNameParams) (database.Organization, error) { return fetch(q.log, q.auth, q.db.GetOrganizationByName)(ctx, name) } @@ -1889,7 +1970,7 @@ func (q *querier) GetOrganizations(ctx context.Context, args database.GetOrganiz return fetchWithPostFilter(q.auth, policy.ActionRead, fetch)(ctx, nil) } -func (q *querier) GetOrganizationsByUserID(ctx context.Context, userID uuid.UUID) ([]database.Organization, error) { +func (q *querier) GetOrganizationsByUserID(ctx context.Context, userID database.GetOrganizationsByUserIDParams) ([]database.Organization, error) { return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetOrganizationsByUserID)(ctx, userID) } @@ -1914,6 +1995,33 @@ func (q *querier) GetParameterSchemasByJobID(ctx context.Context, jobID uuid.UUI return q.db.GetParameterSchemasByJobID(ctx, jobID) } +func (q *querier) GetPresetByWorkspaceBuildID(ctx context.Context, workspaceID uuid.UUID) (database.TemplateVersionPreset, error) { + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTemplate); err != nil { + return database.TemplateVersionPreset{}, err + } + return q.db.GetPresetByWorkspaceBuildID(ctx, workspaceID) +} + +func (q *querier) GetPresetParametersByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]database.TemplateVersionPresetParameter, error) { + // An actor can read template version presets if they can read the related template version. + _, err := q.GetTemplateVersionByID(ctx, templateVersionID) + if err != nil { + return nil, err + } + + return q.db.GetPresetParametersByTemplateVersionID(ctx, templateVersionID) +} + +func (q *querier) GetPresetsByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]database.TemplateVersionPreset, error) { + // An actor can read template version presets if they can read the related template version. + _, err := q.GetTemplateVersionByID(ctx, templateVersionID) + if err != nil { + return nil, err + } + + return q.db.GetPresetsByTemplateVersionID(ctx, templateVersionID) +} + func (q *querier) GetPreviousTemplateVersion(ctx context.Context, arg database.GetPreviousTemplateVersionParams) (database.TemplateVersion, error) { // An actor can read the previous template version if they can read the related template. // If no linked template exists, we check if the actor can read *a* template. @@ -2096,6 +2204,20 @@ func (q *querier) GetTailnetTunnelPeerIDs(ctx context.Context, srcID uuid.UUID) return q.db.GetTailnetTunnelPeerIDs(ctx, srcID) } +func (q *querier) GetTelemetryItem(ctx context.Context, key string) (database.TelemetryItem, error) { + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { + return database.TelemetryItem{}, err + } + return q.db.GetTelemetryItem(ctx, key) +} + +func (q *querier) GetTelemetryItems(ctx context.Context) ([]database.TelemetryItem, error) { + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { + return nil, err + } + return q.db.GetTelemetryItems(ctx) +} + func (q *querier) GetTemplateAppInsights(ctx context.Context, arg database.GetTemplateAppInsightsParams) ([]database.GetTemplateAppInsightsRow, error) { if err := q.authorizeTemplateInsights(ctx, arg.TemplateIDs); err != nil { return nil, err @@ -2912,14 +3034,11 @@ func (q *querier) InsertCryptoKey(ctx context.Context, arg database.InsertCrypto func (q *querier) InsertCustomRole(ctx context.Context, arg database.InsertCustomRoleParams) (database.CustomRole, error) { // Org and site role upsert share the same query. So switch the assertion based on the org uuid. - if arg.OrganizationID.UUID != uuid.Nil { - if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceAssignOrgRole.InOrg(arg.OrganizationID.UUID)); err != nil { - return database.CustomRole{}, err - } - } else { - if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceAssignRole); err != nil { - return database.CustomRole{}, err - } + if !arg.OrganizationID.Valid || arg.OrganizationID.UUID == uuid.Nil { + return database.CustomRole{}, NotAuthorizedError{Err: xerrors.New("custom roles must belong to an organization")} + } + if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceAssignOrgRole.InOrg(arg.OrganizationID.UUID)); err != nil { + return database.CustomRole{}, err } if err := q.customRoleCheck(ctx, database.CustomRole{ @@ -2989,6 +3108,14 @@ func (q *querier) InsertLicense(ctx context.Context, arg database.InsertLicenseP return q.db.InsertLicense(ctx, arg) } +func (q *querier) InsertMemoryResourceMonitor(ctx context.Context, arg database.InsertMemoryResourceMonitorParams) (database.WorkspaceAgentMemoryResourceMonitor, error) { + if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceWorkspaceAgentResourceMonitor); err != nil { + return database.WorkspaceAgentMemoryResourceMonitor{}, err + } + + return q.db.InsertMemoryResourceMonitor(ctx, arg) +} + func (q *querier) InsertMissingGroups(ctx context.Context, arg database.InsertMissingGroupsParams) ([]database.Group, error) { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { return nil, err @@ -3041,7 +3168,7 @@ func (q *querier) InsertOrganizationMember(ctx context.Context, arg database.Ins // All roles are added roles. Org member is always implied. addedRoles := append(orgRoles, rbac.ScopedRoleOrgMember(arg.OrganizationID)) - err = q.canAssignRoles(ctx, &arg.OrganizationID, addedRoles, []rbac.RoleIdentifier{}) + err = q.canAssignRoles(ctx, arg.OrganizationID, addedRoles, []rbac.RoleIdentifier{}) if err != nil { return database.OrganizationMember{}, err } @@ -3050,6 +3177,24 @@ func (q *querier) InsertOrganizationMember(ctx context.Context, arg database.Ins return insert(q.log, q.auth, obj, q.db.InsertOrganizationMember)(ctx, arg) } +func (q *querier) InsertPreset(ctx context.Context, arg database.InsertPresetParams) (database.TemplateVersionPreset, error) { + err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceTemplate) + if err != nil { + return database.TemplateVersionPreset{}, err + } + + return q.db.InsertPreset(ctx, arg) +} + +func (q *querier) InsertPresetParameters(ctx context.Context, arg database.InsertPresetParametersParams) ([]database.TemplateVersionPresetParameter, error) { + err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceTemplate) + if err != nil { + return nil, err + } + + return q.db.InsertPresetParameters(ctx, arg) +} + // TODO: We need to create a ProvisionerJob resource type func (q *querier) InsertProvisionerJob(ctx context.Context, arg database.InsertProvisionerJobParams) (database.ProvisionerJob, error) { // if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { @@ -3075,7 +3220,7 @@ func (q *querier) InsertProvisionerJobTimings(ctx context.Context, arg database. } func (q *querier) InsertProvisionerKey(ctx context.Context, arg database.InsertProvisionerKeyParams) (database.ProvisionerKey, error) { - return insert(q.log, q.auth, rbac.ResourceProvisionerKeys.InOrg(arg.OrganizationID).WithID(arg.ID), q.db.InsertProvisionerKey)(ctx, arg) + return insert(q.log, q.auth, rbac.ResourceProvisionerDaemon.InOrg(arg.OrganizationID).WithID(arg.ID), q.db.InsertProvisionerKey)(ctx, arg) } func (q *querier) InsertReplica(ctx context.Context, arg database.InsertReplicaParams) (database.Replica, error) { @@ -3085,6 +3230,13 @@ func (q *querier) InsertReplica(ctx context.Context, arg database.InsertReplicaP return q.db.InsertReplica(ctx, arg) } +func (q *querier) InsertTelemetryItemIfNotExists(ctx context.Context, arg database.InsertTelemetryItemIfNotExistsParams) error { + if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { + return err + } + return q.db.InsertTelemetryItemIfNotExists(ctx, arg) +} + func (q *querier) InsertTemplate(ctx context.Context, arg database.InsertTemplateParams) error { obj := rbac.ResourceTemplate.InOrg(arg.OrganizationID) if err := q.authorizeContext(ctx, policy.ActionCreate, obj); err != nil { @@ -3140,7 +3292,7 @@ func (q *querier) InsertTemplateVersionWorkspaceTag(ctx context.Context, arg dat func (q *querier) InsertUser(ctx context.Context, arg database.InsertUserParams) (database.User, error) { // Always check if the assigned roles can actually be assigned by this actor. impliedRoles := append([]rbac.RoleIdentifier{rbac.RoleMember()}, q.convertToDeploymentRoles(arg.RBACRoles)...) - err := q.canAssignRoles(ctx, nil, impliedRoles, []rbac.RoleIdentifier{}) + err := q.canAssignRoles(ctx, uuid.Nil, impliedRoles, []rbac.RoleIdentifier{}) if err != nil { return database.User{}, err } @@ -3174,6 +3326,14 @@ func (q *querier) InsertUserLink(ctx context.Context, arg database.InsertUserLin return q.db.InsertUserLink(ctx, arg) } +func (q *querier) InsertVolumeResourceMonitor(ctx context.Context, arg database.InsertVolumeResourceMonitorParams) (database.WorkspaceAgentVolumeResourceMonitor, error) { + if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceWorkspaceAgentResourceMonitor); err != nil { + return database.WorkspaceAgentVolumeResourceMonitor{}, err + } + + return q.db.InsertVolumeResourceMonitor(ctx, arg) +} + func (q *querier) InsertWorkspace(ctx context.Context, arg database.InsertWorkspaceParams) (database.WorkspaceTable, error) { obj := rbac.ResourceWorkspace.WithOwner(arg.OwnerID.String()).InOrg(arg.OrganizationID) tpl, err := q.GetTemplateByID(ctx, arg.TemplateID) @@ -3470,14 +3630,11 @@ func (q *querier) UpdateCryptoKeyDeletesAt(ctx context.Context, arg database.Upd } func (q *querier) UpdateCustomRole(ctx context.Context, arg database.UpdateCustomRoleParams) (database.CustomRole, error) { - if arg.OrganizationID.UUID != uuid.Nil { - if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceAssignOrgRole.InOrg(arg.OrganizationID.UUID)); err != nil { - return database.CustomRole{}, err - } - } else { - if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceAssignRole); err != nil { - return database.CustomRole{}, err - } + if !arg.OrganizationID.Valid || arg.OrganizationID.UUID == uuid.Nil { + return database.CustomRole{}, NotAuthorizedError{Err: xerrors.New("custom roles must belong to an organization")} + } + if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceAssignOrgRole.InOrg(arg.OrganizationID.UUID)); err != nil { + return database.CustomRole{}, err } if err := q.customRoleCheck(ctx, database.CustomRole{ @@ -3557,7 +3714,7 @@ func (q *querier) UpdateMemberRoles(ctx context.Context, arg database.UpdateMemb impliedTypes := append(scopedGranted, rbac.ScopedRoleOrgMember(arg.OrgID)) added, removed := rbac.ChangeRoleSet(originalRoles, impliedTypes) - err = q.canAssignRoles(ctx, &arg.OrgID, added, removed) + err = q.canAssignRoles(ctx, arg.OrgID, added, removed) if err != nil { return database.OrganizationMember{}, err } @@ -3565,6 +3722,14 @@ func (q *querier) UpdateMemberRoles(ctx context.Context, arg database.UpdateMemb return q.db.UpdateMemberRoles(ctx, arg) } +func (q *querier) UpdateMemoryResourceMonitor(ctx context.Context, arg database.UpdateMemoryResourceMonitorParams) error { + if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceWorkspaceAgentResourceMonitor); err != nil { + return err + } + + return q.db.UpdateMemoryResourceMonitor(ctx, arg) +} + func (q *querier) UpdateNotificationTemplateMethodByID(ctx context.Context, arg database.UpdateNotificationTemplateMethodByIDParams) (database.NotificationTemplate, error) { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceNotificationTemplate); err != nil { return database.NotificationTemplate{}, err @@ -3593,6 +3758,16 @@ func (q *querier) UpdateOrganization(ctx context.Context, arg database.UpdateOrg return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateOrganization)(ctx, arg) } +func (q *querier) UpdateOrganizationDeletedByID(ctx context.Context, arg database.UpdateOrganizationDeletedByIDParams) error { + deleteF := func(ctx context.Context, id uuid.UUID) error { + return q.db.UpdateOrganizationDeletedByID(ctx, database.UpdateOrganizationDeletedByIDParams{ + ID: id, + UpdatedAt: dbtime.Now(), + }) + } + return deleteQ(q.log, q.auth, q.db.GetOrganizationByID, deleteF)(ctx, arg.ID) +} + func (q *querier) UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg database.UpdateProvisionerDaemonLastSeenAtParams) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceProvisionerDaemon); err != nil { return err @@ -3946,7 +4121,7 @@ func (q *querier) UpdateUserRoles(ctx context.Context, arg database.UpdateUserRo impliedTypes := append(q.convertToDeploymentRoles(arg.GrantedRoles), rbac.RoleMember()) // If the changeset is nothing, less rbac checks need to be done. added, removed := rbac.ChangeRoleSet(q.convertToDeploymentRoles(user.RBACRoles), impliedTypes) - err = q.canAssignRoles(ctx, nil, added, removed) + err = q.canAssignRoles(ctx, uuid.Nil, added, removed) if err != nil { return database.User{}, err } @@ -3961,6 +4136,14 @@ func (q *querier) UpdateUserStatus(ctx context.Context, arg database.UpdateUserS return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateUserStatus)(ctx, arg) } +func (q *querier) UpdateVolumeResourceMonitor(ctx context.Context, arg database.UpdateVolumeResourceMonitorParams) error { + if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceWorkspaceAgentResourceMonitor); err != nil { + return err + } + + return q.db.UpdateVolumeResourceMonitor(ctx, arg) +} + func (q *querier) UpdateWorkspace(ctx context.Context, arg database.UpdateWorkspaceParams) (database.WorkspaceTable, error) { fetch := func(ctx context.Context, arg database.UpdateWorkspaceParams) (database.WorkspaceTable, error) { w, err := q.db.GetWorkspaceByID(ctx, arg.ID) @@ -4278,6 +4461,13 @@ func (q *querier) UpsertNotificationsSettings(ctx context.Context, value string) return q.db.UpsertNotificationsSettings(ctx, value) } +func (q *querier) UpsertOAuth2GithubDefaultEligible(ctx context.Context, eligible bool) error { + if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil { + return err + } + return q.db.UpsertOAuth2GithubDefaultEligible(ctx, eligible) +} + func (q *querier) UpsertOAuthSigningKey(ctx context.Context, value string) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { return err @@ -4345,6 +4535,13 @@ func (q *querier) UpsertTailnetTunnel(ctx context.Context, arg database.UpsertTa return q.db.UpsertTailnetTunnel(ctx, arg) } +func (q *querier) UpsertTelemetryItem(ctx context.Context, arg database.UpsertTelemetryItemParams) error { + if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { + return err + } + return q.db.UpsertTelemetryItem(ctx, arg) +} + func (q *querier) UpsertTemplateUsageStats(ctx context.Context) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { return err diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index fdbbcc8b34ca6..259fd941d2921 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -815,7 +815,7 @@ func (s *MethodTestSuite) TestOrganization() { })) s.Run("GetOrganizationByName", s.Subtest(func(db database.Store, check *expects) { o := dbgen.Organization(s.T(), db, database.Organization{}) - check.Args(o.Name).Asserts(o, policy.ActionRead).Returns(o) + check.Args(database.GetOrganizationByNameParams{Name: o.Name, Deleted: o.Deleted}).Asserts(o, policy.ActionRead).Returns(o) })) s.Run("GetOrganizationIDsByMemberIDs", s.Subtest(func(db database.Store, check *expects) { oa := dbgen.Organization(s.T(), db, database.Organization{}) @@ -839,7 +839,7 @@ func (s *MethodTestSuite) TestOrganization() { _ = dbgen.OrganizationMember(s.T(), db, database.OrganizationMember{UserID: u.ID, OrganizationID: a.ID}) b := dbgen.Organization(s.T(), db, database.Organization{}) _ = dbgen.OrganizationMember(s.T(), db, database.OrganizationMember{UserID: u.ID, OrganizationID: b.ID}) - check.Args(u.ID).Asserts(a, policy.ActionRead, b, policy.ActionRead).Returns(slice.New(a, b)) + check.Args(database.GetOrganizationsByUserIDParams{UserID: u.ID, Deleted: false}).Asserts(a, policy.ActionRead, b, policy.ActionRead).Returns(slice.New(a, b)) })) s.Run("InsertOrganization", s.Subtest(func(db database.Store, check *expects) { check.Args(database.InsertOrganizationParams{ @@ -859,6 +859,77 @@ func (s *MethodTestSuite) TestOrganization() { rbac.ResourceAssignOrgRole.InOrg(o.ID), policy.ActionAssign, rbac.ResourceOrganizationMember.InOrg(o.ID).WithID(u.ID), policy.ActionCreate) })) + s.Run("InsertPreset", s.Subtest(func(db database.Store, check *expects) { + org := dbgen.Organization(s.T(), db, database.Organization{}) + user := dbgen.User(s.T(), db, database.User{}) + template := dbgen.Template(s.T(), db, database.Template{ + CreatedBy: user.ID, + OrganizationID: org.ID, + }) + templateVersion := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{ + TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true}, + OrganizationID: org.ID, + CreatedBy: user.ID, + }) + workspace := dbgen.Workspace(s.T(), db, database.WorkspaceTable{ + OrganizationID: org.ID, + OwnerID: user.ID, + TemplateID: template.ID, + }) + job := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{ + OrganizationID: org.ID, + }) + workspaceBuild := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{ + WorkspaceID: workspace.ID, + TemplateVersionID: templateVersion.ID, + InitiatorID: user.ID, + JobID: job.ID, + }) + insertPresetParams := database.InsertPresetParams{ + TemplateVersionID: workspaceBuild.TemplateVersionID, + Name: "test", + } + check.Args(insertPresetParams).Asserts(rbac.ResourceTemplate, policy.ActionUpdate) + })) + s.Run("InsertPresetParameters", s.Subtest(func(db database.Store, check *expects) { + org := dbgen.Organization(s.T(), db, database.Organization{}) + user := dbgen.User(s.T(), db, database.User{}) + template := dbgen.Template(s.T(), db, database.Template{ + CreatedBy: user.ID, + OrganizationID: org.ID, + }) + templateVersion := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{ + TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true}, + OrganizationID: org.ID, + CreatedBy: user.ID, + }) + workspace := dbgen.Workspace(s.T(), db, database.WorkspaceTable{ + OrganizationID: org.ID, + OwnerID: user.ID, + TemplateID: template.ID, + }) + job := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{ + OrganizationID: org.ID, + }) + workspaceBuild := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{ + WorkspaceID: workspace.ID, + TemplateVersionID: templateVersion.ID, + InitiatorID: user.ID, + JobID: job.ID, + }) + insertPresetParams := database.InsertPresetParams{ + TemplateVersionID: workspaceBuild.TemplateVersionID, + Name: "test", + } + preset, err := db.InsertPreset(context.Background(), insertPresetParams) + require.NoError(s.T(), err) + insertPresetParametersParams := database.InsertPresetParametersParams{ + TemplateVersionPresetID: preset.ID, + Names: []string{"test"}, + Values: []string{"test"}, + } + check.Args(insertPresetParametersParams).Asserts(rbac.ResourceTemplate, policy.ActionUpdate) + })) s.Run("DeleteOrganizationMember", s.Subtest(func(db database.Store, check *expects) { o := dbgen.Organization(s.T(), db, database.Organization{}) u := dbgen.User(s.T(), db, database.User{}) @@ -889,13 +960,14 @@ func (s *MethodTestSuite) TestOrganization() { Name: "something-different", }).Asserts(o, policy.ActionUpdate) })) - s.Run("DeleteOrganization", s.Subtest(func(db database.Store, check *expects) { + s.Run("UpdateOrganizationDeletedByID", s.Subtest(func(db database.Store, check *expects) { o := dbgen.Organization(s.T(), db, database.Organization{ Name: "doomed", }) - check.Args( - o.ID, - ).Asserts(o, policy.ActionDelete) + check.Args(database.UpdateOrganizationDeletedByIDParams{ + ID: o.ID, + UpdatedAt: o.UpdatedAt, + }).Asserts(o, policy.ActionDelete).Returns() })) s.Run("OrganizationMembers", s.Subtest(func(db database.Store, check *expects) { o := dbgen.Organization(s.T(), db, database.Organization{}) @@ -939,7 +1011,7 @@ func (s *MethodTestSuite) TestOrganization() { Asserts( mem, policy.ActionRead, rbac.ResourceAssignOrgRole.InOrg(o.ID), policy.ActionAssign, // org-mem - rbac.ResourceAssignOrgRole.InOrg(o.ID), policy.ActionDelete, // org-admin + rbac.ResourceAssignOrgRole.InOrg(o.ID), policy.ActionUnassign, // org-admin ).Returns(out) })) } @@ -1547,7 +1619,7 @@ func (s *MethodTestSuite) TestUser() { }).Asserts( u, policy.ActionRead, rbac.ResourceAssignRole, policy.ActionAssign, - rbac.ResourceAssignRole, policy.ActionDelete, + rbac.ResourceAssignRole, policy.ActionUnassign, ).Returns(o) })) s.Run("AllUserIDs", s.Subtest(func(db database.Store, check *expects) { @@ -1581,30 +1653,28 @@ func (s *MethodTestSuite) TestUser() { check.Args(database.DeleteCustomRoleParams{ Name: customRole.Name, }).Asserts( - rbac.ResourceAssignRole, policy.ActionDelete) + // fails immediately, missing organization id + ).Errors(dbauthz.NotAuthorizedError{Err: xerrors.New("custom roles must belong to an organization")}) })) s.Run("Blank/UpdateCustomRole", s.Subtest(func(db database.Store, check *expects) { dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) - customRole := dbgen.CustomRole(s.T(), db, database.CustomRole{}) + customRole := dbgen.CustomRole(s.T(), db, database.CustomRole{ + OrganizationID: uuid.NullUUID{UUID: uuid.New(), Valid: true}, + }) // Blank is no perms in the role check.Args(database.UpdateCustomRoleParams{ Name: customRole.Name, DisplayName: "Test Name", + OrganizationID: customRole.OrganizationID, SitePermissions: nil, OrgPermissions: nil, UserPermissions: nil, - }).Asserts(rbac.ResourceAssignRole, policy.ActionUpdate).ErrorsWithPG(sql.ErrNoRows) + }).Asserts(rbac.ResourceAssignOrgRole.InOrg(customRole.OrganizationID.UUID), policy.ActionUpdate) })) s.Run("SitePermissions/UpdateCustomRole", s.Subtest(func(db database.Store, check *expects) { - customRole := dbgen.CustomRole(s.T(), db, database.CustomRole{ - OrganizationID: uuid.NullUUID{ - UUID: uuid.Nil, - Valid: false, - }, - }) check.Args(database.UpdateCustomRoleParams{ - Name: customRole.Name, - OrganizationID: customRole.OrganizationID, + Name: "", + OrganizationID: uuid.NullUUID{UUID: uuid.Nil, Valid: false}, DisplayName: "Test Name", SitePermissions: db2sdk.List(codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{ codersdk.ResourceTemplate: {codersdk.ActionCreate, codersdk.ActionRead, codersdk.ActionUpdate, codersdk.ActionDelete, codersdk.ActionViewInsights}, @@ -1614,17 +1684,8 @@ func (s *MethodTestSuite) TestUser() { codersdk.ResourceWorkspace: {codersdk.ActionRead}, }), convertSDKPerm), }).Asserts( - // First check - rbac.ResourceAssignRole, policy.ActionUpdate, - // Escalation checks - rbac.ResourceTemplate, policy.ActionCreate, - rbac.ResourceTemplate, policy.ActionRead, - rbac.ResourceTemplate, policy.ActionUpdate, - rbac.ResourceTemplate, policy.ActionDelete, - rbac.ResourceTemplate, policy.ActionViewInsights, - - rbac.ResourceWorkspace.WithOwner(testActorID.String()), policy.ActionRead, - ).ErrorsWithPG(sql.ErrNoRows) + // fails immediately, missing organization id + ).Errors(dbauthz.NotAuthorizedError{Err: xerrors.New("custom roles must belong to an organization")}) })) s.Run("OrgPermissions/UpdateCustomRole", s.Subtest(func(db database.Store, check *expects) { orgID := uuid.New() @@ -1654,13 +1715,15 @@ func (s *MethodTestSuite) TestUser() { })) s.Run("Blank/InsertCustomRole", s.Subtest(func(db database.Store, check *expects) { // Blank is no perms in the role + orgID := uuid.New() check.Args(database.InsertCustomRoleParams{ Name: "test", DisplayName: "Test Name", + OrganizationID: uuid.NullUUID{UUID: orgID, Valid: true}, SitePermissions: nil, OrgPermissions: nil, UserPermissions: nil, - }).Asserts(rbac.ResourceAssignRole, policy.ActionCreate) + }).Asserts(rbac.ResourceAssignOrgRole.InOrg(orgID), policy.ActionCreate) })) s.Run("SitePermissions/InsertCustomRole", s.Subtest(func(db database.Store, check *expects) { check.Args(database.InsertCustomRoleParams{ @@ -1674,17 +1737,8 @@ func (s *MethodTestSuite) TestUser() { codersdk.ResourceWorkspace: {codersdk.ActionRead}, }), convertSDKPerm), }).Asserts( - // First check - rbac.ResourceAssignRole, policy.ActionCreate, - // Escalation checks - rbac.ResourceTemplate, policy.ActionCreate, - rbac.ResourceTemplate, policy.ActionRead, - rbac.ResourceTemplate, policy.ActionUpdate, - rbac.ResourceTemplate, policy.ActionDelete, - rbac.ResourceTemplate, policy.ActionViewInsights, - - rbac.ResourceWorkspace.WithOwner(testActorID.String()), policy.ActionRead, - ) + // fails immediately, missing organization id + ).Errors(dbauthz.NotAuthorizedError{Err: xerrors.New("custom roles must belong to an organization")}) })) s.Run("OrgPermissions/InsertCustomRole", s.Subtest(func(db database.Store, check *expects) { orgID := uuid.New() @@ -3283,11 +3337,11 @@ func (s *MethodTestSuite) TestExtraMethods() { dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{ID: wbID, WorkspaceID: w.ID, TemplateVersionID: tv.ID, JobID: j2.ID}) ds, err := db.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner(context.Background(), database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams{ - OrganizationID: uuid.NullUUID{Valid: true, UUID: org.ID}, + OrganizationID: org.ID, }) s.NoError(err, "get provisioner jobs by org") check.Args(database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams{ - OrganizationID: uuid.NullUUID{Valid: true, UUID: org.ID}, + OrganizationID: org.ID, }).Asserts(j1, policy.ActionRead, j2, policy.ActionRead).Returns(ds) })) } @@ -3695,6 +3749,96 @@ func (s *MethodTestSuite) TestSystemFunctions() { ErrorsWithInMemDB(sql.ErrNoRows). Returns([]database.ParameterSchema{}) })) + s.Run("GetPresetByWorkspaceBuildID", s.Subtest(func(db database.Store, check *expects) { + org := dbgen.Organization(s.T(), db, database.Organization{}) + user := dbgen.User(s.T(), db, database.User{}) + template := dbgen.Template(s.T(), db, database.Template{ + CreatedBy: user.ID, + OrganizationID: org.ID, + }) + templateVersion := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{ + TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true}, + OrganizationID: org.ID, + CreatedBy: user.ID, + }) + preset, err := db.InsertPreset(context.Background(), database.InsertPresetParams{ + TemplateVersionID: templateVersion.ID, + Name: "test", + }) + require.NoError(s.T(), err) + workspace := dbgen.Workspace(s.T(), db, database.WorkspaceTable{ + OrganizationID: org.ID, + OwnerID: user.ID, + TemplateID: template.ID, + }) + job := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{ + OrganizationID: org.ID, + }) + workspaceBuild := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{ + WorkspaceID: workspace.ID, + TemplateVersionID: templateVersion.ID, + TemplateVersionPresetID: uuid.NullUUID{UUID: preset.ID, Valid: true}, + InitiatorID: user.ID, + JobID: job.ID, + }) + _, err = db.GetPresetByWorkspaceBuildID(context.Background(), workspaceBuild.ID) + require.NoError(s.T(), err) + check.Args(workspaceBuild.ID).Asserts(rbac.ResourceTemplate, policy.ActionRead) + })) + s.Run("GetPresetParametersByTemplateVersionID", s.Subtest(func(db database.Store, check *expects) { + ctx := context.Background() + org := dbgen.Organization(s.T(), db, database.Organization{}) + user := dbgen.User(s.T(), db, database.User{}) + template := dbgen.Template(s.T(), db, database.Template{ + CreatedBy: user.ID, + OrganizationID: org.ID, + }) + templateVersion := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{ + TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true}, + OrganizationID: org.ID, + CreatedBy: user.ID, + }) + preset, err := db.InsertPreset(ctx, database.InsertPresetParams{ + TemplateVersionID: templateVersion.ID, + Name: "test", + }) + require.NoError(s.T(), err) + _, err = db.InsertPresetParameters(ctx, database.InsertPresetParametersParams{ + TemplateVersionPresetID: preset.ID, + Names: []string{"test"}, + Values: []string{"test"}, + }) + require.NoError(s.T(), err) + presetParameters, err := db.GetPresetParametersByTemplateVersionID(ctx, templateVersion.ID) + require.NoError(s.T(), err) + + check.Args(templateVersion.ID).Asserts(template.RBACObject(), policy.ActionRead).Returns(presetParameters) + })) + s.Run("GetPresetsByTemplateVersionID", s.Subtest(func(db database.Store, check *expects) { + ctx := context.Background() + org := dbgen.Organization(s.T(), db, database.Organization{}) + user := dbgen.User(s.T(), db, database.User{}) + template := dbgen.Template(s.T(), db, database.Template{ + CreatedBy: user.ID, + OrganizationID: org.ID, + }) + templateVersion := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{ + TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true}, + OrganizationID: org.ID, + CreatedBy: user.ID, + }) + + _, err := db.InsertPreset(ctx, database.InsertPresetParams{ + TemplateVersionID: templateVersion.ID, + Name: "test", + }) + require.NoError(s.T(), err) + + presets, err := db.GetPresetsByTemplateVersionID(ctx, templateVersion.ID) + require.NoError(s.T(), err) + + check.Args(templateVersion.ID).Asserts(template.RBACObject(), policy.ActionRead).Returns(presets) + })) s.Run("GetWorkspaceAppsByAgentIDs", s.Subtest(func(db database.Store, check *expects) { dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) aWs := dbgen.Workspace(s.T(), db, database.WorkspaceTable{}) @@ -3757,7 +3901,8 @@ func (s *MethodTestSuite) TestSystemFunctions() { s.Run("InsertWorkspaceAgent", s.Subtest(func(db database.Store, check *expects) { dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) check.Args(database.InsertWorkspaceAgentParams{ - ID: uuid.New(), + ID: uuid.New(), + Name: "dev", }).Asserts(rbac.ResourceSystem, policy.ActionCreate) })) s.Run("InsertWorkspaceApp", s.Subtest(func(db database.Store, check *expects) { @@ -4224,6 +4369,30 @@ func (s *MethodTestSuite) TestSystemFunctions() { s.Run("GetWorkspaceModulesCreatedAfter", s.Subtest(func(db database.Store, check *expects) { check.Args(dbtime.Now()).Asserts(rbac.ResourceSystem, policy.ActionRead) })) + s.Run("GetTelemetryItem", s.Subtest(func(db database.Store, check *expects) { + check.Args("test").Asserts(rbac.ResourceSystem, policy.ActionRead).Errors(sql.ErrNoRows) + })) + s.Run("GetTelemetryItems", s.Subtest(func(db database.Store, check *expects) { + check.Args().Asserts(rbac.ResourceSystem, policy.ActionRead) + })) + s.Run("InsertTelemetryItemIfNotExists", s.Subtest(func(db database.Store, check *expects) { + check.Args(database.InsertTelemetryItemIfNotExistsParams{ + Key: "test", + Value: "value", + }).Asserts(rbac.ResourceSystem, policy.ActionCreate) + })) + s.Run("UpsertTelemetryItem", s.Subtest(func(db database.Store, check *expects) { + check.Args(database.UpsertTelemetryItemParams{ + Key: "test", + Value: "value", + }).Asserts(rbac.ResourceSystem, policy.ActionUpdate) + })) + s.Run("GetOAuth2GithubDefaultEligible", s.Subtest(func(db database.Store, check *expects) { + check.Args().Asserts(rbac.ResourceDeploymentConfig, policy.ActionRead).Errors(sql.ErrNoRows) + })) + s.Run("UpsertOAuth2GithubDefaultEligible", s.Subtest(func(db database.Store, check *expects) { + check.Args(true).Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate) + })) } func (s *MethodTestSuite) TestNotifications() { @@ -4544,3 +4713,115 @@ func (s *MethodTestSuite) TestOAuth2ProviderAppTokens() { }).Asserts(rbac.ResourceOauth2AppCodeToken.WithOwner(user.ID.String()), policy.ActionDelete) })) } + +func (s *MethodTestSuite) TestResourcesMonitor() { + createAgent := func(t *testing.T, db database.Store) (database.WorkspaceAgent, database.WorkspaceTable) { + t.Helper() + + u := dbgen.User(t, db, database.User{}) + o := dbgen.Organization(t, db, database.Organization{}) + tpl := dbgen.Template(t, db, database.Template{ + OrganizationID: o.ID, + CreatedBy: u.ID, + }) + tv := dbgen.TemplateVersion(t, db, database.TemplateVersion{ + TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true}, + OrganizationID: o.ID, + CreatedBy: u.ID, + }) + w := dbgen.Workspace(t, db, database.WorkspaceTable{ + TemplateID: tpl.ID, + OrganizationID: o.ID, + OwnerID: u.ID, + }) + j := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{ + Type: database.ProvisionerJobTypeWorkspaceBuild, + }) + b := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ + JobID: j.ID, + WorkspaceID: w.ID, + TemplateVersionID: tv.ID, + }) + res := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{JobID: b.JobID}) + agt := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ResourceID: res.ID}) + + return agt, w + } + + s.Run("InsertMemoryResourceMonitor", s.Subtest(func(db database.Store, check *expects) { + agt, _ := createAgent(s.T(), db) + + check.Args(database.InsertMemoryResourceMonitorParams{ + AgentID: agt.ID, + State: database.WorkspaceAgentMonitorStateOK, + }).Asserts(rbac.ResourceWorkspaceAgentResourceMonitor, policy.ActionCreate) + })) + + s.Run("InsertVolumeResourceMonitor", s.Subtest(func(db database.Store, check *expects) { + agt, _ := createAgent(s.T(), db) + + check.Args(database.InsertVolumeResourceMonitorParams{ + AgentID: agt.ID, + State: database.WorkspaceAgentMonitorStateOK, + }).Asserts(rbac.ResourceWorkspaceAgentResourceMonitor, policy.ActionCreate) + })) + + s.Run("UpdateMemoryResourceMonitor", s.Subtest(func(db database.Store, check *expects) { + agt, _ := createAgent(s.T(), db) + + check.Args(database.UpdateMemoryResourceMonitorParams{ + AgentID: agt.ID, + State: database.WorkspaceAgentMonitorStateOK, + }).Asserts(rbac.ResourceWorkspaceAgentResourceMonitor, policy.ActionUpdate) + })) + + s.Run("UpdateVolumeResourceMonitor", s.Subtest(func(db database.Store, check *expects) { + agt, _ := createAgent(s.T(), db) + + check.Args(database.UpdateVolumeResourceMonitorParams{ + AgentID: agt.ID, + State: database.WorkspaceAgentMonitorStateOK, + }).Asserts(rbac.ResourceWorkspaceAgentResourceMonitor, policy.ActionUpdate) + })) + + s.Run("FetchMemoryResourceMonitorsUpdatedAfter", s.Subtest(func(db database.Store, check *expects) { + check.Args(dbtime.Now()).Asserts(rbac.ResourceWorkspaceAgentResourceMonitor, policy.ActionRead) + })) + + s.Run("FetchVolumesResourceMonitorsUpdatedAfter", s.Subtest(func(db database.Store, check *expects) { + check.Args(dbtime.Now()).Asserts(rbac.ResourceWorkspaceAgentResourceMonitor, policy.ActionRead) + })) + + s.Run("FetchMemoryResourceMonitorsByAgentID", s.Subtest(func(db database.Store, check *expects) { + agt, w := createAgent(s.T(), db) + + dbgen.WorkspaceAgentMemoryResourceMonitor(s.T(), db, database.WorkspaceAgentMemoryResourceMonitor{ + AgentID: agt.ID, + Enabled: true, + Threshold: 80, + CreatedAt: dbtime.Now(), + }) + + monitor, err := db.FetchMemoryResourceMonitorsByAgentID(context.Background(), agt.ID) + require.NoError(s.T(), err) + + check.Args(agt.ID).Asserts(w, policy.ActionRead).Returns(monitor) + })) + + s.Run("FetchVolumesResourceMonitorsByAgentID", s.Subtest(func(db database.Store, check *expects) { + agt, w := createAgent(s.T(), db) + + dbgen.WorkspaceAgentVolumeResourceMonitor(s.T(), db, database.WorkspaceAgentVolumeResourceMonitor{ + AgentID: agt.ID, + Path: "/var/lib", + Enabled: true, + Threshold: 80, + CreatedAt: dbtime.Now(), + }) + + monitors, err := db.FetchVolumesResourceMonitorsByAgentID(context.Background(), agt.ID) + require.NoError(s.T(), err) + + check.Args(agt.ID).Asserts(w, policy.ActionRead).Returns(monitors) + })) +} diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index 9c5a09f40ff65..197502ebac42c 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -91,7 +91,8 @@ func (b WorkspaceBuildBuilder) WithAgent(mutations ...func([]*sdkproto.Agent) [] //nolint: revive // returns modified struct b.agentToken = uuid.NewString() agents := []*sdkproto.Agent{{ - Id: uuid.NewString(), + Id: uuid.NewString(), + Name: "dev", Auth: &sdkproto.Agent_Token{ Token: b.agentToken, }, diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index 566540dcb2906..9c4ebbe8bb8ca 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -314,6 +314,10 @@ func WorkspaceBuild(t testing.TB, db database.Store, orig database.WorkspaceBuil Deadline: takeFirst(orig.Deadline, dbtime.Now().Add(time.Hour)), MaxDeadline: takeFirst(orig.MaxDeadline, time.Time{}), Reason: takeFirst(orig.Reason, database.BuildReasonInitiator), + TemplateVersionPresetID: takeFirst(orig.TemplateVersionPresetID, uuid.NullUUID{ + UUID: uuid.UUID{}, + Valid: false, + }), }) if err != nil { return err @@ -1032,6 +1036,35 @@ func OAuth2ProviderAppToken(t testing.TB, db database.Store, seed database.OAuth return token } +func WorkspaceAgentMemoryResourceMonitor(t testing.TB, db database.Store, seed database.WorkspaceAgentMemoryResourceMonitor) database.WorkspaceAgentMemoryResourceMonitor { + monitor, err := db.InsertMemoryResourceMonitor(genCtx, database.InsertMemoryResourceMonitorParams{ + AgentID: takeFirst(seed.AgentID, uuid.New()), + Enabled: takeFirst(seed.Enabled, true), + State: takeFirst(seed.State, database.WorkspaceAgentMonitorStateOK), + Threshold: takeFirst(seed.Threshold, 100), + CreatedAt: takeFirst(seed.CreatedAt, dbtime.Now()), + UpdatedAt: takeFirst(seed.UpdatedAt, dbtime.Now()), + DebouncedUntil: takeFirst(seed.DebouncedUntil, time.Time{}), + }) + require.NoError(t, err, "insert workspace agent memory resource monitor") + return monitor +} + +func WorkspaceAgentVolumeResourceMonitor(t testing.TB, db database.Store, seed database.WorkspaceAgentVolumeResourceMonitor) database.WorkspaceAgentVolumeResourceMonitor { + monitor, err := db.InsertVolumeResourceMonitor(genCtx, database.InsertVolumeResourceMonitorParams{ + AgentID: takeFirst(seed.AgentID, uuid.New()), + Path: takeFirst(seed.Path, "/"), + Enabled: takeFirst(seed.Enabled, true), + State: takeFirst(seed.State, database.WorkspaceAgentMonitorStateOK), + Threshold: takeFirst(seed.Threshold, 100), + CreatedAt: takeFirst(seed.CreatedAt, dbtime.Now()), + UpdatedAt: takeFirst(seed.UpdatedAt, dbtime.Now()), + DebouncedUntil: takeFirst(seed.DebouncedUntil, time.Time{}), + }) + require.NoError(t, err, "insert workspace agent volume resource monitor") + return monitor +} + func CustomRole(t testing.TB, db database.Store, seed database.CustomRole) database.CustomRole { role, err := db.InsertCustomRole(genCtx, database.InsertCustomRoleParams{ Name: takeFirst(seed.Name, strings.ToLower(testutil.GetRandomName(t))), @@ -1093,6 +1126,23 @@ func ProvisionerJobTimings(t testing.TB, db database.Store, build database.Works return timings } +func TelemetryItem(t testing.TB, db database.Store, seed database.TelemetryItem) database.TelemetryItem { + if seed.Key == "" { + seed.Key = testutil.GetRandomName(t) + } + if seed.Value == "" { + seed.Value = time.Now().Format(time.RFC3339) + } + err := db.UpsertTelemetryItem(genCtx, database.UpsertTelemetryItemParams{ + Key: seed.Key, + Value: seed.Value, + }) + require.NoError(t, err, "upsert telemetry item") + item, err := db.GetTelemetryItem(genCtx, seed.Key) + require.NoError(t, err, "get telemetry item") + return item +} + func provisionerJobTiming(t testing.TB, db database.Store, seed database.ProvisionerJobTiming) database.ProvisionerJobTiming { timing, err := db.InsertProvisionerJobTimings(genCtx, database.InsertProvisionerJobTimingsParams{ JobID: takeFirst(seed.JobID, uuid.New()), diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 6b518c7696369..e4956958e0073 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -89,6 +89,9 @@ func New() database.Store { locks: map[int64]struct{}{}, runtimeConfig: map[string]string{}, userStatusChanges: make([]database.UserStatusChange, 0), + telemetryItems: make([]database.TelemetryItem, 0), + presets: make([]database.TemplateVersionPreset, 0), + presetParameters: make([]database.TemplateVersionPresetParameter, 0), }, } // Always start with a default org. Matching migration 198. @@ -114,7 +117,7 @@ func New() database.Store { q.defaultProxyIconURL = "/emojis/1f3e1.png" _, err = q.InsertProvisionerKey(context.Background(), database.InsertProvisionerKeyParams{ - ID: uuid.MustParse(codersdk.ProvisionerKeyIDBuiltIn), + ID: codersdk.ProvisionerKeyUUIDBuiltIn, OrganizationID: defaultOrg.ID, CreatedAt: dbtime.Now(), HashedSecret: []byte{}, @@ -125,7 +128,7 @@ func New() database.Store { panic(xerrors.Errorf("failed to create built-in provisioner key: %w", err)) } _, err = q.InsertProvisionerKey(context.Background(), database.InsertProvisionerKeyParams{ - ID: uuid.MustParse(codersdk.ProvisionerKeyIDUserAuth), + ID: codersdk.ProvisionerKeyUUIDUserAuth, OrganizationID: defaultOrg.ID, CreatedAt: dbtime.Now(), HashedSecret: []byte{}, @@ -136,7 +139,7 @@ func New() database.Store { panic(xerrors.Errorf("failed to create user-auth provisioner key: %w", err)) } _, err = q.InsertProvisionerKey(context.Background(), database.InsertProvisionerKeyParams{ - ID: uuid.MustParse(codersdk.ProvisionerKeyIDPSK), + ID: codersdk.ProvisionerKeyUUIDPSK, OrganizationID: defaultOrg.ID, CreatedAt: dbtime.Now(), HashedSecret: []byte{}, @@ -190,56 +193,58 @@ type data struct { userLinks []database.UserLink // New tables - auditLogs []database.AuditLog - cryptoKeys []database.CryptoKey - dbcryptKeys []database.DBCryptKey - files []database.File - externalAuthLinks []database.ExternalAuthLink - gitSSHKey []database.GitSSHKey - groupMembers []database.GroupMemberTable - groups []database.Group - jfrogXRayScans []database.JfrogXrayScan - licenses []database.License - notificationMessages []database.NotificationMessage - notificationPreferences []database.NotificationPreference - notificationReportGeneratorLogs []database.NotificationReportGeneratorLog - oauth2ProviderApps []database.OAuth2ProviderApp - oauth2ProviderAppSecrets []database.OAuth2ProviderAppSecret - oauth2ProviderAppCodes []database.OAuth2ProviderAppCode - oauth2ProviderAppTokens []database.OAuth2ProviderAppToken - parameterSchemas []database.ParameterSchema - provisionerDaemons []database.ProvisionerDaemon - provisionerJobLogs []database.ProvisionerJobLog - provisionerJobs []database.ProvisionerJob - provisionerKeys []database.ProvisionerKey - replicas []database.Replica - templateVersions []database.TemplateVersionTable - templateVersionParameters []database.TemplateVersionParameter - templateVersionVariables []database.TemplateVersionVariable - templateVersionWorkspaceTags []database.TemplateVersionWorkspaceTag - templates []database.TemplateTable - templateUsageStats []database.TemplateUsageStat - workspaceAgents []database.WorkspaceAgent - workspaceAgentMetadata []database.WorkspaceAgentMetadatum - workspaceAgentLogs []database.WorkspaceAgentLog - workspaceAgentLogSources []database.WorkspaceAgentLogSource - workspaceAgentPortShares []database.WorkspaceAgentPortShare - workspaceAgentScriptTimings []database.WorkspaceAgentScriptTiming - workspaceAgentScripts []database.WorkspaceAgentScript - workspaceAgentStats []database.WorkspaceAgentStat - workspaceApps []database.WorkspaceApp - workspaceAppStatsLastInsertID int64 - workspaceAppStats []database.WorkspaceAppStat - workspaceBuilds []database.WorkspaceBuild - workspaceBuildParameters []database.WorkspaceBuildParameter - workspaceResourceMetadata []database.WorkspaceResourceMetadatum - workspaceResources []database.WorkspaceResource - workspaceModules []database.WorkspaceModule - workspaces []database.WorkspaceTable - workspaceProxies []database.WorkspaceProxy - customRoles []database.CustomRole - provisionerJobTimings []database.ProvisionerJobTiming - runtimeConfig map[string]string + auditLogs []database.AuditLog + cryptoKeys []database.CryptoKey + dbcryptKeys []database.DBCryptKey + files []database.File + externalAuthLinks []database.ExternalAuthLink + gitSSHKey []database.GitSSHKey + groupMembers []database.GroupMemberTable + groups []database.Group + jfrogXRayScans []database.JfrogXrayScan + licenses []database.License + notificationMessages []database.NotificationMessage + notificationPreferences []database.NotificationPreference + notificationReportGeneratorLogs []database.NotificationReportGeneratorLog + oauth2ProviderApps []database.OAuth2ProviderApp + oauth2ProviderAppSecrets []database.OAuth2ProviderAppSecret + oauth2ProviderAppCodes []database.OAuth2ProviderAppCode + oauth2ProviderAppTokens []database.OAuth2ProviderAppToken + parameterSchemas []database.ParameterSchema + provisionerDaemons []database.ProvisionerDaemon + provisionerJobLogs []database.ProvisionerJobLog + provisionerJobs []database.ProvisionerJob + provisionerKeys []database.ProvisionerKey + replicas []database.Replica + templateVersions []database.TemplateVersionTable + templateVersionParameters []database.TemplateVersionParameter + templateVersionVariables []database.TemplateVersionVariable + templateVersionWorkspaceTags []database.TemplateVersionWorkspaceTag + templates []database.TemplateTable + templateUsageStats []database.TemplateUsageStat + workspaceAgents []database.WorkspaceAgent + workspaceAgentMetadata []database.WorkspaceAgentMetadatum + workspaceAgentLogs []database.WorkspaceAgentLog + workspaceAgentLogSources []database.WorkspaceAgentLogSource + workspaceAgentPortShares []database.WorkspaceAgentPortShare + workspaceAgentScriptTimings []database.WorkspaceAgentScriptTiming + workspaceAgentScripts []database.WorkspaceAgentScript + workspaceAgentStats []database.WorkspaceAgentStat + workspaceAgentMemoryResourceMonitors []database.WorkspaceAgentMemoryResourceMonitor + workspaceAgentVolumeResourceMonitors []database.WorkspaceAgentVolumeResourceMonitor + workspaceApps []database.WorkspaceApp + workspaceAppStatsLastInsertID int64 + workspaceAppStats []database.WorkspaceAppStat + workspaceBuilds []database.WorkspaceBuild + workspaceBuildParameters []database.WorkspaceBuildParameter + workspaceResourceMetadata []database.WorkspaceResourceMetadatum + workspaceResources []database.WorkspaceResource + workspaceModules []database.WorkspaceModule + workspaces []database.WorkspaceTable + workspaceProxies []database.WorkspaceProxy + customRoles []database.CustomRole + provisionerJobTimings []database.ProvisionerJobTiming + runtimeConfig map[string]string // Locks is a map of lock names. Any keys within the map are currently // locked. locks map[int64]struct{} @@ -249,6 +254,7 @@ type data struct { announcementBanners []byte healthSettings []byte notificationsSettings []byte + oauth2GithubDefaultEligible *bool applicationName string logoURL string appSecurityKey string @@ -258,6 +264,9 @@ type data struct { defaultProxyDisplayName string defaultProxyIconURL string userStatusChanges []database.UserStatusChange + telemetryItems []database.TelemetryItem + presets []database.TemplateVersionPreset + presetParameters []database.TemplateVersionPresetParameter } func tryPercentile(fs []float64, p float64) float64 { @@ -2149,19 +2158,6 @@ func (q *FakeQuerier) DeleteOldWorkspaceAgentStats(_ context.Context) error { return nil } -func (q *FakeQuerier) DeleteOrganization(_ context.Context, id uuid.UUID) error { - q.mutex.Lock() - defer q.mutex.Unlock() - - for i, org := range q.organizations { - if org.ID == id && !org.IsDefault { - q.organizations = append(q.organizations[:i], q.organizations[i+1:]...) - return nil - } - } - return sql.ErrNoRows -} - func (q *FakeQuerier) DeleteOrganizationMember(ctx context.Context, arg database.DeleteOrganizationMemberParams) error { err := validateDatabaseType(arg) if err != nil { @@ -2355,6 +2351,29 @@ func (q *FakeQuerier) FavoriteWorkspace(_ context.Context, arg uuid.UUID) error return nil } +func (q *FakeQuerier) FetchMemoryResourceMonitorsByAgentID(_ context.Context, agentID uuid.UUID) (database.WorkspaceAgentMemoryResourceMonitor, error) { + for _, monitor := range q.workspaceAgentMemoryResourceMonitors { + if monitor.AgentID == agentID { + return monitor, nil + } + } + + return database.WorkspaceAgentMemoryResourceMonitor{}, sql.ErrNoRows +} + +func (q *FakeQuerier) FetchMemoryResourceMonitorsUpdatedAfter(_ context.Context, updatedAt time.Time) ([]database.WorkspaceAgentMemoryResourceMonitor, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + monitors := []database.WorkspaceAgentMemoryResourceMonitor{} + for _, monitor := range q.workspaceAgentMemoryResourceMonitors { + if monitor.UpdatedAt.After(updatedAt) { + monitors = append(monitors, monitor) + } + } + return monitors, nil +} + func (q *FakeQuerier) FetchNewMessageMetadata(_ context.Context, arg database.FetchNewMessageMetadataParams) (database.FetchNewMessageMetadataRow, error) { err := validateDatabaseType(arg) if err != nil { @@ -2387,6 +2406,31 @@ func (q *FakeQuerier) FetchNewMessageMetadata(_ context.Context, arg database.Fe }, nil } +func (q *FakeQuerier) FetchVolumesResourceMonitorsByAgentID(_ context.Context, agentID uuid.UUID) ([]database.WorkspaceAgentVolumeResourceMonitor, error) { + monitors := []database.WorkspaceAgentVolumeResourceMonitor{} + + for _, monitor := range q.workspaceAgentVolumeResourceMonitors { + if monitor.AgentID == agentID { + monitors = append(monitors, monitor) + } + } + + return monitors, nil +} + +func (q *FakeQuerier) FetchVolumesResourceMonitorsUpdatedAfter(_ context.Context, updatedAt time.Time) ([]database.WorkspaceAgentVolumeResourceMonitor, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + monitors := []database.WorkspaceAgentVolumeResourceMonitor{} + for _, monitor := range q.workspaceAgentVolumeResourceMonitors { + if monitor.UpdatedAt.After(updatedAt) { + monitors = append(monitors, monitor) + } + } + return monitors, nil +} + func (q *FakeQuerier) GetAPIKeyByID(_ context.Context, id string) (database.APIKey, error) { q.mutex.RLock() defer q.mutex.RUnlock() @@ -3498,6 +3542,16 @@ func (q *FakeQuerier) GetNotificationsSettings(_ context.Context) (string, error return string(q.notificationsSettings), nil } +func (q *FakeQuerier) GetOAuth2GithubDefaultEligible(_ context.Context) (bool, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + if q.oauth2GithubDefaultEligible == nil { + return false, sql.ErrNoRows + } + return *q.oauth2GithubDefaultEligible, nil +} + func (q *FakeQuerier) GetOAuth2ProviderAppByID(_ context.Context, id uuid.UUID) (database.OAuth2ProviderApp, error) { q.mutex.Lock() defer q.mutex.Unlock() @@ -3658,12 +3712,12 @@ func (q *FakeQuerier) GetOrganizationByID(_ context.Context, id uuid.UUID) (data return q.getOrganizationByIDNoLock(id) } -func (q *FakeQuerier) GetOrganizationByName(_ context.Context, name string) (database.Organization, error) { +func (q *FakeQuerier) GetOrganizationByName(_ context.Context, params database.GetOrganizationByNameParams) (database.Organization, error) { q.mutex.RLock() defer q.mutex.RUnlock() for _, organization := range q.organizations { - if organization.Name == name { + if organization.Name == params.Name && organization.Deleted == params.Deleted { return organization, nil } } @@ -3710,17 +3764,17 @@ func (q *FakeQuerier) GetOrganizations(_ context.Context, args database.GetOrgan return tmp, nil } -func (q *FakeQuerier) GetOrganizationsByUserID(_ context.Context, userID uuid.UUID) ([]database.Organization, error) { +func (q *FakeQuerier) GetOrganizationsByUserID(_ context.Context, arg database.GetOrganizationsByUserIDParams) ([]database.Organization, error) { q.mutex.RLock() defer q.mutex.RUnlock() organizations := make([]database.Organization, 0) for _, organizationMember := range q.organizationMembers { - if organizationMember.UserID != userID { + if organizationMember.UserID != arg.UserID { continue } for _, organization := range q.organizations { - if organization.ID != organizationMember.OrganizationID { + if organization.ID != organizationMember.OrganizationID || organization.Deleted != arg.Deleted { continue } organizations = append(organizations, organization) @@ -3750,6 +3804,61 @@ func (q *FakeQuerier) GetParameterSchemasByJobID(_ context.Context, jobID uuid.U return parameters, nil } +func (q *FakeQuerier) GetPresetByWorkspaceBuildID(_ context.Context, workspaceBuildID uuid.UUID) (database.TemplateVersionPreset, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + for _, workspaceBuild := range q.workspaceBuilds { + if workspaceBuild.ID != workspaceBuildID { + continue + } + for _, preset := range q.presets { + if preset.TemplateVersionID == workspaceBuild.TemplateVersionID { + return preset, nil + } + } + } + return database.TemplateVersionPreset{}, sql.ErrNoRows +} + +func (q *FakeQuerier) GetPresetParametersByTemplateVersionID(_ context.Context, templateVersionID uuid.UUID) ([]database.TemplateVersionPresetParameter, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + presets := make([]database.TemplateVersionPreset, 0) + parameters := make([]database.TemplateVersionPresetParameter, 0) + for _, preset := range q.presets { + if preset.TemplateVersionID != templateVersionID { + continue + } + presets = append(presets, preset) + } + for _, parameter := range q.presetParameters { + for _, preset := range presets { + if parameter.TemplateVersionPresetID != preset.ID { + continue + } + parameters = append(parameters, parameter) + break + } + } + + return parameters, nil +} + +func (q *FakeQuerier) GetPresetsByTemplateVersionID(_ context.Context, templateVersionID uuid.UUID) ([]database.TemplateVersionPreset, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + presets := make([]database.TemplateVersionPreset, 0) + for _, preset := range q.presets { + if preset.TemplateVersionID == templateVersionID { + presets = append(presets, preset) + } + } + return presets, nil +} + func (q *FakeQuerier) GetPreviousTemplateVersion(_ context.Context, arg database.GetPreviousTemplateVersionParams) (database.TemplateVersion, error) { if err := validateDatabaseType(arg); err != nil { return database.TemplateVersion{}, err @@ -3846,7 +3955,7 @@ func (q *FakeQuerier) GetProvisionerDaemonsByOrganization(_ context.Context, arg return daemons, nil } -func (q *FakeQuerier) GetProvisionerDaemonsWithStatusByOrganization(_ context.Context, arg database.GetProvisionerDaemonsWithStatusByOrganizationParams) ([]database.GetProvisionerDaemonsWithStatusByOrganizationRow, error) { +func (q *FakeQuerier) GetProvisionerDaemonsWithStatusByOrganization(ctx context.Context, arg database.GetProvisionerDaemonsWithStatusByOrganizationParams) ([]database.GetProvisionerDaemonsWithStatusByOrganizationRow, error) { err := validateDatabaseType(arg) if err != nil { return nil, err @@ -3896,6 +4005,31 @@ func (q *FakeQuerier) GetProvisionerDaemonsWithStatusByOrganization(_ context.Co status = database.ProvisionerDaemonStatusIdle } } + var currentTemplate database.Template + if currentJob.ID != uuid.Nil { + var input codersdk.ProvisionerJobInput + err := json.Unmarshal(currentJob.Input, &input) + if err != nil { + return nil, err + } + if input.WorkspaceBuildID != nil { + b, err := q.getWorkspaceBuildByIDNoLock(ctx, *input.WorkspaceBuildID) + if err != nil { + return nil, err + } + input.TemplateVersionID = &b.TemplateVersionID + } + if input.TemplateVersionID != nil { + v, err := q.getTemplateVersionByIDNoLock(ctx, *input.TemplateVersionID) + if err != nil { + return nil, err + } + currentTemplate, err = q.getTemplateByIDNoLock(ctx, v.TemplateID.UUID) + if err != nil { + return nil, err + } + } + } var previousJob database.ProvisionerJob for _, job := range q.provisionerJobs { @@ -3912,6 +4046,31 @@ func (q *FakeQuerier) GetProvisionerDaemonsWithStatusByOrganization(_ context.Co } } } + var previousTemplate database.Template + if previousJob.ID != uuid.Nil { + var input codersdk.ProvisionerJobInput + err := json.Unmarshal(previousJob.Input, &input) + if err != nil { + return nil, err + } + if input.WorkspaceBuildID != nil { + b, err := q.getWorkspaceBuildByIDNoLock(ctx, *input.WorkspaceBuildID) + if err != nil { + return nil, err + } + input.TemplateVersionID = &b.TemplateVersionID + } + if input.TemplateVersionID != nil { + v, err := q.getTemplateVersionByIDNoLock(ctx, *input.TemplateVersionID) + if err != nil { + return nil, err + } + previousTemplate, err = q.getTemplateByIDNoLock(ctx, v.TemplateID.UUID) + if err != nil { + return nil, err + } + } + } // Get the provisioner key name var keyName string @@ -3923,13 +4082,19 @@ func (q *FakeQuerier) GetProvisionerDaemonsWithStatusByOrganization(_ context.Co } rows = append(rows, database.GetProvisionerDaemonsWithStatusByOrganizationRow{ - ProvisionerDaemon: daemon, - Status: status, - KeyName: keyName, - CurrentJobID: uuid.NullUUID{UUID: currentJob.ID, Valid: currentJob.ID != uuid.Nil}, - CurrentJobStatus: database.NullProvisionerJobStatus{ProvisionerJobStatus: currentJob.JobStatus, Valid: currentJob.ID != uuid.Nil}, - PreviousJobID: uuid.NullUUID{UUID: previousJob.ID, Valid: previousJob.ID != uuid.Nil}, - PreviousJobStatus: database.NullProvisionerJobStatus{ProvisionerJobStatus: previousJob.JobStatus, Valid: previousJob.ID != uuid.Nil}, + ProvisionerDaemon: daemon, + Status: status, + KeyName: keyName, + CurrentJobID: uuid.NullUUID{UUID: currentJob.ID, Valid: currentJob.ID != uuid.Nil}, + CurrentJobStatus: database.NullProvisionerJobStatus{ProvisionerJobStatus: currentJob.JobStatus, Valid: currentJob.ID != uuid.Nil}, + CurrentJobTemplateName: currentTemplate.Name, + CurrentJobTemplateDisplayName: currentTemplate.DisplayName, + CurrentJobTemplateIcon: currentTemplate.Icon, + PreviousJobID: uuid.NullUUID{UUID: previousJob.ID, Valid: previousJob.ID != uuid.Nil}, + PreviousJobStatus: database.NullProvisionerJobStatus{ProvisionerJobStatus: previousJob.JobStatus, Valid: previousJob.ID != uuid.Nil}, + PreviousJobTemplateName: previousTemplate.Name, + PreviousJobTemplateDisplayName: previousTemplate.DisplayName, + PreviousJobTemplateIcon: previousTemplate.Icon, }) } @@ -3937,6 +4102,10 @@ func (q *FakeQuerier) GetProvisionerDaemonsWithStatusByOrganization(_ context.Co return a.ProvisionerDaemon.CreatedAt.Compare(b.ProvisionerDaemon.CreatedAt) }) + if arg.Limit.Valid && arg.Limit.Int32 > 0 && len(rows) > int(arg.Limit.Int32) { + rows = rows[:arg.Limit.Int32] + } + return rows, nil } @@ -4076,7 +4245,7 @@ func (q *FakeQuerier) GetProvisionerJobsByOrganizationAndStatusWithQueuePosition for _, rowQP := range rowsWithQueuePosition { job := rowQP.ProvisionerJob - if arg.OrganizationID.Valid && job.OrganizationID != arg.OrganizationID.UUID { + if job.OrganizationID != arg.OrganizationID { continue } if len(arg.Status) > 0 && !slices.Contains(arg.Status, job.JobStatus) { @@ -4085,12 +4254,54 @@ func (q *FakeQuerier) GetProvisionerJobsByOrganizationAndStatusWithQueuePosition if len(arg.IDs) > 0 && !slices.Contains(arg.IDs, job.ID) { continue } + if len(arg.Tags) > 0 && !tagsSubset(job.Tags, arg.Tags) { + continue + } row := database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow{ ProvisionerJob: rowQP.ProvisionerJob, QueuePosition: rowQP.QueuePosition, QueueSize: rowQP.QueueSize, } + + // Start add metadata. + var input codersdk.ProvisionerJobInput + err := json.Unmarshal([]byte(job.Input), &input) + if err != nil { + return nil, err + } + templateVersionID := input.TemplateVersionID + if input.WorkspaceBuildID != nil { + workspaceBuild, err := q.getWorkspaceBuildByIDNoLock(ctx, *input.WorkspaceBuildID) + if err != nil { + return nil, err + } + workspace, err := q.getWorkspaceByIDNoLock(ctx, workspaceBuild.WorkspaceID) + if err != nil { + return nil, err + } + row.WorkspaceID = uuid.NullUUID{UUID: workspace.ID, Valid: true} + row.WorkspaceName = workspace.Name + if templateVersionID == nil { + templateVersionID = &workspaceBuild.TemplateVersionID + } + } + if templateVersionID != nil { + templateVersion, err := q.getTemplateVersionByIDNoLock(ctx, *templateVersionID) + if err != nil { + return nil, err + } + row.TemplateVersionName = templateVersion.Name + template, err := q.getTemplateByIDNoLock(ctx, templateVersion.TemplateID.UUID) + if err != nil { + return nil, err + } + row.TemplateID = uuid.NullUUID{UUID: template.ID, Valid: true} + row.TemplateName = template.Name + row.TemplateDisplayName = template.DisplayName + } + // End add metadata. + if row.QueuePosition > 0 { var availableWorkers []database.ProvisionerDaemon for _, daemon := range q.provisionerDaemons { @@ -4330,6 +4541,23 @@ func (*FakeQuerier) GetTailnetTunnelPeerIDs(context.Context, uuid.UUID) ([]datab return nil, ErrUnimplemented } +func (q *FakeQuerier) GetTelemetryItem(_ context.Context, key string) (database.TelemetryItem, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + for _, item := range q.telemetryItems { + if item.Key == key { + return item, nil + } + } + + return database.TelemetryItem{}, sql.ErrNoRows +} + +func (q *FakeQuerier) GetTelemetryItems(_ context.Context) ([]database.TelemetryItem, error) { + return q.telemetryItems, nil +} + func (q *FakeQuerier) GetTemplateAppInsights(ctx context.Context, arg database.GetTemplateAppInsightsParams) ([]database.GetTemplateAppInsightsRow, error) { err := validateDatabaseType(arg) if err != nil { @@ -7776,6 +8004,30 @@ func (q *FakeQuerier) InsertLicense( return l, nil } +func (q *FakeQuerier) InsertMemoryResourceMonitor(_ context.Context, arg database.InsertMemoryResourceMonitorParams) (database.WorkspaceAgentMemoryResourceMonitor, error) { + err := validateDatabaseType(arg) + if err != nil { + return database.WorkspaceAgentMemoryResourceMonitor{}, err + } + + q.mutex.Lock() + defer q.mutex.Unlock() + + //nolint:unconvert // The structs field-order differs so this is needed. + monitor := database.WorkspaceAgentMemoryResourceMonitor(database.WorkspaceAgentMemoryResourceMonitor{ + AgentID: arg.AgentID, + Enabled: arg.Enabled, + State: arg.State, + Threshold: arg.Threshold, + CreatedAt: arg.CreatedAt, + UpdatedAt: arg.UpdatedAt, + DebouncedUntil: arg.DebouncedUntil, + }) + + q.workspaceAgentMemoryResourceMonitors = append(q.workspaceAgentMemoryResourceMonitors, monitor) + return monitor, nil +} + func (q *FakeQuerier) InsertMissingGroups(_ context.Context, arg database.InsertMissingGroupsParams) ([]database.Group, error) { err := validateDatabaseType(arg) if err != nil { @@ -7984,6 +8236,50 @@ func (q *FakeQuerier) InsertOrganizationMember(_ context.Context, arg database.I return organizationMember, nil } +func (q *FakeQuerier) InsertPreset(_ context.Context, arg database.InsertPresetParams) (database.TemplateVersionPreset, error) { + err := validateDatabaseType(arg) + if err != nil { + return database.TemplateVersionPreset{}, err + } + + q.mutex.Lock() + defer q.mutex.Unlock() + + //nolint:gosimple // arg needs to keep its type for interface reasons and that type is not appropriate for preset below. + preset := database.TemplateVersionPreset{ + ID: uuid.New(), + TemplateVersionID: arg.TemplateVersionID, + Name: arg.Name, + CreatedAt: arg.CreatedAt, + } + q.presets = append(q.presets, preset) + return preset, nil +} + +func (q *FakeQuerier) InsertPresetParameters(_ context.Context, arg database.InsertPresetParametersParams) ([]database.TemplateVersionPresetParameter, error) { + err := validateDatabaseType(arg) + if err != nil { + return nil, err + } + + q.mutex.Lock() + defer q.mutex.Unlock() + + presetParameters := make([]database.TemplateVersionPresetParameter, 0, len(arg.Names)) + for i, v := range arg.Names { + presetParameter := database.TemplateVersionPresetParameter{ + ID: uuid.New(), + TemplateVersionPresetID: arg.TemplateVersionPresetID, + Name: v, + Value: arg.Values[i], + } + presetParameters = append(presetParameters, presetParameter) + q.presetParameters = append(q.presetParameters, presetParameter) + } + + return presetParameters, nil +} + func (q *FakeQuerier) InsertProvisionerJob(_ context.Context, arg database.InsertProvisionerJobParams) (database.ProvisionerJob, error) { if err := validateDatabaseType(arg); err != nil { return database.ProvisionerJob{}, err @@ -8120,6 +8416,30 @@ func (q *FakeQuerier) InsertReplica(_ context.Context, arg database.InsertReplic return replica, nil } +func (q *FakeQuerier) InsertTelemetryItemIfNotExists(_ context.Context, arg database.InsertTelemetryItemIfNotExistsParams) error { + err := validateDatabaseType(arg) + if err != nil { + return err + } + + q.mutex.Lock() + defer q.mutex.Unlock() + + for _, item := range q.telemetryItems { + if item.Key == arg.Key { + return nil + } + } + + q.telemetryItems = append(q.telemetryItems, database.TelemetryItem{ + Key: arg.Key, + Value: arg.Value, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }) + return nil +} + func (q *FakeQuerier) InsertTemplate(_ context.Context, arg database.InsertTemplateParams) error { if err := validateDatabaseType(arg); err != nil { return err @@ -8379,6 +8699,30 @@ func (q *FakeQuerier) InsertUserLink(_ context.Context, args database.InsertUser return link, nil } +func (q *FakeQuerier) InsertVolumeResourceMonitor(_ context.Context, arg database.InsertVolumeResourceMonitorParams) (database.WorkspaceAgentVolumeResourceMonitor, error) { + err := validateDatabaseType(arg) + if err != nil { + return database.WorkspaceAgentVolumeResourceMonitor{}, err + } + + q.mutex.Lock() + defer q.mutex.Unlock() + + monitor := database.WorkspaceAgentVolumeResourceMonitor{ + AgentID: arg.AgentID, + Path: arg.Path, + Enabled: arg.Enabled, + State: arg.State, + Threshold: arg.Threshold, + CreatedAt: arg.CreatedAt, + UpdatedAt: arg.UpdatedAt, + DebouncedUntil: arg.DebouncedUntil, + } + + q.workspaceAgentVolumeResourceMonitors = append(q.workspaceAgentVolumeResourceMonitors, monitor) + return monitor, nil +} + func (q *FakeQuerier) InsertWorkspace(_ context.Context, arg database.InsertWorkspaceParams) (database.WorkspaceTable, error) { if err := validateDatabaseType(arg); err != nil { return database.WorkspaceTable{}, err @@ -9383,6 +9727,30 @@ func (q *FakeQuerier) UpdateMemberRoles(_ context.Context, arg database.UpdateMe return database.OrganizationMember{}, sql.ErrNoRows } +func (q *FakeQuerier) UpdateMemoryResourceMonitor(_ context.Context, arg database.UpdateMemoryResourceMonitorParams) error { + err := validateDatabaseType(arg) + if err != nil { + return err + } + + q.mutex.Lock() + defer q.mutex.Unlock() + + for i, monitor := range q.workspaceAgentMemoryResourceMonitors { + if monitor.AgentID != arg.AgentID { + continue + } + + monitor.State = arg.State + monitor.UpdatedAt = arg.UpdatedAt + monitor.DebouncedUntil = arg.DebouncedUntil + q.workspaceAgentMemoryResourceMonitors[i] = monitor + return nil + } + + return nil +} + func (*FakeQuerier) UpdateNotificationTemplateMethodByID(_ context.Context, _ database.UpdateNotificationTemplateMethodByIDParams) (database.NotificationTemplate, error) { // Not implementing this function because it relies on state in the database which is created with migrations. // We could consider using code-generation to align the database state and dbmem, but it's not worth it right now. @@ -9478,6 +9846,26 @@ func (q *FakeQuerier) UpdateOrganization(_ context.Context, arg database.UpdateO return database.Organization{}, sql.ErrNoRows } +func (q *FakeQuerier) UpdateOrganizationDeletedByID(_ context.Context, arg database.UpdateOrganizationDeletedByIDParams) error { + if err := validateDatabaseType(arg); err != nil { + return err + } + + q.mutex.Lock() + defer q.mutex.Unlock() + + for index, organization := range q.organizations { + if organization.ID != arg.ID || organization.IsDefault { + continue + } + organization.Deleted = true + organization.UpdatedAt = arg.UpdatedAt + q.organizations[index] = organization + return nil + } + return sql.ErrNoRows +} + func (q *FakeQuerier) UpdateProvisionerDaemonLastSeenAt(_ context.Context, arg database.UpdateProvisionerDaemonLastSeenAtParams) error { err := validateDatabaseType(arg) if err != nil { @@ -10161,6 +10549,30 @@ func (q *FakeQuerier) UpdateUserStatus(_ context.Context, arg database.UpdateUse return database.User{}, sql.ErrNoRows } +func (q *FakeQuerier) UpdateVolumeResourceMonitor(_ context.Context, arg database.UpdateVolumeResourceMonitorParams) error { + err := validateDatabaseType(arg) + if err != nil { + return err + } + + q.mutex.Lock() + defer q.mutex.Unlock() + + for i, monitor := range q.workspaceAgentVolumeResourceMonitors { + if monitor.AgentID != arg.AgentID || monitor.Path != arg.Path { + continue + } + + monitor.State = arg.State + monitor.UpdatedAt = arg.UpdatedAt + monitor.DebouncedUntil = arg.DebouncedUntil + q.workspaceAgentVolumeResourceMonitors[i] = monitor + return nil + } + + return nil +} + func (q *FakeQuerier) UpdateWorkspace(_ context.Context, arg database.UpdateWorkspaceParams) (database.WorkspaceTable, error) { if err := validateDatabaseType(arg); err != nil { return database.WorkspaceTable{}, err @@ -10779,6 +11191,14 @@ func (q *FakeQuerier) UpsertNotificationsSettings(_ context.Context, data string return nil } +func (q *FakeQuerier) UpsertOAuth2GithubDefaultEligible(_ context.Context, eligible bool) error { + q.mutex.Lock() + defer q.mutex.Unlock() + + q.oauth2GithubDefaultEligible = &eligible + return nil +} + func (q *FakeQuerier) UpsertOAuthSigningKey(_ context.Context, value string) error { q.mutex.Lock() defer q.mutex.Unlock() @@ -10874,6 +11294,33 @@ func (*FakeQuerier) UpsertTailnetTunnel(_ context.Context, arg database.UpsertTa return database.TailnetTunnel{}, ErrUnimplemented } +func (q *FakeQuerier) UpsertTelemetryItem(_ context.Context, arg database.UpsertTelemetryItemParams) error { + err := validateDatabaseType(arg) + if err != nil { + return err + } + + q.mutex.Lock() + defer q.mutex.Unlock() + + for i, item := range q.telemetryItems { + if item.Key == arg.Key { + q.telemetryItems[i].Value = arg.Value + q.telemetryItems[i].UpdatedAt = time.Now() + return nil + } + } + + q.telemetryItems = append(q.telemetryItems, database.TelemetryItem{ + Key: arg.Key, + Value: arg.Value, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }) + + return nil +} + func (q *FakeQuerier) UpsertTemplateUsageStats(ctx context.Context) error { q.mutex.Lock() defer q.mutex.Unlock() @@ -12098,10 +12545,13 @@ func (q *FakeQuerier) GetAuthorizedAuditLogsOffset(ctx context.Context, arg data arg.OffsetOpt-- continue } + if arg.RequestID != uuid.Nil && arg.RequestID != alog.RequestID { + continue + } if arg.OrganizationID != uuid.Nil && arg.OrganizationID != alog.OrganizationID { continue } - if arg.Action != "" && !strings.Contains(string(alog.Action), arg.Action) { + if arg.Action != "" && string(alog.Action) != arg.Action { continue } if arg.ResourceType != "" && !strings.Contains(string(alog.ResourceType), arg.ResourceType) { diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index ba8a1f9cdc8a6..b53e0a72126e6 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -77,6 +77,16 @@ func (m queryMetricsStore) InTx(f func(database.Store) error, options *database. return m.dbMetrics.InTx(f, options) } +func (m queryMetricsStore) DeleteOrganization(ctx context.Context, id uuid.UUID) error { + start := time.Now() + r0 := m.s.UpdateOrganizationDeletedByID(ctx, database.UpdateOrganizationDeletedByIDParams{ + ID: id, + UpdatedAt: time.Now(), + }) + m.queryLatencies.WithLabelValues("DeleteOrganization").Observe(time.Since(start).Seconds()) + return r0 +} + func (m queryMetricsStore) AcquireLock(ctx context.Context, pgAdvisoryXactLock int64) error { start := time.Now() err := m.s.AcquireLock(ctx, pgAdvisoryXactLock) @@ -329,13 +339,6 @@ func (m queryMetricsStore) DeleteOldWorkspaceAgentStats(ctx context.Context) err return err } -func (m queryMetricsStore) DeleteOrganization(ctx context.Context, id uuid.UUID) error { - start := time.Now() - r0 := m.s.DeleteOrganization(ctx, id) - m.queryLatencies.WithLabelValues("DeleteOrganization").Observe(time.Since(start).Seconds()) - return r0 -} - func (m queryMetricsStore) DeleteOrganizationMember(ctx context.Context, arg database.DeleteOrganizationMemberParams) error { start := time.Now() r0 := m.s.DeleteOrganizationMember(ctx, arg) @@ -434,6 +437,20 @@ func (m queryMetricsStore) FavoriteWorkspace(ctx context.Context, arg uuid.UUID) return r0 } +func (m queryMetricsStore) FetchMemoryResourceMonitorsByAgentID(ctx context.Context, agentID uuid.UUID) (database.WorkspaceAgentMemoryResourceMonitor, error) { + start := time.Now() + r0, r1 := m.s.FetchMemoryResourceMonitorsByAgentID(ctx, agentID) + m.queryLatencies.WithLabelValues("FetchMemoryResourceMonitorsByAgentID").Observe(time.Since(start).Seconds()) + return r0, r1 +} + +func (m queryMetricsStore) FetchMemoryResourceMonitorsUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]database.WorkspaceAgentMemoryResourceMonitor, error) { + start := time.Now() + r0, r1 := m.s.FetchMemoryResourceMonitorsUpdatedAfter(ctx, updatedAt) + m.queryLatencies.WithLabelValues("FetchMemoryResourceMonitorsUpdatedAfter").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) FetchNewMessageMetadata(ctx context.Context, arg database.FetchNewMessageMetadataParams) (database.FetchNewMessageMetadataRow, error) { start := time.Now() r0, r1 := m.s.FetchNewMessageMetadata(ctx, arg) @@ -441,6 +458,20 @@ func (m queryMetricsStore) FetchNewMessageMetadata(ctx context.Context, arg data return r0, r1 } +func (m queryMetricsStore) FetchVolumesResourceMonitorsByAgentID(ctx context.Context, agentID uuid.UUID) ([]database.WorkspaceAgentVolumeResourceMonitor, error) { + start := time.Now() + r0, r1 := m.s.FetchVolumesResourceMonitorsByAgentID(ctx, agentID) + m.queryLatencies.WithLabelValues("FetchVolumesResourceMonitorsByAgentID").Observe(time.Since(start).Seconds()) + return r0, r1 +} + +func (m queryMetricsStore) FetchVolumesResourceMonitorsUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]database.WorkspaceAgentVolumeResourceMonitor, error) { + start := time.Now() + r0, r1 := m.s.FetchVolumesResourceMonitorsUpdatedAfter(ctx, updatedAt) + m.queryLatencies.WithLabelValues("FetchVolumesResourceMonitorsUpdatedAfter").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error) { start := time.Now() apiKey, err := m.s.GetAPIKeyByID(ctx, id) @@ -854,6 +885,13 @@ func (m queryMetricsStore) GetNotificationsSettings(ctx context.Context) (string return r0, r1 } +func (m queryMetricsStore) GetOAuth2GithubDefaultEligible(ctx context.Context) (bool, error) { + start := time.Now() + r0, r1 := m.s.GetOAuth2GithubDefaultEligible(ctx) + m.queryLatencies.WithLabelValues("GetOAuth2GithubDefaultEligible").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (database.OAuth2ProviderApp, error) { start := time.Now() r0, r1 := m.s.GetOAuth2ProviderAppByID(ctx, id) @@ -931,7 +969,7 @@ func (m queryMetricsStore) GetOrganizationByID(ctx context.Context, id uuid.UUID return organization, err } -func (m queryMetricsStore) GetOrganizationByName(ctx context.Context, name string) (database.Organization, error) { +func (m queryMetricsStore) GetOrganizationByName(ctx context.Context, name database.GetOrganizationByNameParams) (database.Organization, error) { start := time.Now() organization, err := m.s.GetOrganizationByName(ctx, name) m.queryLatencies.WithLabelValues("GetOrganizationByName").Observe(time.Since(start).Seconds()) @@ -952,7 +990,7 @@ func (m queryMetricsStore) GetOrganizations(ctx context.Context, args database.G return organizations, err } -func (m queryMetricsStore) GetOrganizationsByUserID(ctx context.Context, userID uuid.UUID) ([]database.Organization, error) { +func (m queryMetricsStore) GetOrganizationsByUserID(ctx context.Context, userID database.GetOrganizationsByUserIDParams) ([]database.Organization, error) { start := time.Now() organizations, err := m.s.GetOrganizationsByUserID(ctx, userID) m.queryLatencies.WithLabelValues("GetOrganizationsByUserID").Observe(time.Since(start).Seconds()) @@ -966,6 +1004,27 @@ func (m queryMetricsStore) GetParameterSchemasByJobID(ctx context.Context, jobID return schemas, err } +func (m queryMetricsStore) GetPresetByWorkspaceBuildID(ctx context.Context, workspaceBuildID uuid.UUID) (database.TemplateVersionPreset, error) { + start := time.Now() + r0, r1 := m.s.GetPresetByWorkspaceBuildID(ctx, workspaceBuildID) + m.queryLatencies.WithLabelValues("GetPresetByWorkspaceBuildID").Observe(time.Since(start).Seconds()) + return r0, r1 +} + +func (m queryMetricsStore) GetPresetParametersByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]database.TemplateVersionPresetParameter, error) { + start := time.Now() + r0, r1 := m.s.GetPresetParametersByTemplateVersionID(ctx, templateVersionID) + m.queryLatencies.WithLabelValues("GetPresetParametersByTemplateVersionID").Observe(time.Since(start).Seconds()) + return r0, r1 +} + +func (m queryMetricsStore) GetPresetsByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]database.TemplateVersionPreset, error) { + start := time.Now() + r0, r1 := m.s.GetPresetsByTemplateVersionID(ctx, templateVersionID) + m.queryLatencies.WithLabelValues("GetPresetsByTemplateVersionID").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) GetPreviousTemplateVersion(ctx context.Context, arg database.GetPreviousTemplateVersionParams) (database.TemplateVersion, error) { start := time.Now() version, err := m.s.GetPreviousTemplateVersion(ctx, arg) @@ -1134,6 +1193,20 @@ func (m queryMetricsStore) GetTailnetTunnelPeerIDs(ctx context.Context, srcID uu return r0, r1 } +func (m queryMetricsStore) GetTelemetryItem(ctx context.Context, key string) (database.TelemetryItem, error) { + start := time.Now() + r0, r1 := m.s.GetTelemetryItem(ctx, key) + m.queryLatencies.WithLabelValues("GetTelemetryItem").Observe(time.Since(start).Seconds()) + return r0, r1 +} + +func (m queryMetricsStore) GetTelemetryItems(ctx context.Context) ([]database.TelemetryItem, error) { + start := time.Now() + r0, r1 := m.s.GetTelemetryItems(ctx) + m.queryLatencies.WithLabelValues("GetTelemetryItems").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) GetTemplateAppInsights(ctx context.Context, arg database.GetTemplateAppInsightsParams) ([]database.GetTemplateAppInsightsRow, error) { start := time.Now() r0, r1 := m.s.GetTemplateAppInsights(ctx, arg) @@ -1827,6 +1900,13 @@ func (m queryMetricsStore) InsertLicense(ctx context.Context, arg database.Inser return license, err } +func (m queryMetricsStore) InsertMemoryResourceMonitor(ctx context.Context, arg database.InsertMemoryResourceMonitorParams) (database.WorkspaceAgentMemoryResourceMonitor, error) { + start := time.Now() + r0, r1 := m.s.InsertMemoryResourceMonitor(ctx, arg) + m.queryLatencies.WithLabelValues("InsertMemoryResourceMonitor").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) InsertMissingGroups(ctx context.Context, arg database.InsertMissingGroupsParams) ([]database.Group, error) { start := time.Now() r0, r1 := m.s.InsertMissingGroups(ctx, arg) @@ -1876,6 +1956,20 @@ func (m queryMetricsStore) InsertOrganizationMember(ctx context.Context, arg dat return member, err } +func (m queryMetricsStore) InsertPreset(ctx context.Context, arg database.InsertPresetParams) (database.TemplateVersionPreset, error) { + start := time.Now() + r0, r1 := m.s.InsertPreset(ctx, arg) + m.queryLatencies.WithLabelValues("InsertPreset").Observe(time.Since(start).Seconds()) + return r0, r1 +} + +func (m queryMetricsStore) InsertPresetParameters(ctx context.Context, arg database.InsertPresetParametersParams) ([]database.TemplateVersionPresetParameter, error) { + start := time.Now() + r0, r1 := m.s.InsertPresetParameters(ctx, arg) + m.queryLatencies.WithLabelValues("InsertPresetParameters").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) InsertProvisionerJob(ctx context.Context, arg database.InsertProvisionerJobParams) (database.ProvisionerJob, error) { start := time.Now() job, err := m.s.InsertProvisionerJob(ctx, arg) @@ -1911,6 +2005,13 @@ func (m queryMetricsStore) InsertReplica(ctx context.Context, arg database.Inser return replica, err } +func (m queryMetricsStore) InsertTelemetryItemIfNotExists(ctx context.Context, arg database.InsertTelemetryItemIfNotExistsParams) error { + start := time.Now() + r0 := m.s.InsertTelemetryItemIfNotExists(ctx, arg) + m.queryLatencies.WithLabelValues("InsertTelemetryItemIfNotExists").Observe(time.Since(start).Seconds()) + return r0 +} + func (m queryMetricsStore) InsertTemplate(ctx context.Context, arg database.InsertTemplateParams) error { start := time.Now() err := m.s.InsertTemplate(ctx, arg) @@ -1974,6 +2075,13 @@ func (m queryMetricsStore) InsertUserLink(ctx context.Context, arg database.Inse return link, err } +func (m queryMetricsStore) InsertVolumeResourceMonitor(ctx context.Context, arg database.InsertVolumeResourceMonitorParams) (database.WorkspaceAgentVolumeResourceMonitor, error) { + start := time.Now() + r0, r1 := m.s.InsertVolumeResourceMonitor(ctx, arg) + m.queryLatencies.WithLabelValues("InsertVolumeResourceMonitor").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) InsertWorkspace(ctx context.Context, arg database.InsertWorkspaceParams) (database.WorkspaceTable, error) { start := time.Now() workspace, err := m.s.InsertWorkspace(ctx, arg) @@ -2247,6 +2355,13 @@ func (m queryMetricsStore) UpdateMemberRoles(ctx context.Context, arg database.U return member, err } +func (m queryMetricsStore) UpdateMemoryResourceMonitor(ctx context.Context, arg database.UpdateMemoryResourceMonitorParams) error { + start := time.Now() + r0 := m.s.UpdateMemoryResourceMonitor(ctx, arg) + m.queryLatencies.WithLabelValues("UpdateMemoryResourceMonitor").Observe(time.Since(start).Seconds()) + return r0 +} + func (m queryMetricsStore) UpdateNotificationTemplateMethodByID(ctx context.Context, arg database.UpdateNotificationTemplateMethodByIDParams) (database.NotificationTemplate, error) { start := time.Now() r0, r1 := m.s.UpdateNotificationTemplateMethodByID(ctx, arg) @@ -2275,6 +2390,13 @@ func (m queryMetricsStore) UpdateOrganization(ctx context.Context, arg database. return r0, r1 } +func (m queryMetricsStore) UpdateOrganizationDeletedByID(ctx context.Context, arg database.UpdateOrganizationDeletedByIDParams) error { + start := time.Now() + r0 := m.s.UpdateOrganizationDeletedByID(ctx, arg) + m.queryLatencies.WithLabelValues("UpdateOrganizationDeletedByID").Observe(time.Since(start).Seconds()) + return r0 +} + func (m queryMetricsStore) UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg database.UpdateProvisionerDaemonLastSeenAtParams) error { start := time.Now() r0 := m.s.UpdateProvisionerDaemonLastSeenAt(ctx, arg) @@ -2485,6 +2607,13 @@ func (m queryMetricsStore) UpdateUserStatus(ctx context.Context, arg database.Up return user, err } +func (m queryMetricsStore) UpdateVolumeResourceMonitor(ctx context.Context, arg database.UpdateVolumeResourceMonitorParams) error { + start := time.Now() + r0 := m.s.UpdateVolumeResourceMonitor(ctx, arg) + m.queryLatencies.WithLabelValues("UpdateVolumeResourceMonitor").Observe(time.Since(start).Seconds()) + return r0 +} + func (m queryMetricsStore) UpdateWorkspace(ctx context.Context, arg database.UpdateWorkspaceParams) (database.WorkspaceTable, error) { start := time.Now() workspace, err := m.s.UpdateWorkspace(ctx, arg) @@ -2709,6 +2838,13 @@ func (m queryMetricsStore) UpsertNotificationsSettings(ctx context.Context, valu return r0 } +func (m queryMetricsStore) UpsertOAuth2GithubDefaultEligible(ctx context.Context, eligible bool) error { + start := time.Now() + r0 := m.s.UpsertOAuth2GithubDefaultEligible(ctx, eligible) + m.queryLatencies.WithLabelValues("UpsertOAuth2GithubDefaultEligible").Observe(time.Since(start).Seconds()) + return r0 +} + func (m queryMetricsStore) UpsertOAuthSigningKey(ctx context.Context, value string) error { start := time.Now() r0 := m.s.UpsertOAuthSigningKey(ctx, value) @@ -2772,6 +2908,13 @@ func (m queryMetricsStore) UpsertTailnetTunnel(ctx context.Context, arg database return r0, r1 } +func (m queryMetricsStore) UpsertTelemetryItem(ctx context.Context, arg database.UpsertTelemetryItemParams) error { + start := time.Now() + r0 := m.s.UpsertTelemetryItem(ctx, arg) + m.queryLatencies.WithLabelValues("UpsertTelemetryItem").Observe(time.Since(start).Seconds()) + return r0 +} + func (m queryMetricsStore) UpsertTemplateUsageStats(ctx context.Context) error { start := time.Now() r0 := m.s.UpsertTemplateUsageStats(ctx) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index d2aa8aa6fa62e..766a392ee7313 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -557,20 +557,6 @@ func (mr *MockStoreMockRecorder) DeleteOldWorkspaceAgentStats(ctx any) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOldWorkspaceAgentStats", reflect.TypeOf((*MockStore)(nil).DeleteOldWorkspaceAgentStats), ctx) } -// DeleteOrganization mocks base method. -func (m *MockStore) DeleteOrganization(ctx context.Context, id uuid.UUID) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteOrganization", ctx, id) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteOrganization indicates an expected call of DeleteOrganization. -func (mr *MockStoreMockRecorder) DeleteOrganization(ctx, id any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOrganization", reflect.TypeOf((*MockStore)(nil).DeleteOrganization), ctx, id) -} - // DeleteOrganizationMember mocks base method. func (m *MockStore) DeleteOrganizationMember(ctx context.Context, arg database.DeleteOrganizationMemberParams) error { m.ctrl.T.Helper() @@ -771,6 +757,36 @@ func (mr *MockStoreMockRecorder) FavoriteWorkspace(ctx, id any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FavoriteWorkspace", reflect.TypeOf((*MockStore)(nil).FavoriteWorkspace), ctx, id) } +// FetchMemoryResourceMonitorsByAgentID mocks base method. +func (m *MockStore) FetchMemoryResourceMonitorsByAgentID(ctx context.Context, agentID uuid.UUID) (database.WorkspaceAgentMemoryResourceMonitor, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FetchMemoryResourceMonitorsByAgentID", ctx, agentID) + ret0, _ := ret[0].(database.WorkspaceAgentMemoryResourceMonitor) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FetchMemoryResourceMonitorsByAgentID indicates an expected call of FetchMemoryResourceMonitorsByAgentID. +func (mr *MockStoreMockRecorder) FetchMemoryResourceMonitorsByAgentID(ctx, agentID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchMemoryResourceMonitorsByAgentID", reflect.TypeOf((*MockStore)(nil).FetchMemoryResourceMonitorsByAgentID), ctx, agentID) +} + +// FetchMemoryResourceMonitorsUpdatedAfter mocks base method. +func (m *MockStore) FetchMemoryResourceMonitorsUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]database.WorkspaceAgentMemoryResourceMonitor, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FetchMemoryResourceMonitorsUpdatedAfter", ctx, updatedAt) + ret0, _ := ret[0].([]database.WorkspaceAgentMemoryResourceMonitor) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FetchMemoryResourceMonitorsUpdatedAfter indicates an expected call of FetchMemoryResourceMonitorsUpdatedAfter. +func (mr *MockStoreMockRecorder) FetchMemoryResourceMonitorsUpdatedAfter(ctx, updatedAt any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchMemoryResourceMonitorsUpdatedAfter", reflect.TypeOf((*MockStore)(nil).FetchMemoryResourceMonitorsUpdatedAfter), ctx, updatedAt) +} + // FetchNewMessageMetadata mocks base method. func (m *MockStore) FetchNewMessageMetadata(ctx context.Context, arg database.FetchNewMessageMetadataParams) (database.FetchNewMessageMetadataRow, error) { m.ctrl.T.Helper() @@ -786,6 +802,36 @@ func (mr *MockStoreMockRecorder) FetchNewMessageMetadata(ctx, arg any) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchNewMessageMetadata", reflect.TypeOf((*MockStore)(nil).FetchNewMessageMetadata), ctx, arg) } +// FetchVolumesResourceMonitorsByAgentID mocks base method. +func (m *MockStore) FetchVolumesResourceMonitorsByAgentID(ctx context.Context, agentID uuid.UUID) ([]database.WorkspaceAgentVolumeResourceMonitor, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FetchVolumesResourceMonitorsByAgentID", ctx, agentID) + ret0, _ := ret[0].([]database.WorkspaceAgentVolumeResourceMonitor) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FetchVolumesResourceMonitorsByAgentID indicates an expected call of FetchVolumesResourceMonitorsByAgentID. +func (mr *MockStoreMockRecorder) FetchVolumesResourceMonitorsByAgentID(ctx, agentID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchVolumesResourceMonitorsByAgentID", reflect.TypeOf((*MockStore)(nil).FetchVolumesResourceMonitorsByAgentID), ctx, agentID) +} + +// FetchVolumesResourceMonitorsUpdatedAfter mocks base method. +func (m *MockStore) FetchVolumesResourceMonitorsUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]database.WorkspaceAgentVolumeResourceMonitor, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FetchVolumesResourceMonitorsUpdatedAfter", ctx, updatedAt) + ret0, _ := ret[0].([]database.WorkspaceAgentVolumeResourceMonitor) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FetchVolumesResourceMonitorsUpdatedAfter indicates an expected call of FetchVolumesResourceMonitorsUpdatedAfter. +func (mr *MockStoreMockRecorder) FetchVolumesResourceMonitorsUpdatedAfter(ctx, updatedAt any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchVolumesResourceMonitorsUpdatedAfter", reflect.TypeOf((*MockStore)(nil).FetchVolumesResourceMonitorsUpdatedAfter), ctx, updatedAt) +} + // GetAPIKeyByID mocks base method. func (m *MockStore) GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error) { m.ctrl.T.Helper() @@ -1746,6 +1792,21 @@ func (mr *MockStoreMockRecorder) GetNotificationsSettings(ctx any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationsSettings", reflect.TypeOf((*MockStore)(nil).GetNotificationsSettings), ctx) } +// GetOAuth2GithubDefaultEligible mocks base method. +func (m *MockStore) GetOAuth2GithubDefaultEligible(ctx context.Context) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetOAuth2GithubDefaultEligible", ctx) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetOAuth2GithubDefaultEligible indicates an expected call of GetOAuth2GithubDefaultEligible. +func (mr *MockStoreMockRecorder) GetOAuth2GithubDefaultEligible(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOAuth2GithubDefaultEligible", reflect.TypeOf((*MockStore)(nil).GetOAuth2GithubDefaultEligible), ctx) +} + // GetOAuth2ProviderAppByID mocks base method. func (m *MockStore) GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (database.OAuth2ProviderApp, error) { m.ctrl.T.Helper() @@ -1912,18 +1973,18 @@ func (mr *MockStoreMockRecorder) GetOrganizationByID(ctx, id any) *gomock.Call { } // GetOrganizationByName mocks base method. -func (m *MockStore) GetOrganizationByName(ctx context.Context, name string) (database.Organization, error) { +func (m *MockStore) GetOrganizationByName(ctx context.Context, arg database.GetOrganizationByNameParams) (database.Organization, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetOrganizationByName", ctx, name) + ret := m.ctrl.Call(m, "GetOrganizationByName", ctx, arg) ret0, _ := ret[0].(database.Organization) ret1, _ := ret[1].(error) return ret0, ret1 } // GetOrganizationByName indicates an expected call of GetOrganizationByName. -func (mr *MockStoreMockRecorder) GetOrganizationByName(ctx, name any) *gomock.Call { +func (mr *MockStoreMockRecorder) GetOrganizationByName(ctx, arg any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrganizationByName", reflect.TypeOf((*MockStore)(nil).GetOrganizationByName), ctx, name) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrganizationByName", reflect.TypeOf((*MockStore)(nil).GetOrganizationByName), ctx, arg) } // GetOrganizationIDsByMemberIDs mocks base method. @@ -1957,18 +2018,18 @@ func (mr *MockStoreMockRecorder) GetOrganizations(ctx, arg any) *gomock.Call { } // GetOrganizationsByUserID mocks base method. -func (m *MockStore) GetOrganizationsByUserID(ctx context.Context, userID uuid.UUID) ([]database.Organization, error) { +func (m *MockStore) GetOrganizationsByUserID(ctx context.Context, arg database.GetOrganizationsByUserIDParams) ([]database.Organization, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetOrganizationsByUserID", ctx, userID) + ret := m.ctrl.Call(m, "GetOrganizationsByUserID", ctx, arg) ret0, _ := ret[0].([]database.Organization) ret1, _ := ret[1].(error) return ret0, ret1 } // GetOrganizationsByUserID indicates an expected call of GetOrganizationsByUserID. -func (mr *MockStoreMockRecorder) GetOrganizationsByUserID(ctx, userID any) *gomock.Call { +func (mr *MockStoreMockRecorder) GetOrganizationsByUserID(ctx, arg any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrganizationsByUserID", reflect.TypeOf((*MockStore)(nil).GetOrganizationsByUserID), ctx, userID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrganizationsByUserID", reflect.TypeOf((*MockStore)(nil).GetOrganizationsByUserID), ctx, arg) } // GetParameterSchemasByJobID mocks base method. @@ -1986,6 +2047,51 @@ func (mr *MockStoreMockRecorder) GetParameterSchemasByJobID(ctx, jobID any) *gom return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetParameterSchemasByJobID", reflect.TypeOf((*MockStore)(nil).GetParameterSchemasByJobID), ctx, jobID) } +// GetPresetByWorkspaceBuildID mocks base method. +func (m *MockStore) GetPresetByWorkspaceBuildID(ctx context.Context, workspaceBuildID uuid.UUID) (database.TemplateVersionPreset, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPresetByWorkspaceBuildID", ctx, workspaceBuildID) + ret0, _ := ret[0].(database.TemplateVersionPreset) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPresetByWorkspaceBuildID indicates an expected call of GetPresetByWorkspaceBuildID. +func (mr *MockStoreMockRecorder) GetPresetByWorkspaceBuildID(ctx, workspaceBuildID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPresetByWorkspaceBuildID", reflect.TypeOf((*MockStore)(nil).GetPresetByWorkspaceBuildID), ctx, workspaceBuildID) +} + +// GetPresetParametersByTemplateVersionID mocks base method. +func (m *MockStore) GetPresetParametersByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]database.TemplateVersionPresetParameter, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPresetParametersByTemplateVersionID", ctx, templateVersionID) + ret0, _ := ret[0].([]database.TemplateVersionPresetParameter) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPresetParametersByTemplateVersionID indicates an expected call of GetPresetParametersByTemplateVersionID. +func (mr *MockStoreMockRecorder) GetPresetParametersByTemplateVersionID(ctx, templateVersionID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPresetParametersByTemplateVersionID", reflect.TypeOf((*MockStore)(nil).GetPresetParametersByTemplateVersionID), ctx, templateVersionID) +} + +// GetPresetsByTemplateVersionID mocks base method. +func (m *MockStore) GetPresetsByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]database.TemplateVersionPreset, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPresetsByTemplateVersionID", ctx, templateVersionID) + ret0, _ := ret[0].([]database.TemplateVersionPreset) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPresetsByTemplateVersionID indicates an expected call of GetPresetsByTemplateVersionID. +func (mr *MockStoreMockRecorder) GetPresetsByTemplateVersionID(ctx, templateVersionID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPresetsByTemplateVersionID", reflect.TypeOf((*MockStore)(nil).GetPresetsByTemplateVersionID), ctx, templateVersionID) +} + // GetPreviousTemplateVersion mocks base method. func (m *MockStore) GetPreviousTemplateVersion(ctx context.Context, arg database.GetPreviousTemplateVersionParams) (database.TemplateVersion, error) { m.ctrl.T.Helper() @@ -2346,6 +2452,36 @@ func (mr *MockStoreMockRecorder) GetTailnetTunnelPeerIDs(ctx, srcID any) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTailnetTunnelPeerIDs", reflect.TypeOf((*MockStore)(nil).GetTailnetTunnelPeerIDs), ctx, srcID) } +// GetTelemetryItem mocks base method. +func (m *MockStore) GetTelemetryItem(ctx context.Context, key string) (database.TelemetryItem, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTelemetryItem", ctx, key) + ret0, _ := ret[0].(database.TelemetryItem) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetTelemetryItem indicates an expected call of GetTelemetryItem. +func (mr *MockStoreMockRecorder) GetTelemetryItem(ctx, key any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTelemetryItem", reflect.TypeOf((*MockStore)(nil).GetTelemetryItem), ctx, key) +} + +// GetTelemetryItems mocks base method. +func (m *MockStore) GetTelemetryItems(ctx context.Context) ([]database.TelemetryItem, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTelemetryItems", ctx) + ret0, _ := ret[0].([]database.TelemetryItem) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetTelemetryItems indicates an expected call of GetTelemetryItems. +func (mr *MockStoreMockRecorder) GetTelemetryItems(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTelemetryItems", reflect.TypeOf((*MockStore)(nil).GetTelemetryItems), ctx) +} + // GetTemplateAppInsights mocks base method. func (m *MockStore) GetTemplateAppInsights(ctx context.Context, arg database.GetTemplateAppInsightsParams) ([]database.GetTemplateAppInsightsRow, error) { m.ctrl.T.Helper() @@ -3871,6 +4007,21 @@ func (mr *MockStoreMockRecorder) InsertLicense(ctx, arg any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertLicense", reflect.TypeOf((*MockStore)(nil).InsertLicense), ctx, arg) } +// InsertMemoryResourceMonitor mocks base method. +func (m *MockStore) InsertMemoryResourceMonitor(ctx context.Context, arg database.InsertMemoryResourceMonitorParams) (database.WorkspaceAgentMemoryResourceMonitor, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InsertMemoryResourceMonitor", ctx, arg) + ret0, _ := ret[0].(database.WorkspaceAgentMemoryResourceMonitor) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// InsertMemoryResourceMonitor indicates an expected call of InsertMemoryResourceMonitor. +func (mr *MockStoreMockRecorder) InsertMemoryResourceMonitor(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertMemoryResourceMonitor", reflect.TypeOf((*MockStore)(nil).InsertMemoryResourceMonitor), ctx, arg) +} + // InsertMissingGroups mocks base method. func (m *MockStore) InsertMissingGroups(ctx context.Context, arg database.InsertMissingGroupsParams) ([]database.Group, error) { m.ctrl.T.Helper() @@ -3976,6 +4127,36 @@ func (mr *MockStoreMockRecorder) InsertOrganizationMember(ctx, arg any) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertOrganizationMember", reflect.TypeOf((*MockStore)(nil).InsertOrganizationMember), ctx, arg) } +// InsertPreset mocks base method. +func (m *MockStore) InsertPreset(ctx context.Context, arg database.InsertPresetParams) (database.TemplateVersionPreset, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InsertPreset", ctx, arg) + ret0, _ := ret[0].(database.TemplateVersionPreset) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// InsertPreset indicates an expected call of InsertPreset. +func (mr *MockStoreMockRecorder) InsertPreset(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertPreset", reflect.TypeOf((*MockStore)(nil).InsertPreset), ctx, arg) +} + +// InsertPresetParameters mocks base method. +func (m *MockStore) InsertPresetParameters(ctx context.Context, arg database.InsertPresetParametersParams) ([]database.TemplateVersionPresetParameter, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InsertPresetParameters", ctx, arg) + ret0, _ := ret[0].([]database.TemplateVersionPresetParameter) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// InsertPresetParameters indicates an expected call of InsertPresetParameters. +func (mr *MockStoreMockRecorder) InsertPresetParameters(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertPresetParameters", reflect.TypeOf((*MockStore)(nil).InsertPresetParameters), ctx, arg) +} + // InsertProvisionerJob mocks base method. func (m *MockStore) InsertProvisionerJob(ctx context.Context, arg database.InsertProvisionerJobParams) (database.ProvisionerJob, error) { m.ctrl.T.Helper() @@ -4051,6 +4232,20 @@ func (mr *MockStoreMockRecorder) InsertReplica(ctx, arg any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertReplica", reflect.TypeOf((*MockStore)(nil).InsertReplica), ctx, arg) } +// InsertTelemetryItemIfNotExists mocks base method. +func (m *MockStore) InsertTelemetryItemIfNotExists(ctx context.Context, arg database.InsertTelemetryItemIfNotExistsParams) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InsertTelemetryItemIfNotExists", ctx, arg) + ret0, _ := ret[0].(error) + return ret0 +} + +// InsertTelemetryItemIfNotExists indicates an expected call of InsertTelemetryItemIfNotExists. +func (mr *MockStoreMockRecorder) InsertTelemetryItemIfNotExists(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertTelemetryItemIfNotExists", reflect.TypeOf((*MockStore)(nil).InsertTelemetryItemIfNotExists), ctx, arg) +} + // InsertTemplate mocks base method. func (m *MockStore) InsertTemplate(ctx context.Context, arg database.InsertTemplateParams) error { m.ctrl.T.Helper() @@ -4183,6 +4378,21 @@ func (mr *MockStoreMockRecorder) InsertUserLink(ctx, arg any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertUserLink", reflect.TypeOf((*MockStore)(nil).InsertUserLink), ctx, arg) } +// InsertVolumeResourceMonitor mocks base method. +func (m *MockStore) InsertVolumeResourceMonitor(ctx context.Context, arg database.InsertVolumeResourceMonitorParams) (database.WorkspaceAgentVolumeResourceMonitor, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InsertVolumeResourceMonitor", ctx, arg) + ret0, _ := ret[0].(database.WorkspaceAgentVolumeResourceMonitor) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// InsertVolumeResourceMonitor indicates an expected call of InsertVolumeResourceMonitor. +func (mr *MockStoreMockRecorder) InsertVolumeResourceMonitor(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertVolumeResourceMonitor", reflect.TypeOf((*MockStore)(nil).InsertVolumeResourceMonitor), ctx, arg) +} + // InsertWorkspace mocks base method. func (m *MockStore) InsertWorkspace(ctx context.Context, arg database.InsertWorkspaceParams) (database.WorkspaceTable, error) { m.ctrl.T.Helper() @@ -4786,6 +4996,20 @@ func (mr *MockStoreMockRecorder) UpdateMemberRoles(ctx, arg any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateMemberRoles", reflect.TypeOf((*MockStore)(nil).UpdateMemberRoles), ctx, arg) } +// UpdateMemoryResourceMonitor mocks base method. +func (m *MockStore) UpdateMemoryResourceMonitor(ctx context.Context, arg database.UpdateMemoryResourceMonitorParams) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateMemoryResourceMonitor", ctx, arg) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateMemoryResourceMonitor indicates an expected call of UpdateMemoryResourceMonitor. +func (mr *MockStoreMockRecorder) UpdateMemoryResourceMonitor(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateMemoryResourceMonitor", reflect.TypeOf((*MockStore)(nil).UpdateMemoryResourceMonitor), ctx, arg) +} + // UpdateNotificationTemplateMethodByID mocks base method. func (m *MockStore) UpdateNotificationTemplateMethodByID(ctx context.Context, arg database.UpdateNotificationTemplateMethodByIDParams) (database.NotificationTemplate, error) { m.ctrl.T.Helper() @@ -4846,6 +5070,20 @@ func (mr *MockStoreMockRecorder) UpdateOrganization(ctx, arg any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateOrganization", reflect.TypeOf((*MockStore)(nil).UpdateOrganization), ctx, arg) } +// UpdateOrganizationDeletedByID mocks base method. +func (m *MockStore) UpdateOrganizationDeletedByID(ctx context.Context, arg database.UpdateOrganizationDeletedByIDParams) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateOrganizationDeletedByID", ctx, arg) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateOrganizationDeletedByID indicates an expected call of UpdateOrganizationDeletedByID. +func (mr *MockStoreMockRecorder) UpdateOrganizationDeletedByID(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateOrganizationDeletedByID", reflect.TypeOf((*MockStore)(nil).UpdateOrganizationDeletedByID), ctx, arg) +} + // UpdateProvisionerDaemonLastSeenAt mocks base method. func (m *MockStore) UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg database.UpdateProvisionerDaemonLastSeenAtParams) error { m.ctrl.T.Helper() @@ -5277,6 +5515,20 @@ func (mr *MockStoreMockRecorder) UpdateUserStatus(ctx, arg any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserStatus", reflect.TypeOf((*MockStore)(nil).UpdateUserStatus), ctx, arg) } +// UpdateVolumeResourceMonitor mocks base method. +func (m *MockStore) UpdateVolumeResourceMonitor(ctx context.Context, arg database.UpdateVolumeResourceMonitorParams) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateVolumeResourceMonitor", ctx, arg) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateVolumeResourceMonitor indicates an expected call of UpdateVolumeResourceMonitor. +func (mr *MockStoreMockRecorder) UpdateVolumeResourceMonitor(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateVolumeResourceMonitor", reflect.TypeOf((*MockStore)(nil).UpdateVolumeResourceMonitor), ctx, arg) +} + // UpdateWorkspace mocks base method. func (m *MockStore) UpdateWorkspace(ctx context.Context, arg database.UpdateWorkspaceParams) (database.WorkspaceTable, error) { m.ctrl.T.Helper() @@ -5729,6 +5981,20 @@ func (mr *MockStoreMockRecorder) UpsertNotificationsSettings(ctx, value any) *go return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertNotificationsSettings", reflect.TypeOf((*MockStore)(nil).UpsertNotificationsSettings), ctx, value) } +// UpsertOAuth2GithubDefaultEligible mocks base method. +func (m *MockStore) UpsertOAuth2GithubDefaultEligible(ctx context.Context, eligible bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpsertOAuth2GithubDefaultEligible", ctx, eligible) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpsertOAuth2GithubDefaultEligible indicates an expected call of UpsertOAuth2GithubDefaultEligible. +func (mr *MockStoreMockRecorder) UpsertOAuth2GithubDefaultEligible(ctx, eligible any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertOAuth2GithubDefaultEligible", reflect.TypeOf((*MockStore)(nil).UpsertOAuth2GithubDefaultEligible), ctx, eligible) +} + // UpsertOAuthSigningKey mocks base method. func (m *MockStore) UpsertOAuthSigningKey(ctx context.Context, value string) error { m.ctrl.T.Helper() @@ -5861,6 +6127,20 @@ func (mr *MockStoreMockRecorder) UpsertTailnetTunnel(ctx, arg any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertTailnetTunnel", reflect.TypeOf((*MockStore)(nil).UpsertTailnetTunnel), ctx, arg) } +// UpsertTelemetryItem mocks base method. +func (m *MockStore) UpsertTelemetryItem(ctx context.Context, arg database.UpsertTelemetryItemParams) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpsertTelemetryItem", ctx, arg) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpsertTelemetryItem indicates an expected call of UpsertTelemetryItem. +func (mr *MockStoreMockRecorder) UpsertTelemetryItem(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertTelemetryItem", reflect.TypeOf((*MockStore)(nil).UpsertTelemetryItem), ctx, arg) +} + // UpsertTemplateUsageStats mocks base method. func (m *MockStore) UpsertTemplateUsageStats(ctx context.Context) error { m.ctrl.T.Helper() diff --git a/coderd/database/dbpurge/dbpurge.go b/coderd/database/dbpurge/dbpurge.go index e9c22611f1879..b7a308cfd6a06 100644 --- a/coderd/database/dbpurge/dbpurge.go +++ b/coderd/database/dbpurge/dbpurge.go @@ -63,7 +63,7 @@ func New(ctx context.Context, logger slog.Logger, db database.Store, clk quartz. return xerrors.Errorf("failed to delete old notification messages: %w", err) } - logger.Info(ctx, "purged old database entries", slog.F("duration", clk.Since(start))) + logger.Debug(ctx, "purged old database entries", slog.F("duration", clk.Since(start))) return nil }, database.DefaultTXOptions().WithID("db_purge")); err != nil { diff --git a/coderd/database/dbpurge/dbpurge_test.go b/coderd/database/dbpurge/dbpurge_test.go index 4677602328c89..3b21b1076cceb 100644 --- a/coderd/database/dbpurge/dbpurge_test.go +++ b/coderd/database/dbpurge/dbpurge_test.go @@ -66,7 +66,7 @@ func TestDeleteOldWorkspaceAgentStats(t *testing.T) { defer func() { if t.Failed() { - t.Logf("Test failed, printing rows...") + t.Log("Test failed, printing rows...") ctx := testutil.Context(t, testutil.WaitShort) buf := &bytes.Buffer{} enc := json.NewEncoder(buf) @@ -413,7 +413,7 @@ func TestDeleteOldProvisionerDaemons(t *testing.T) { Version: "1.0.0", APIVersion: proto.CurrentVersion.String(), OrganizationID: defaultOrg.ID, - KeyID: uuid.MustParse(codersdk.ProvisionerKeyIDBuiltIn), + KeyID: codersdk.ProvisionerKeyUUIDBuiltIn, }) require.NoError(t, err) _, err = db.UpsertProvisionerDaemon(ctx, database.UpsertProvisionerDaemonParams{ @@ -426,7 +426,7 @@ func TestDeleteOldProvisionerDaemons(t *testing.T) { Version: "1.0.0", APIVersion: proto.CurrentVersion.String(), OrganizationID: defaultOrg.ID, - KeyID: uuid.MustParse(codersdk.ProvisionerKeyIDBuiltIn), + KeyID: codersdk.ProvisionerKeyUUIDBuiltIn, }) require.NoError(t, err) _, err = db.UpsertProvisionerDaemon(ctx, database.UpsertProvisionerDaemonParams{ @@ -441,7 +441,7 @@ func TestDeleteOldProvisionerDaemons(t *testing.T) { Version: "1.0.0", APIVersion: proto.CurrentVersion.String(), OrganizationID: defaultOrg.ID, - KeyID: uuid.MustParse(codersdk.ProvisionerKeyIDBuiltIn), + KeyID: codersdk.ProvisionerKeyUUIDBuiltIn, }) require.NoError(t, err) _, err = db.UpsertProvisionerDaemon(ctx, database.UpsertProvisionerDaemonParams{ @@ -457,7 +457,7 @@ func TestDeleteOldProvisionerDaemons(t *testing.T) { Version: "1.0.0", APIVersion: proto.CurrentVersion.String(), OrganizationID: defaultOrg.ID, - KeyID: uuid.MustParse(codersdk.ProvisionerKeyIDBuiltIn), + KeyID: codersdk.ProvisionerKeyUUIDBuiltIn, }) require.NoError(t, err) diff --git a/coderd/database/dbtestutil/postgres_test.go b/coderd/database/dbtestutil/postgres_test.go index d4aaacdf909d8..f1b9336d57b37 100644 --- a/coderd/database/dbtestutil/postgres_test.go +++ b/coderd/database/dbtestutil/postgres_test.go @@ -1,5 +1,3 @@ -//go:build linux - package dbtestutil_test import ( @@ -21,6 +19,9 @@ func TestMain(m *testing.M) { func TestOpen(t *testing.T) { t.Parallel() + if !dbtestutil.WillUsePostgres() { + t.Skip("this test requires postgres") + } connect, err := dbtestutil.Open(t) require.NoError(t, err) @@ -35,6 +36,9 @@ func TestOpen(t *testing.T) { func TestOpen_InvalidDBFrom(t *testing.T) { t.Parallel() + if !dbtestutil.WillUsePostgres() { + t.Skip("this test requires postgres") + } _, err := dbtestutil.Open(t, dbtestutil.WithDBFrom("__invalid__")) require.Error(t, err) @@ -44,6 +48,9 @@ func TestOpen_InvalidDBFrom(t *testing.T) { func TestOpen_ValidDBFrom(t *testing.T) { t.Parallel() + if !dbtestutil.WillUsePostgres() { + t.Skip("this test requires postgres") + } // first check if we can create a new template db dsn, err := dbtestutil.Open(t, dbtestutil.WithDBFrom("")) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index c241548e166c2..e05d3a06d31f5 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -25,7 +25,11 @@ CREATE TYPE audit_action AS ENUM ( 'login', 'logout', 'register', - 'request_password_reset' + 'request_password_reset', + 'connect', + 'disconnect', + 'open', + 'close' ); CREATE TYPE automatic_updates AS ENUM ( @@ -201,7 +205,9 @@ CREATE TYPE resource_type AS ENUM ( 'notification_template', 'idp_sync_settings_organization', 'idp_sync_settings_group', - 'idp_sync_settings_role' + 'idp_sync_settings_role', + 'workspace_agent', + 'workspace_app' ); CREATE TYPE startup_script_behavior AS ENUM ( @@ -238,6 +244,11 @@ CREATE TYPE workspace_agent_lifecycle_state AS ENUM ( 'off' ); +CREATE TYPE workspace_agent_monitor_state AS ENUM ( + 'OK', + 'NOK' +); + CREATE TYPE workspace_agent_script_timing_stage AS ENUM ( 'start', 'stop', @@ -427,6 +438,74 @@ BEGIN END; $$; +CREATE FUNCTION protect_deleting_organizations() RETURNS trigger + LANGUAGE plpgsql + AS $$ +DECLARE + workspace_count int; + template_count int; + group_count int; + member_count int; + provisioner_keys_count int; +BEGIN + workspace_count := ( + SELECT count(*) as count FROM workspaces + WHERE + workspaces.organization_id = OLD.id + AND workspaces.deleted = false + ); + + template_count := ( + SELECT count(*) as count FROM templates + WHERE + templates.organization_id = OLD.id + AND templates.deleted = false + ); + + group_count := ( + SELECT count(*) as count FROM groups + WHERE + groups.organization_id = OLD.id + ); + + member_count := ( + SELECT count(*) as count FROM organization_members + WHERE + organization_members.organization_id = OLD.id + ); + + provisioner_keys_count := ( + Select count(*) as count FROM provisioner_keys + WHERE + provisioner_keys.organization_id = OLD.id + ); + + -- Fail the deletion if one of the following: + -- * the organization has 1 or more workspaces + -- * the organization has 1 or more templates + -- * the organization has 1 or more groups other than "Everyone" group + -- * the organization has 1 or more members other than the organization owner + -- * the organization has 1 or more provisioner keys + + IF (workspace_count + template_count + provisioner_keys_count) > 0 THEN + RAISE EXCEPTION 'cannot delete organization: organization has % workspaces, % templates, and % provisioner keys that must be deleted first', workspace_count, template_count, provisioner_keys_count; + END IF; + + IF (group_count) > 1 THEN + RAISE EXCEPTION 'cannot delete organization: organization has % groups that must be deleted first', group_count - 1; + END IF; + + -- Allow 1 member to exist, because you cannot remove yourself. You can + -- remove everyone else. Ideally, we only omit the member that matches + -- the user_id of the caller, however in a trigger, the caller is unknown. + IF (member_count) > 1 THEN + RAISE EXCEPTION 'cannot delete organization: organization has % members that must be deleted first', member_count - 1; + END IF; + + RETURN NEW; +END; +$$; + CREATE FUNCTION provisioner_tagset_contains(provisioner_tags tagset, job_tags tagset) RETURNS boolean LANGUAGE plpgsql AS $$ @@ -956,7 +1035,8 @@ CREATE TABLE organizations ( updated_at timestamp with time zone NOT NULL, is_default boolean DEFAULT false NOT NULL, display_name text NOT NULL, - icon text DEFAULT ''::text NOT NULL + icon text DEFAULT ''::text NOT NULL, + deleted boolean DEFAULT false NOT NULL ); CREATE TABLE parameter_schemas ( @@ -1164,6 +1244,13 @@ CREATE TABLE tailnet_tunnels ( updated_at timestamp with time zone NOT NULL ); +CREATE TABLE telemetry_items ( + key text NOT NULL, + value text NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL +); + CREATE TABLE template_usage_stats ( start_time timestamp with time zone NOT NULL, end_time timestamp with time zone NOT NULL, @@ -1258,6 +1345,20 @@ COMMENT ON COLUMN template_version_parameters.display_order IS 'Specifies the or COMMENT ON COLUMN template_version_parameters.ephemeral IS 'The value of an ephemeral parameter will not be preserved between consecutive workspace builds.'; +CREATE TABLE template_version_preset_parameters ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + template_version_preset_id uuid NOT NULL, + name text NOT NULL, + value text NOT NULL +); + +CREATE TABLE template_version_presets ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + template_version_id uuid NOT NULL, + name text NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + CREATE TABLE template_version_variables ( template_version_id uuid NOT NULL, name text NOT NULL, @@ -1479,6 +1580,16 @@ CREATE UNLOGGED TABLE workspace_agent_logs ( log_source_id uuid DEFAULT '00000000-0000-0000-0000-000000000000'::uuid NOT NULL ); +CREATE TABLE workspace_agent_memory_resource_monitors ( + agent_id uuid NOT NULL, + enabled boolean NOT NULL, + threshold integer NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + state workspace_agent_monitor_state DEFAULT 'OK'::workspace_agent_monitor_state NOT NULL, + debounced_until timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL +); + CREATE UNLOGGED TABLE workspace_agent_metadata ( workspace_agent_id uuid NOT NULL, display_name character varying(127) NOT NULL, @@ -1556,6 +1667,17 @@ CREATE TABLE workspace_agent_stats ( usage boolean DEFAULT false NOT NULL ); +CREATE TABLE workspace_agent_volume_resource_monitors ( + agent_id uuid NOT NULL, + enabled boolean NOT NULL, + threshold integer NOT NULL, + path text NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + state workspace_agent_monitor_state DEFAULT 'OK'::workspace_agent_monitor_state NOT NULL, + debounced_until timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL +); + CREATE TABLE workspace_agents ( id uuid NOT NULL, created_at timestamp with time zone NOT NULL, @@ -1707,7 +1829,8 @@ CREATE TABLE workspace_builds ( deadline timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL, reason build_reason DEFAULT 'initiator'::build_reason NOT NULL, daily_cost integer DEFAULT 0 NOT NULL, - max_deadline timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL + max_deadline timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL, + template_version_preset_id uuid ); CREATE VIEW workspace_build_with_user AS @@ -1725,6 +1848,7 @@ CREATE VIEW workspace_build_with_user AS workspace_builds.reason, workspace_builds.daily_cost, workspace_builds.max_deadline, + workspace_builds.template_version_preset_id, COALESCE(visible_users.avatar_url, ''::text) AS initiator_by_avatar_url, COALESCE(visible_users.username, ''::text) AS initiator_by_username FROM (workspace_builds @@ -1975,9 +2099,6 @@ ALTER TABLE ONLY oauth2_provider_apps ALTER TABLE ONLY organization_members ADD CONSTRAINT organization_members_pkey PRIMARY KEY (organization_id, user_id); -ALTER TABLE ONLY organizations - ADD CONSTRAINT organizations_name UNIQUE (name); - ALTER TABLE ONLY organizations ADD CONSTRAINT organizations_pkey PRIMARY KEY (id); @@ -2026,12 +2147,21 @@ ALTER TABLE ONLY tailnet_peers ALTER TABLE ONLY tailnet_tunnels ADD CONSTRAINT tailnet_tunnels_pkey PRIMARY KEY (coordinator_id, src_id, dst_id); +ALTER TABLE ONLY telemetry_items + ADD CONSTRAINT telemetry_items_pkey PRIMARY KEY (key); + ALTER TABLE ONLY template_usage_stats ADD CONSTRAINT template_usage_stats_pkey PRIMARY KEY (start_time, template_id, user_id); ALTER TABLE ONLY template_version_parameters ADD CONSTRAINT template_version_parameters_template_version_id_name_key UNIQUE (template_version_id, name); +ALTER TABLE ONLY template_version_preset_parameters + ADD CONSTRAINT template_version_preset_parameters_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY template_version_presets + ADD CONSTRAINT template_version_presets_pkey PRIMARY KEY (id); + ALTER TABLE ONLY template_version_variables ADD CONSTRAINT template_version_variables_template_version_id_name_key UNIQUE (template_version_id, name); @@ -2062,6 +2192,9 @@ ALTER TABLE ONLY users ALTER TABLE ONLY workspace_agent_log_sources ADD CONSTRAINT workspace_agent_log_sources_pkey PRIMARY KEY (workspace_agent_id, id); +ALTER TABLE ONLY workspace_agent_memory_resource_monitors + ADD CONSTRAINT workspace_agent_memory_resource_monitors_pkey PRIMARY KEY (agent_id); + ALTER TABLE ONLY workspace_agent_metadata ADD CONSTRAINT workspace_agent_metadata_pkey PRIMARY KEY (workspace_agent_id, key); @@ -2077,6 +2210,9 @@ ALTER TABLE ONLY workspace_agent_scripts ALTER TABLE ONLY workspace_agent_logs ADD CONSTRAINT workspace_agent_startup_logs_pkey PRIMARY KEY (id); +ALTER TABLE ONLY workspace_agent_volume_resource_monitors + ADD CONSTRAINT workspace_agent_volume_resource_monitors_pkey PRIMARY KEY (agent_id, path); + ALTER TABLE ONLY workspace_agents ADD CONSTRAINT workspace_agents_pkey PRIMARY KEY (id); @@ -2148,9 +2284,7 @@ CREATE INDEX idx_organization_member_organization_id_uuid ON organization_member CREATE INDEX idx_organization_member_user_id_uuid ON organization_members USING btree (user_id); -CREATE UNIQUE INDEX idx_organization_name ON organizations USING btree (name); - -CREATE UNIQUE INDEX idx_organization_name_lower ON organizations USING btree (lower(name)); +CREATE UNIQUE INDEX idx_organization_name_lower ON organizations USING btree (lower(name)) WHERE (deleted = false); CREATE UNIQUE INDEX idx_provisioner_daemons_org_name_owner_key ON provisioner_daemons USING btree (organization_id, name, lower(COALESCE((tags ->> 'owner'::text), ''::text))); @@ -2282,6 +2416,8 @@ CREATE OR REPLACE VIEW provisioner_job_stats AS CREATE TRIGGER inhibit_enqueue_if_disabled BEFORE INSERT ON notification_messages FOR EACH ROW EXECUTE FUNCTION inhibit_enqueue_if_disabled(); +CREATE TRIGGER protect_deleting_organizations BEFORE UPDATE ON organizations FOR EACH ROW WHEN (((new.deleted = true) AND (old.deleted = false))) EXECUTE FUNCTION protect_deleting_organizations(); + CREATE TRIGGER remove_organization_member_custom_role BEFORE DELETE ON custom_roles FOR EACH ROW EXECUTE FUNCTION remove_organization_member_role(); COMMENT ON TRIGGER remove_organization_member_custom_role ON custom_roles IS 'When a custom_role is deleted, this trigger removes the role from all organization members.'; @@ -2416,6 +2552,12 @@ ALTER TABLE ONLY tailnet_tunnels ALTER TABLE ONLY template_version_parameters ADD CONSTRAINT template_version_parameters_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; +ALTER TABLE ONLY template_version_preset_parameters + ADD CONSTRAINT template_version_preset_paramet_template_version_preset_id_fkey FOREIGN KEY (template_version_preset_id) REFERENCES template_version_presets(id) ON DELETE CASCADE; + +ALTER TABLE ONLY template_version_presets + ADD CONSTRAINT template_version_presets_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; + ALTER TABLE ONLY template_version_variables ADD CONSTRAINT template_version_variables_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; @@ -2455,6 +2597,9 @@ ALTER TABLE ONLY user_status_changes ALTER TABLE ONLY workspace_agent_log_sources ADD CONSTRAINT workspace_agent_log_sources_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; +ALTER TABLE ONLY workspace_agent_memory_resource_monitors + ADD CONSTRAINT workspace_agent_memory_resource_monitors_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ALTER TABLE ONLY workspace_agent_metadata ADD CONSTRAINT workspace_agent_metadata_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; @@ -2470,6 +2615,9 @@ ALTER TABLE ONLY workspace_agent_scripts ALTER TABLE ONLY workspace_agent_logs ADD CONSTRAINT workspace_agent_startup_logs_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; +ALTER TABLE ONLY workspace_agent_volume_resource_monitors + ADD CONSTRAINT workspace_agent_volume_resource_monitors_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ALTER TABLE ONLY workspace_agents ADD CONSTRAINT workspace_agents_resource_id_fkey FOREIGN KEY (resource_id) REFERENCES workspace_resources(id) ON DELETE CASCADE; @@ -2494,6 +2642,9 @@ ALTER TABLE ONLY workspace_builds ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; +ALTER TABLE ONLY workspace_builds + ADD CONSTRAINT workspace_builds_template_version_preset_id_fkey FOREIGN KEY (template_version_preset_id) REFERENCES template_version_presets(id) ON DELETE SET NULL; + ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; diff --git a/coderd/database/foreign_key_constraint.go b/coderd/database/foreign_key_constraint.go index 52f98a679a71b..66c379a749e01 100644 --- a/coderd/database/foreign_key_constraint.go +++ b/coderd/database/foreign_key_constraint.go @@ -6,71 +6,76 @@ type ForeignKeyConstraint string // ForeignKeyConstraint enums. const ( - ForeignKeyAPIKeysUserIDUUID ForeignKeyConstraint = "api_keys_user_id_uuid_fkey" // ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyCryptoKeysSecretKeyID ForeignKeyConstraint = "crypto_keys_secret_key_id_fkey" // ALTER TABLE ONLY crypto_keys ADD CONSTRAINT crypto_keys_secret_key_id_fkey FOREIGN KEY (secret_key_id) REFERENCES dbcrypt_keys(active_key_digest); - ForeignKeyGitAuthLinksOauthAccessTokenKeyID ForeignKeyConstraint = "git_auth_links_oauth_access_token_key_id_fkey" // ALTER TABLE ONLY external_auth_links ADD CONSTRAINT git_auth_links_oauth_access_token_key_id_fkey FOREIGN KEY (oauth_access_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); - ForeignKeyGitAuthLinksOauthRefreshTokenKeyID ForeignKeyConstraint = "git_auth_links_oauth_refresh_token_key_id_fkey" // ALTER TABLE ONLY external_auth_links ADD CONSTRAINT git_auth_links_oauth_refresh_token_key_id_fkey FOREIGN KEY (oauth_refresh_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); - ForeignKeyGitSSHKeysUserID ForeignKeyConstraint = "gitsshkeys_user_id_fkey" // ALTER TABLE ONLY gitsshkeys ADD CONSTRAINT gitsshkeys_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id); - ForeignKeyGroupMembersGroupID ForeignKeyConstraint = "group_members_group_id_fkey" // ALTER TABLE ONLY group_members ADD CONSTRAINT group_members_group_id_fkey FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE; - ForeignKeyGroupMembersUserID ForeignKeyConstraint = "group_members_user_id_fkey" // ALTER TABLE ONLY group_members ADD CONSTRAINT group_members_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyGroupsOrganizationID ForeignKeyConstraint = "groups_organization_id_fkey" // ALTER TABLE ONLY groups ADD CONSTRAINT groups_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; - ForeignKeyJfrogXrayScansAgentID ForeignKeyConstraint = "jfrog_xray_scans_agent_id_fkey" // ALTER TABLE ONLY jfrog_xray_scans ADD CONSTRAINT jfrog_xray_scans_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; - ForeignKeyJfrogXrayScansWorkspaceID ForeignKeyConstraint = "jfrog_xray_scans_workspace_id_fkey" // ALTER TABLE ONLY jfrog_xray_scans ADD CONSTRAINT jfrog_xray_scans_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; - ForeignKeyNotificationMessagesNotificationTemplateID ForeignKeyConstraint = "notification_messages_notification_template_id_fkey" // ALTER TABLE ONLY notification_messages ADD CONSTRAINT notification_messages_notification_template_id_fkey FOREIGN KEY (notification_template_id) REFERENCES notification_templates(id) ON DELETE CASCADE; - ForeignKeyNotificationMessagesUserID ForeignKeyConstraint = "notification_messages_user_id_fkey" // ALTER TABLE ONLY notification_messages ADD CONSTRAINT notification_messages_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyNotificationPreferencesNotificationTemplateID ForeignKeyConstraint = "notification_preferences_notification_template_id_fkey" // ALTER TABLE ONLY notification_preferences ADD CONSTRAINT notification_preferences_notification_template_id_fkey FOREIGN KEY (notification_template_id) REFERENCES notification_templates(id) ON DELETE CASCADE; - ForeignKeyNotificationPreferencesUserID ForeignKeyConstraint = "notification_preferences_user_id_fkey" // ALTER TABLE ONLY notification_preferences ADD CONSTRAINT notification_preferences_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyOauth2ProviderAppCodesAppID ForeignKeyConstraint = "oauth2_provider_app_codes_app_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_codes ADD CONSTRAINT oauth2_provider_app_codes_app_id_fkey FOREIGN KEY (app_id) REFERENCES oauth2_provider_apps(id) ON DELETE CASCADE; - ForeignKeyOauth2ProviderAppCodesUserID ForeignKeyConstraint = "oauth2_provider_app_codes_user_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_codes ADD CONSTRAINT oauth2_provider_app_codes_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyOauth2ProviderAppSecretsAppID ForeignKeyConstraint = "oauth2_provider_app_secrets_app_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_secrets ADD CONSTRAINT oauth2_provider_app_secrets_app_id_fkey FOREIGN KEY (app_id) REFERENCES oauth2_provider_apps(id) ON DELETE CASCADE; - ForeignKeyOauth2ProviderAppTokensAPIKeyID ForeignKeyConstraint = "oauth2_provider_app_tokens_api_key_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_tokens ADD CONSTRAINT oauth2_provider_app_tokens_api_key_id_fkey FOREIGN KEY (api_key_id) REFERENCES api_keys(id) ON DELETE CASCADE; - ForeignKeyOauth2ProviderAppTokensAppSecretID ForeignKeyConstraint = "oauth2_provider_app_tokens_app_secret_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_tokens ADD CONSTRAINT oauth2_provider_app_tokens_app_secret_id_fkey FOREIGN KEY (app_secret_id) REFERENCES oauth2_provider_app_secrets(id) ON DELETE CASCADE; - ForeignKeyOrganizationMembersOrganizationIDUUID ForeignKeyConstraint = "organization_members_organization_id_uuid_fkey" // ALTER TABLE ONLY organization_members ADD CONSTRAINT organization_members_organization_id_uuid_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; - ForeignKeyOrganizationMembersUserIDUUID ForeignKeyConstraint = "organization_members_user_id_uuid_fkey" // ALTER TABLE ONLY organization_members ADD CONSTRAINT organization_members_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyParameterSchemasJobID ForeignKeyConstraint = "parameter_schemas_job_id_fkey" // ALTER TABLE ONLY parameter_schemas ADD CONSTRAINT parameter_schemas_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; - ForeignKeyProvisionerDaemonsKeyID ForeignKeyConstraint = "provisioner_daemons_key_id_fkey" // ALTER TABLE ONLY provisioner_daemons ADD CONSTRAINT provisioner_daemons_key_id_fkey FOREIGN KEY (key_id) REFERENCES provisioner_keys(id) ON DELETE CASCADE; - ForeignKeyProvisionerDaemonsOrganizationID ForeignKeyConstraint = "provisioner_daemons_organization_id_fkey" // ALTER TABLE ONLY provisioner_daemons ADD CONSTRAINT provisioner_daemons_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; - ForeignKeyProvisionerJobLogsJobID ForeignKeyConstraint = "provisioner_job_logs_job_id_fkey" // ALTER TABLE ONLY provisioner_job_logs ADD CONSTRAINT provisioner_job_logs_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; - ForeignKeyProvisionerJobTimingsJobID ForeignKeyConstraint = "provisioner_job_timings_job_id_fkey" // ALTER TABLE ONLY provisioner_job_timings ADD CONSTRAINT provisioner_job_timings_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; - ForeignKeyProvisionerJobsOrganizationID ForeignKeyConstraint = "provisioner_jobs_organization_id_fkey" // ALTER TABLE ONLY provisioner_jobs ADD CONSTRAINT provisioner_jobs_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; - ForeignKeyProvisionerKeysOrganizationID ForeignKeyConstraint = "provisioner_keys_organization_id_fkey" // ALTER TABLE ONLY provisioner_keys ADD CONSTRAINT provisioner_keys_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; - ForeignKeyTailnetAgentsCoordinatorID ForeignKeyConstraint = "tailnet_agents_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_agents ADD CONSTRAINT tailnet_agents_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; - ForeignKeyTailnetClientSubscriptionsCoordinatorID ForeignKeyConstraint = "tailnet_client_subscriptions_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_client_subscriptions ADD CONSTRAINT tailnet_client_subscriptions_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; - ForeignKeyTailnetClientsCoordinatorID ForeignKeyConstraint = "tailnet_clients_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_clients ADD CONSTRAINT tailnet_clients_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; - ForeignKeyTailnetPeersCoordinatorID ForeignKeyConstraint = "tailnet_peers_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_peers ADD CONSTRAINT tailnet_peers_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; - ForeignKeyTailnetTunnelsCoordinatorID ForeignKeyConstraint = "tailnet_tunnels_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_tunnels ADD CONSTRAINT tailnet_tunnels_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; - ForeignKeyTemplateVersionParametersTemplateVersionID ForeignKeyConstraint = "template_version_parameters_template_version_id_fkey" // ALTER TABLE ONLY template_version_parameters ADD CONSTRAINT template_version_parameters_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; - ForeignKeyTemplateVersionVariablesTemplateVersionID ForeignKeyConstraint = "template_version_variables_template_version_id_fkey" // ALTER TABLE ONLY template_version_variables ADD CONSTRAINT template_version_variables_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; - ForeignKeyTemplateVersionWorkspaceTagsTemplateVersionID ForeignKeyConstraint = "template_version_workspace_tags_template_version_id_fkey" // ALTER TABLE ONLY template_version_workspace_tags ADD CONSTRAINT template_version_workspace_tags_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; - ForeignKeyTemplateVersionsCreatedBy ForeignKeyConstraint = "template_versions_created_by_fkey" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_created_by_fkey FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE RESTRICT; - ForeignKeyTemplateVersionsOrganizationID ForeignKeyConstraint = "template_versions_organization_id_fkey" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; - ForeignKeyTemplateVersionsTemplateID ForeignKeyConstraint = "template_versions_template_id_fkey" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_template_id_fkey FOREIGN KEY (template_id) REFERENCES templates(id) ON DELETE CASCADE; - ForeignKeyTemplatesCreatedBy ForeignKeyConstraint = "templates_created_by_fkey" // ALTER TABLE ONLY templates ADD CONSTRAINT templates_created_by_fkey FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE RESTRICT; - ForeignKeyTemplatesOrganizationID ForeignKeyConstraint = "templates_organization_id_fkey" // ALTER TABLE ONLY templates ADD CONSTRAINT templates_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; - ForeignKeyUserDeletedUserID ForeignKeyConstraint = "user_deleted_user_id_fkey" // ALTER TABLE ONLY user_deleted ADD CONSTRAINT user_deleted_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id); - ForeignKeyUserLinksOauthAccessTokenKeyID ForeignKeyConstraint = "user_links_oauth_access_token_key_id_fkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_oauth_access_token_key_id_fkey FOREIGN KEY (oauth_access_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); - ForeignKeyUserLinksOauthRefreshTokenKeyID ForeignKeyConstraint = "user_links_oauth_refresh_token_key_id_fkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_oauth_refresh_token_key_id_fkey FOREIGN KEY (oauth_refresh_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); - ForeignKeyUserLinksUserID ForeignKeyConstraint = "user_links_user_id_fkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyUserStatusChangesUserID ForeignKeyConstraint = "user_status_changes_user_id_fkey" // ALTER TABLE ONLY user_status_changes ADD CONSTRAINT user_status_changes_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id); - ForeignKeyWorkspaceAgentLogSourcesWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_log_sources_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_log_sources ADD CONSTRAINT workspace_agent_log_sources_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAgentMetadataWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_metadata_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_metadata ADD CONSTRAINT workspace_agent_metadata_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAgentPortShareWorkspaceID ForeignKeyConstraint = "workspace_agent_port_share_workspace_id_fkey" // ALTER TABLE ONLY workspace_agent_port_share ADD CONSTRAINT workspace_agent_port_share_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAgentScriptTimingsScriptID ForeignKeyConstraint = "workspace_agent_script_timings_script_id_fkey" // ALTER TABLE ONLY workspace_agent_script_timings ADD CONSTRAINT workspace_agent_script_timings_script_id_fkey FOREIGN KEY (script_id) REFERENCES workspace_agent_scripts(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAgentScriptsWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_scripts_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_scripts ADD CONSTRAINT workspace_agent_scripts_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAgentStartupLogsAgentID ForeignKeyConstraint = "workspace_agent_startup_logs_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_logs ADD CONSTRAINT workspace_agent_startup_logs_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAgentsResourceID ForeignKeyConstraint = "workspace_agents_resource_id_fkey" // ALTER TABLE ONLY workspace_agents ADD CONSTRAINT workspace_agents_resource_id_fkey FOREIGN KEY (resource_id) REFERENCES workspace_resources(id) ON DELETE CASCADE; - ForeignKeyWorkspaceAppStatsAgentID ForeignKeyConstraint = "workspace_app_stats_agent_id_fkey" // ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id); - ForeignKeyWorkspaceAppStatsUserID ForeignKeyConstraint = "workspace_app_stats_user_id_fkey" // ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id); - ForeignKeyWorkspaceAppStatsWorkspaceID ForeignKeyConstraint = "workspace_app_stats_workspace_id_fkey" // ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id); - ForeignKeyWorkspaceAppsAgentID ForeignKeyConstraint = "workspace_apps_agent_id_fkey" // ALTER TABLE ONLY workspace_apps ADD CONSTRAINT workspace_apps_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; - ForeignKeyWorkspaceBuildParametersWorkspaceBuildID ForeignKeyConstraint = "workspace_build_parameters_workspace_build_id_fkey" // ALTER TABLE ONLY workspace_build_parameters ADD CONSTRAINT workspace_build_parameters_workspace_build_id_fkey FOREIGN KEY (workspace_build_id) REFERENCES workspace_builds(id) ON DELETE CASCADE; - ForeignKeyWorkspaceBuildsJobID ForeignKeyConstraint = "workspace_builds_job_id_fkey" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; - ForeignKeyWorkspaceBuildsTemplateVersionID ForeignKeyConstraint = "workspace_builds_template_version_id_fkey" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; - ForeignKeyWorkspaceBuildsWorkspaceID ForeignKeyConstraint = "workspace_builds_workspace_id_fkey" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; - ForeignKeyWorkspaceModulesJobID ForeignKeyConstraint = "workspace_modules_job_id_fkey" // ALTER TABLE ONLY workspace_modules ADD CONSTRAINT workspace_modules_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; - ForeignKeyWorkspaceResourceMetadataWorkspaceResourceID ForeignKeyConstraint = "workspace_resource_metadata_workspace_resource_id_fkey" // ALTER TABLE ONLY workspace_resource_metadata ADD CONSTRAINT workspace_resource_metadata_workspace_resource_id_fkey FOREIGN KEY (workspace_resource_id) REFERENCES workspace_resources(id) ON DELETE CASCADE; - ForeignKeyWorkspaceResourcesJobID ForeignKeyConstraint = "workspace_resources_job_id_fkey" // ALTER TABLE ONLY workspace_resources ADD CONSTRAINT workspace_resources_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; - ForeignKeyWorkspacesOrganizationID ForeignKeyConstraint = "workspaces_organization_id_fkey" // ALTER TABLE ONLY workspaces ADD CONSTRAINT workspaces_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE RESTRICT; - ForeignKeyWorkspacesOwnerID ForeignKeyConstraint = "workspaces_owner_id_fkey" // ALTER TABLE ONLY workspaces ADD CONSTRAINT workspaces_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE RESTRICT; - ForeignKeyWorkspacesTemplateID ForeignKeyConstraint = "workspaces_template_id_fkey" // ALTER TABLE ONLY workspaces ADD CONSTRAINT workspaces_template_id_fkey FOREIGN KEY (template_id) REFERENCES templates(id) ON DELETE RESTRICT; + ForeignKeyAPIKeysUserIDUUID ForeignKeyConstraint = "api_keys_user_id_uuid_fkey" // ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyCryptoKeysSecretKeyID ForeignKeyConstraint = "crypto_keys_secret_key_id_fkey" // ALTER TABLE ONLY crypto_keys ADD CONSTRAINT crypto_keys_secret_key_id_fkey FOREIGN KEY (secret_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ForeignKeyGitAuthLinksOauthAccessTokenKeyID ForeignKeyConstraint = "git_auth_links_oauth_access_token_key_id_fkey" // ALTER TABLE ONLY external_auth_links ADD CONSTRAINT git_auth_links_oauth_access_token_key_id_fkey FOREIGN KEY (oauth_access_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ForeignKeyGitAuthLinksOauthRefreshTokenKeyID ForeignKeyConstraint = "git_auth_links_oauth_refresh_token_key_id_fkey" // ALTER TABLE ONLY external_auth_links ADD CONSTRAINT git_auth_links_oauth_refresh_token_key_id_fkey FOREIGN KEY (oauth_refresh_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ForeignKeyGitSSHKeysUserID ForeignKeyConstraint = "gitsshkeys_user_id_fkey" // ALTER TABLE ONLY gitsshkeys ADD CONSTRAINT gitsshkeys_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id); + ForeignKeyGroupMembersGroupID ForeignKeyConstraint = "group_members_group_id_fkey" // ALTER TABLE ONLY group_members ADD CONSTRAINT group_members_group_id_fkey FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE; + ForeignKeyGroupMembersUserID ForeignKeyConstraint = "group_members_user_id_fkey" // ALTER TABLE ONLY group_members ADD CONSTRAINT group_members_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyGroupsOrganizationID ForeignKeyConstraint = "groups_organization_id_fkey" // ALTER TABLE ONLY groups ADD CONSTRAINT groups_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; + ForeignKeyJfrogXrayScansAgentID ForeignKeyConstraint = "jfrog_xray_scans_agent_id_fkey" // ALTER TABLE ONLY jfrog_xray_scans ADD CONSTRAINT jfrog_xray_scans_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyJfrogXrayScansWorkspaceID ForeignKeyConstraint = "jfrog_xray_scans_workspace_id_fkey" // ALTER TABLE ONLY jfrog_xray_scans ADD CONSTRAINT jfrog_xray_scans_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; + ForeignKeyNotificationMessagesNotificationTemplateID ForeignKeyConstraint = "notification_messages_notification_template_id_fkey" // ALTER TABLE ONLY notification_messages ADD CONSTRAINT notification_messages_notification_template_id_fkey FOREIGN KEY (notification_template_id) REFERENCES notification_templates(id) ON DELETE CASCADE; + ForeignKeyNotificationMessagesUserID ForeignKeyConstraint = "notification_messages_user_id_fkey" // ALTER TABLE ONLY notification_messages ADD CONSTRAINT notification_messages_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyNotificationPreferencesNotificationTemplateID ForeignKeyConstraint = "notification_preferences_notification_template_id_fkey" // ALTER TABLE ONLY notification_preferences ADD CONSTRAINT notification_preferences_notification_template_id_fkey FOREIGN KEY (notification_template_id) REFERENCES notification_templates(id) ON DELETE CASCADE; + ForeignKeyNotificationPreferencesUserID ForeignKeyConstraint = "notification_preferences_user_id_fkey" // ALTER TABLE ONLY notification_preferences ADD CONSTRAINT notification_preferences_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyOauth2ProviderAppCodesAppID ForeignKeyConstraint = "oauth2_provider_app_codes_app_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_codes ADD CONSTRAINT oauth2_provider_app_codes_app_id_fkey FOREIGN KEY (app_id) REFERENCES oauth2_provider_apps(id) ON DELETE CASCADE; + ForeignKeyOauth2ProviderAppCodesUserID ForeignKeyConstraint = "oauth2_provider_app_codes_user_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_codes ADD CONSTRAINT oauth2_provider_app_codes_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyOauth2ProviderAppSecretsAppID ForeignKeyConstraint = "oauth2_provider_app_secrets_app_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_secrets ADD CONSTRAINT oauth2_provider_app_secrets_app_id_fkey FOREIGN KEY (app_id) REFERENCES oauth2_provider_apps(id) ON DELETE CASCADE; + ForeignKeyOauth2ProviderAppTokensAPIKeyID ForeignKeyConstraint = "oauth2_provider_app_tokens_api_key_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_tokens ADD CONSTRAINT oauth2_provider_app_tokens_api_key_id_fkey FOREIGN KEY (api_key_id) REFERENCES api_keys(id) ON DELETE CASCADE; + ForeignKeyOauth2ProviderAppTokensAppSecretID ForeignKeyConstraint = "oauth2_provider_app_tokens_app_secret_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_tokens ADD CONSTRAINT oauth2_provider_app_tokens_app_secret_id_fkey FOREIGN KEY (app_secret_id) REFERENCES oauth2_provider_app_secrets(id) ON DELETE CASCADE; + ForeignKeyOrganizationMembersOrganizationIDUUID ForeignKeyConstraint = "organization_members_organization_id_uuid_fkey" // ALTER TABLE ONLY organization_members ADD CONSTRAINT organization_members_organization_id_uuid_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; + ForeignKeyOrganizationMembersUserIDUUID ForeignKeyConstraint = "organization_members_user_id_uuid_fkey" // ALTER TABLE ONLY organization_members ADD CONSTRAINT organization_members_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyParameterSchemasJobID ForeignKeyConstraint = "parameter_schemas_job_id_fkey" // ALTER TABLE ONLY parameter_schemas ADD CONSTRAINT parameter_schemas_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; + ForeignKeyProvisionerDaemonsKeyID ForeignKeyConstraint = "provisioner_daemons_key_id_fkey" // ALTER TABLE ONLY provisioner_daemons ADD CONSTRAINT provisioner_daemons_key_id_fkey FOREIGN KEY (key_id) REFERENCES provisioner_keys(id) ON DELETE CASCADE; + ForeignKeyProvisionerDaemonsOrganizationID ForeignKeyConstraint = "provisioner_daemons_organization_id_fkey" // ALTER TABLE ONLY provisioner_daemons ADD CONSTRAINT provisioner_daemons_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; + ForeignKeyProvisionerJobLogsJobID ForeignKeyConstraint = "provisioner_job_logs_job_id_fkey" // ALTER TABLE ONLY provisioner_job_logs ADD CONSTRAINT provisioner_job_logs_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; + ForeignKeyProvisionerJobTimingsJobID ForeignKeyConstraint = "provisioner_job_timings_job_id_fkey" // ALTER TABLE ONLY provisioner_job_timings ADD CONSTRAINT provisioner_job_timings_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; + ForeignKeyProvisionerJobsOrganizationID ForeignKeyConstraint = "provisioner_jobs_organization_id_fkey" // ALTER TABLE ONLY provisioner_jobs ADD CONSTRAINT provisioner_jobs_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; + ForeignKeyProvisionerKeysOrganizationID ForeignKeyConstraint = "provisioner_keys_organization_id_fkey" // ALTER TABLE ONLY provisioner_keys ADD CONSTRAINT provisioner_keys_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; + ForeignKeyTailnetAgentsCoordinatorID ForeignKeyConstraint = "tailnet_agents_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_agents ADD CONSTRAINT tailnet_agents_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; + ForeignKeyTailnetClientSubscriptionsCoordinatorID ForeignKeyConstraint = "tailnet_client_subscriptions_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_client_subscriptions ADD CONSTRAINT tailnet_client_subscriptions_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; + ForeignKeyTailnetClientsCoordinatorID ForeignKeyConstraint = "tailnet_clients_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_clients ADD CONSTRAINT tailnet_clients_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; + ForeignKeyTailnetPeersCoordinatorID ForeignKeyConstraint = "tailnet_peers_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_peers ADD CONSTRAINT tailnet_peers_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; + ForeignKeyTailnetTunnelsCoordinatorID ForeignKeyConstraint = "tailnet_tunnels_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_tunnels ADD CONSTRAINT tailnet_tunnels_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; + ForeignKeyTemplateVersionParametersTemplateVersionID ForeignKeyConstraint = "template_version_parameters_template_version_id_fkey" // ALTER TABLE ONLY template_version_parameters ADD CONSTRAINT template_version_parameters_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; + ForeignKeyTemplateVersionPresetParametTemplateVersionPresetID ForeignKeyConstraint = "template_version_preset_paramet_template_version_preset_id_fkey" // ALTER TABLE ONLY template_version_preset_parameters ADD CONSTRAINT template_version_preset_paramet_template_version_preset_id_fkey FOREIGN KEY (template_version_preset_id) REFERENCES template_version_presets(id) ON DELETE CASCADE; + ForeignKeyTemplateVersionPresetsTemplateVersionID ForeignKeyConstraint = "template_version_presets_template_version_id_fkey" // ALTER TABLE ONLY template_version_presets ADD CONSTRAINT template_version_presets_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; + ForeignKeyTemplateVersionVariablesTemplateVersionID ForeignKeyConstraint = "template_version_variables_template_version_id_fkey" // ALTER TABLE ONLY template_version_variables ADD CONSTRAINT template_version_variables_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; + ForeignKeyTemplateVersionWorkspaceTagsTemplateVersionID ForeignKeyConstraint = "template_version_workspace_tags_template_version_id_fkey" // ALTER TABLE ONLY template_version_workspace_tags ADD CONSTRAINT template_version_workspace_tags_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; + ForeignKeyTemplateVersionsCreatedBy ForeignKeyConstraint = "template_versions_created_by_fkey" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_created_by_fkey FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE RESTRICT; + ForeignKeyTemplateVersionsOrganizationID ForeignKeyConstraint = "template_versions_organization_id_fkey" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; + ForeignKeyTemplateVersionsTemplateID ForeignKeyConstraint = "template_versions_template_id_fkey" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_template_id_fkey FOREIGN KEY (template_id) REFERENCES templates(id) ON DELETE CASCADE; + ForeignKeyTemplatesCreatedBy ForeignKeyConstraint = "templates_created_by_fkey" // ALTER TABLE ONLY templates ADD CONSTRAINT templates_created_by_fkey FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE RESTRICT; + ForeignKeyTemplatesOrganizationID ForeignKeyConstraint = "templates_organization_id_fkey" // ALTER TABLE ONLY templates ADD CONSTRAINT templates_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; + ForeignKeyUserDeletedUserID ForeignKeyConstraint = "user_deleted_user_id_fkey" // ALTER TABLE ONLY user_deleted ADD CONSTRAINT user_deleted_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id); + ForeignKeyUserLinksOauthAccessTokenKeyID ForeignKeyConstraint = "user_links_oauth_access_token_key_id_fkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_oauth_access_token_key_id_fkey FOREIGN KEY (oauth_access_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ForeignKeyUserLinksOauthRefreshTokenKeyID ForeignKeyConstraint = "user_links_oauth_refresh_token_key_id_fkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_oauth_refresh_token_key_id_fkey FOREIGN KEY (oauth_refresh_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ForeignKeyUserLinksUserID ForeignKeyConstraint = "user_links_user_id_fkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyUserStatusChangesUserID ForeignKeyConstraint = "user_status_changes_user_id_fkey" // ALTER TABLE ONLY user_status_changes ADD CONSTRAINT user_status_changes_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id); + ForeignKeyWorkspaceAgentLogSourcesWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_log_sources_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_log_sources ADD CONSTRAINT workspace_agent_log_sources_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAgentMemoryResourceMonitorsAgentID ForeignKeyConstraint = "workspace_agent_memory_resource_monitors_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_memory_resource_monitors ADD CONSTRAINT workspace_agent_memory_resource_monitors_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAgentMetadataWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_metadata_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_metadata ADD CONSTRAINT workspace_agent_metadata_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAgentPortShareWorkspaceID ForeignKeyConstraint = "workspace_agent_port_share_workspace_id_fkey" // ALTER TABLE ONLY workspace_agent_port_share ADD CONSTRAINT workspace_agent_port_share_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAgentScriptTimingsScriptID ForeignKeyConstraint = "workspace_agent_script_timings_script_id_fkey" // ALTER TABLE ONLY workspace_agent_script_timings ADD CONSTRAINT workspace_agent_script_timings_script_id_fkey FOREIGN KEY (script_id) REFERENCES workspace_agent_scripts(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAgentScriptsWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_scripts_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_scripts ADD CONSTRAINT workspace_agent_scripts_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAgentStartupLogsAgentID ForeignKeyConstraint = "workspace_agent_startup_logs_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_logs ADD CONSTRAINT workspace_agent_startup_logs_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAgentVolumeResourceMonitorsAgentID ForeignKeyConstraint = "workspace_agent_volume_resource_monitors_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_volume_resource_monitors ADD CONSTRAINT workspace_agent_volume_resource_monitors_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAgentsResourceID ForeignKeyConstraint = "workspace_agents_resource_id_fkey" // ALTER TABLE ONLY workspace_agents ADD CONSTRAINT workspace_agents_resource_id_fkey FOREIGN KEY (resource_id) REFERENCES workspace_resources(id) ON DELETE CASCADE; + ForeignKeyWorkspaceAppStatsAgentID ForeignKeyConstraint = "workspace_app_stats_agent_id_fkey" // ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id); + ForeignKeyWorkspaceAppStatsUserID ForeignKeyConstraint = "workspace_app_stats_user_id_fkey" // ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id); + ForeignKeyWorkspaceAppStatsWorkspaceID ForeignKeyConstraint = "workspace_app_stats_workspace_id_fkey" // ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id); + ForeignKeyWorkspaceAppsAgentID ForeignKeyConstraint = "workspace_apps_agent_id_fkey" // ALTER TABLE ONLY workspace_apps ADD CONSTRAINT workspace_apps_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; + ForeignKeyWorkspaceBuildParametersWorkspaceBuildID ForeignKeyConstraint = "workspace_build_parameters_workspace_build_id_fkey" // ALTER TABLE ONLY workspace_build_parameters ADD CONSTRAINT workspace_build_parameters_workspace_build_id_fkey FOREIGN KEY (workspace_build_id) REFERENCES workspace_builds(id) ON DELETE CASCADE; + ForeignKeyWorkspaceBuildsJobID ForeignKeyConstraint = "workspace_builds_job_id_fkey" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; + ForeignKeyWorkspaceBuildsTemplateVersionID ForeignKeyConstraint = "workspace_builds_template_version_id_fkey" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; + ForeignKeyWorkspaceBuildsTemplateVersionPresetID ForeignKeyConstraint = "workspace_builds_template_version_preset_id_fkey" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_template_version_preset_id_fkey FOREIGN KEY (template_version_preset_id) REFERENCES template_version_presets(id) ON DELETE SET NULL; + ForeignKeyWorkspaceBuildsWorkspaceID ForeignKeyConstraint = "workspace_builds_workspace_id_fkey" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; + ForeignKeyWorkspaceModulesJobID ForeignKeyConstraint = "workspace_modules_job_id_fkey" // ALTER TABLE ONLY workspace_modules ADD CONSTRAINT workspace_modules_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; + ForeignKeyWorkspaceResourceMetadataWorkspaceResourceID ForeignKeyConstraint = "workspace_resource_metadata_workspace_resource_id_fkey" // ALTER TABLE ONLY workspace_resource_metadata ADD CONSTRAINT workspace_resource_metadata_workspace_resource_id_fkey FOREIGN KEY (workspace_resource_id) REFERENCES workspace_resources(id) ON DELETE CASCADE; + ForeignKeyWorkspaceResourcesJobID ForeignKeyConstraint = "workspace_resources_job_id_fkey" // ALTER TABLE ONLY workspace_resources ADD CONSTRAINT workspace_resources_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; + ForeignKeyWorkspacesOrganizationID ForeignKeyConstraint = "workspaces_organization_id_fkey" // ALTER TABLE ONLY workspaces ADD CONSTRAINT workspaces_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE RESTRICT; + ForeignKeyWorkspacesOwnerID ForeignKeyConstraint = "workspaces_owner_id_fkey" // ALTER TABLE ONLY workspaces ADD CONSTRAINT workspaces_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE RESTRICT; + ForeignKeyWorkspacesTemplateID ForeignKeyConstraint = "workspaces_template_id_fkey" // ALTER TABLE ONLY workspaces ADD CONSTRAINT workspaces_template_id_fkey FOREIGN KEY (template_id) REFERENCES templates(id) ON DELETE RESTRICT; ) diff --git a/coderd/database/migrations/000288_telemetry_items.down.sql b/coderd/database/migrations/000288_telemetry_items.down.sql new file mode 100644 index 0000000000000..118188f519e76 --- /dev/null +++ b/coderd/database/migrations/000288_telemetry_items.down.sql @@ -0,0 +1 @@ +DROP TABLE telemetry_items; diff --git a/coderd/database/migrations/000288_telemetry_items.up.sql b/coderd/database/migrations/000288_telemetry_items.up.sql new file mode 100644 index 0000000000000..40279827788d6 --- /dev/null +++ b/coderd/database/migrations/000288_telemetry_items.up.sql @@ -0,0 +1,6 @@ +CREATE TABLE telemetry_items ( + key TEXT NOT NULL PRIMARY KEY, + value TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() +); diff --git a/coderd/database/migrations/000289_agent_resource_monitors.down.sql b/coderd/database/migrations/000289_agent_resource_monitors.down.sql new file mode 100644 index 0000000000000..ba8f63af23f56 --- /dev/null +++ b/coderd/database/migrations/000289_agent_resource_monitors.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS workspace_agent_memory_resource_monitors; +DROP TABLE IF EXISTS workspace_agent_volume_resource_monitors; diff --git a/coderd/database/migrations/000289_agent_resource_monitors.up.sql b/coderd/database/migrations/000289_agent_resource_monitors.up.sql new file mode 100644 index 0000000000000..335507bdaf609 --- /dev/null +++ b/coderd/database/migrations/000289_agent_resource_monitors.up.sql @@ -0,0 +1,16 @@ +CREATE TABLE workspace_agent_memory_resource_monitors ( + agent_id uuid NOT NULL REFERENCES workspace_agents(id) ON DELETE CASCADE, + enabled boolean NOT NULL, + threshold integer NOT NULL, + created_at timestamp with time zone NOT NULL, + PRIMARY KEY (agent_id) +); + +CREATE TABLE workspace_agent_volume_resource_monitors ( + agent_id uuid NOT NULL REFERENCES workspace_agents(id) ON DELETE CASCADE, + enabled boolean NOT NULL, + threshold integer NOT NULL, + path text NOT NULL, + created_at timestamp with time zone NOT NULL, + PRIMARY KEY (agent_id, path) +); diff --git a/coderd/database/migrations/000290_oom_and_ood_notification.down.sql b/coderd/database/migrations/000290_oom_and_ood_notification.down.sql new file mode 100644 index 0000000000000..a7d54ccf6ec7a --- /dev/null +++ b/coderd/database/migrations/000290_oom_and_ood_notification.down.sql @@ -0,0 +1,2 @@ +DELETE FROM notification_templates WHERE id = 'f047f6a3-5713-40f7-85aa-0394cce9fa3a'; +DELETE FROM notification_templates WHERE id = 'a9d027b4-ac49-4fb1-9f6d-45af15f64e7a'; diff --git a/coderd/database/migrations/000290_oom_and_ood_notification.up.sql b/coderd/database/migrations/000290_oom_and_ood_notification.up.sql new file mode 100644 index 0000000000000..f0489606bb5b9 --- /dev/null +++ b/coderd/database/migrations/000290_oom_and_ood_notification.up.sql @@ -0,0 +1,40 @@ +INSERT INTO notification_templates + (id, name, title_template, body_template, "group", actions) +VALUES ( + 'a9d027b4-ac49-4fb1-9f6d-45af15f64e7a', + 'Workspace Out Of Memory', + E'Your workspace "{{.Labels.workspace}}" is low on memory', + E'Hi {{.UserName}},\n\n'|| + E'Your workspace **{{.Labels.workspace}}** has reached the memory usage threshold set at **{{.Labels.threshold}}**.', + 'Workspace Events', + '[ + { + "label": "View workspace", + "url": "{{base_url}}/@{{.UserUsername}}/{{.Labels.workspace}}" + } + ]'::jsonb +); + +INSERT INTO notification_templates + (id, name, title_template, body_template, "group", actions) +VALUES ( + 'f047f6a3-5713-40f7-85aa-0394cce9fa3a', + 'Workspace Out Of Disk', + E'Your workspace "{{.Labels.workspace}}" is low on volume space', + E'Hi {{.UserName}},\n\n'|| + E'{{ if eq (len .Data.volumes) 1 }}{{ $volume := index .Data.volumes 0 }}'|| + E'Volume **`{{$volume.path}}`** is over {{$volume.threshold}} full in workspace **{{.Labels.workspace}}**.'|| + E'{{ else }}'|| + E'The following volumes are nearly full in workspace **{{.Labels.workspace}}**\n\n'|| + E'{{ range $volume := .Data.volumes }}'|| + E'- **`{{$volume.path}}`** is over {{$volume.threshold}} full\n'|| + E'{{ end }}'|| + E'{{ end }}', + 'Workspace Events', + '[ + { + "label": "View workspace", + "url": "{{base_url}}/@{{.UserUsername}}/{{.Labels.workspace}}" + } + ]'::jsonb +); diff --git a/coderd/database/migrations/000291_workspace_parameter_presets.down.sql b/coderd/database/migrations/000291_workspace_parameter_presets.down.sql new file mode 100644 index 0000000000000..487c4b1ab6a0c --- /dev/null +++ b/coderd/database/migrations/000291_workspace_parameter_presets.down.sql @@ -0,0 +1,29 @@ +-- DROP the workspace_build_with_user view so that we can recreate without +-- workspace_builds.template_version_preset_id below. We need to drop the view +-- before dropping workspace_builds.template_version_preset_id because the view +-- references it. We can only recreate the view after dropping the column, +-- because the view needs to be created without the column. +DROP VIEW workspace_build_with_user; + +ALTER TABLE workspace_builds +DROP COLUMN template_version_preset_id; + +DROP TABLE template_version_preset_parameters; + +DROP TABLE template_version_presets; + +CREATE VIEW + workspace_build_with_user +AS +SELECT + workspace_builds.*, + coalesce(visible_users.avatar_url, '') AS initiator_by_avatar_url, + coalesce(visible_users.username, '') AS initiator_by_username +FROM + workspace_builds + LEFT JOIN + visible_users + ON + workspace_builds.initiator_id = visible_users.id; + +COMMENT ON VIEW workspace_build_with_user IS 'Joins in the username + avatar url of the initiated by user.'; diff --git a/coderd/database/migrations/000291_workspace_parameter_presets.up.sql b/coderd/database/migrations/000291_workspace_parameter_presets.up.sql new file mode 100644 index 0000000000000..d4a768081ec05 --- /dev/null +++ b/coderd/database/migrations/000291_workspace_parameter_presets.up.sql @@ -0,0 +1,44 @@ +CREATE TABLE template_version_presets +( + id UUID PRIMARY KEY NOT NULL, + template_version_id UUID NOT NULL, + name TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (template_version_id) REFERENCES template_versions (id) ON DELETE CASCADE +); + +CREATE TABLE template_version_preset_parameters +( + id UUID PRIMARY KEY NOT NULL, + template_version_preset_id UUID NOT NULL, + name TEXT NOT NULL, + value TEXT NOT NULL, + FOREIGN KEY (template_version_preset_id) REFERENCES template_version_presets (id) ON DELETE CASCADE +); + +ALTER TABLE workspace_builds +ADD COLUMN template_version_preset_id UUID NULL; + +ALTER TABLE workspace_builds +ADD CONSTRAINT workspace_builds_template_version_preset_id_fkey +FOREIGN KEY (template_version_preset_id) +REFERENCES template_version_presets (id) +ON DELETE SET NULL; + +-- Recreate the view to include the new column. +DROP VIEW workspace_build_with_user; +CREATE VIEW + workspace_build_with_user +AS +SELECT + workspace_builds.*, + coalesce(visible_users.avatar_url, '') AS initiator_by_avatar_url, + coalesce(visible_users.username, '') AS initiator_by_username +FROM + workspace_builds + LEFT JOIN + visible_users + ON + workspace_builds.initiator_id = visible_users.id; + +COMMENT ON VIEW workspace_build_with_user IS 'Joins in the username + avatar url of the initiated by user.'; diff --git a/coderd/database/migrations/000292_generate_default_preset_parameter_ids.down.sql b/coderd/database/migrations/000292_generate_default_preset_parameter_ids.down.sql new file mode 100644 index 0000000000000..0cb92a2619d22 --- /dev/null +++ b/coderd/database/migrations/000292_generate_default_preset_parameter_ids.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE template_version_presets +ALTER COLUMN id DROP DEFAULT; + +ALTER TABLE template_version_preset_parameters +ALTER COLUMN id DROP DEFAULT; diff --git a/coderd/database/migrations/000292_generate_default_preset_parameter_ids.up.sql b/coderd/database/migrations/000292_generate_default_preset_parameter_ids.up.sql new file mode 100644 index 0000000000000..9801d1f37cdc5 --- /dev/null +++ b/coderd/database/migrations/000292_generate_default_preset_parameter_ids.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE template_version_presets +ALTER COLUMN id SET DEFAULT gen_random_uuid(); + +ALTER TABLE template_version_preset_parameters +ALTER COLUMN id SET DEFAULT gen_random_uuid(); diff --git a/coderd/database/migrations/000293_add_audit_types_for_connect_and_open.down.sql b/coderd/database/migrations/000293_add_audit_types_for_connect_and_open.down.sql new file mode 100644 index 0000000000000..35020b349fc4e --- /dev/null +++ b/coderd/database/migrations/000293_add_audit_types_for_connect_and_open.down.sql @@ -0,0 +1 @@ +-- No-op, enum values can't be dropped. diff --git a/coderd/database/migrations/000293_add_audit_types_for_connect_and_open.up.sql b/coderd/database/migrations/000293_add_audit_types_for_connect_and_open.up.sql new file mode 100644 index 0000000000000..b894a45eaf443 --- /dev/null +++ b/coderd/database/migrations/000293_add_audit_types_for_connect_and_open.up.sql @@ -0,0 +1,13 @@ +-- Add new audit types for connect and open actions. +ALTER TYPE audit_action + ADD VALUE IF NOT EXISTS 'connect'; +ALTER TYPE audit_action + ADD VALUE IF NOT EXISTS 'disconnect'; +ALTER TYPE resource_type + ADD VALUE IF NOT EXISTS 'workspace_agent'; +ALTER TYPE audit_action + ADD VALUE IF NOT EXISTS 'open'; +ALTER TYPE audit_action + ADD VALUE IF NOT EXISTS 'close'; +ALTER TYPE resource_type + ADD VALUE IF NOT EXISTS 'workspace_app'; diff --git a/coderd/database/migrations/000294_workspace_monitors_state.down.sql b/coderd/database/migrations/000294_workspace_monitors_state.down.sql new file mode 100644 index 0000000000000..c3c6ce7c614ac --- /dev/null +++ b/coderd/database/migrations/000294_workspace_monitors_state.down.sql @@ -0,0 +1,11 @@ +ALTER TABLE workspace_agent_volume_resource_monitors + DROP COLUMN updated_at, + DROP COLUMN state, + DROP COLUMN debounced_until; + +ALTER TABLE workspace_agent_memory_resource_monitors + DROP COLUMN updated_at, + DROP COLUMN state, + DROP COLUMN debounced_until; + +DROP TYPE workspace_agent_monitor_state; diff --git a/coderd/database/migrations/000294_workspace_monitors_state.up.sql b/coderd/database/migrations/000294_workspace_monitors_state.up.sql new file mode 100644 index 0000000000000..a6b1f7609d7da --- /dev/null +++ b/coderd/database/migrations/000294_workspace_monitors_state.up.sql @@ -0,0 +1,14 @@ +CREATE TYPE workspace_agent_monitor_state AS ENUM ( + 'OK', + 'NOK' +); + +ALTER TABLE workspace_agent_memory_resource_monitors + ADD COLUMN updated_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + ADD COLUMN state workspace_agent_monitor_state NOT NULL DEFAULT 'OK', + ADD COLUMN debounced_until timestamp with time zone NOT NULL DEFAULT '0001-01-01 00:00:00'::timestamptz; + +ALTER TABLE workspace_agent_volume_resource_monitors + ADD COLUMN updated_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + ADD COLUMN state workspace_agent_monitor_state NOT NULL DEFAULT 'OK', + ADD COLUMN debounced_until timestamp with time zone NOT NULL DEFAULT '0001-01-01 00:00:00'::timestamptz; diff --git a/coderd/database/migrations/000295_test_notification.down.sql b/coderd/database/migrations/000295_test_notification.down.sql new file mode 100644 index 0000000000000..f2e3558c8e4cc --- /dev/null +++ b/coderd/database/migrations/000295_test_notification.down.sql @@ -0,0 +1 @@ +DELETE FROM notification_templates WHERE id = 'c425f63e-716a-4bf4-ae24-78348f706c3f'; diff --git a/coderd/database/migrations/000295_test_notification.up.sql b/coderd/database/migrations/000295_test_notification.up.sql new file mode 100644 index 0000000000000..19c9e3655e89f --- /dev/null +++ b/coderd/database/migrations/000295_test_notification.up.sql @@ -0,0 +1,16 @@ +INSERT INTO notification_templates + (id, name, title_template, body_template, "group", actions) +VALUES ( + 'c425f63e-716a-4bf4-ae24-78348f706c3f', + 'Test Notification', + E'A test notification', + E'Hi {{.UserName}},\n\n'|| + E'This is a test notification.', + 'Notification Events', + '[ + { + "label": "View notification settings", + "url": "{{base_url}}/deployment/notifications?tab=settings" + } + ]'::jsonb +); diff --git a/coderd/database/migrations/000296_organization_soft_delete.down.sql b/coderd/database/migrations/000296_organization_soft_delete.down.sql new file mode 100644 index 0000000000000..3db107e8a79f5 --- /dev/null +++ b/coderd/database/migrations/000296_organization_soft_delete.down.sql @@ -0,0 +1,12 @@ +DROP INDEX IF EXISTS idx_organization_name_lower; + +CREATE UNIQUE INDEX IF NOT EXISTS idx_organization_name ON organizations USING btree (name); +CREATE UNIQUE INDEX IF NOT EXISTS idx_organization_name_lower ON organizations USING btree (lower(name)); + +ALTER TABLE ONLY organizations + ADD CONSTRAINT organizations_name UNIQUE (name); + +DROP TRIGGER IF EXISTS protect_deleting_organizations ON organizations; +DROP FUNCTION IF EXISTS protect_deleting_organizations; + +ALTER TABLE organizations DROP COLUMN deleted; diff --git a/coderd/database/migrations/000296_organization_soft_delete.up.sql b/coderd/database/migrations/000296_organization_soft_delete.up.sql new file mode 100644 index 0000000000000..34b25139c950a --- /dev/null +++ b/coderd/database/migrations/000296_organization_soft_delete.up.sql @@ -0,0 +1,85 @@ +ALTER TABLE organizations ADD COLUMN deleted boolean DEFAULT FALSE NOT NULL; + +DROP INDEX IF EXISTS idx_organization_name; +DROP INDEX IF EXISTS idx_organization_name_lower; + +CREATE UNIQUE INDEX IF NOT EXISTS idx_organization_name_lower ON organizations USING btree (lower(name)) + where deleted = false; + +ALTER TABLE ONLY organizations + DROP CONSTRAINT IF EXISTS organizations_name; + +CREATE FUNCTION protect_deleting_organizations() + RETURNS TRIGGER AS +$$ +DECLARE + workspace_count int; + template_count int; + group_count int; + member_count int; + provisioner_keys_count int; +BEGIN + workspace_count := ( + SELECT count(*) as count FROM workspaces + WHERE + workspaces.organization_id = OLD.id + AND workspaces.deleted = false + ); + + template_count := ( + SELECT count(*) as count FROM templates + WHERE + templates.organization_id = OLD.id + AND templates.deleted = false + ); + + group_count := ( + SELECT count(*) as count FROM groups + WHERE + groups.organization_id = OLD.id + ); + + member_count := ( + SELECT count(*) as count FROM organization_members + WHERE + organization_members.organization_id = OLD.id + ); + + provisioner_keys_count := ( + Select count(*) as count FROM provisioner_keys + WHERE + provisioner_keys.organization_id = OLD.id + ); + + -- Fail the deletion if one of the following: + -- * the organization has 1 or more workspaces + -- * the organization has 1 or more templates + -- * the organization has 1 or more groups other than "Everyone" group + -- * the organization has 1 or more members other than the organization owner + -- * the organization has 1 or more provisioner keys + + IF (workspace_count + template_count + provisioner_keys_count) > 0 THEN + RAISE EXCEPTION 'cannot delete organization: organization has % workspaces, % templates, and % provisioner keys that must be deleted first', workspace_count, template_count, provisioner_keys_count; + END IF; + + IF (group_count) > 1 THEN + RAISE EXCEPTION 'cannot delete organization: organization has % groups that must be deleted first', group_count - 1; + END IF; + + -- Allow 1 member to exist, because you cannot remove yourself. You can + -- remove everyone else. Ideally, we only omit the member that matches + -- the user_id of the caller, however in a trigger, the caller is unknown. + IF (member_count) > 1 THEN + RAISE EXCEPTION 'cannot delete organization: organization has % members that must be deleted first', member_count - 1; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Trigger to protect organizations from being soft deleted with existing resources +CREATE TRIGGER protect_deleting_organizations + BEFORE UPDATE ON organizations + FOR EACH ROW + WHEN (NEW.deleted = true AND OLD.deleted = false) + EXECUTE FUNCTION protect_deleting_organizations(); diff --git a/coderd/database/migrations/migrate_test.go b/coderd/database/migrations/migrate_test.go index 7d016f7978fb1..bd347af0be1ea 100644 --- a/coderd/database/migrations/migrate_test.go +++ b/coderd/database/migrations/migrate_test.go @@ -1,5 +1,3 @@ -//go:build linux - package migrations_test import ( @@ -283,9 +281,9 @@ func TestMigrateUpWithFixtures(t *testing.T) { } } if len(emptyTables) > 0 { - t.Logf("The following tables have zero rows, consider adding fixtures for them or create a full database dump:") + t.Log("The following tables have zero rows, consider adding fixtures for them or create a full database dump:") t.Errorf("tables have zero rows: %v", emptyTables) - t.Logf("See https://github.com/coder/coder/blob/main/docs/CONTRIBUTING.md#database-fixtures-for-testing-migrations for more information") + t.Log("See https://github.com/coder/coder/blob/main/docs/CONTRIBUTING.md#database-fixtures-for-testing-migrations for more information") } }) diff --git a/coderd/database/migrations/testdata/fixtures/000288_telemetry_items.up.sql b/coderd/database/migrations/testdata/fixtures/000288_telemetry_items.up.sql new file mode 100644 index 0000000000000..0189558292915 --- /dev/null +++ b/coderd/database/migrations/testdata/fixtures/000288_telemetry_items.up.sql @@ -0,0 +1,4 @@ +INSERT INTO + telemetry_items (key, value) +VALUES + ('example_key', 'example_value'); diff --git a/coderd/database/migrations/testdata/fixtures/000289_agent_resource_monitors.up.sql b/coderd/database/migrations/testdata/fixtures/000289_agent_resource_monitors.up.sql new file mode 100644 index 0000000000000..a103b8a979f70 --- /dev/null +++ b/coderd/database/migrations/testdata/fixtures/000289_agent_resource_monitors.up.sql @@ -0,0 +1,30 @@ +INSERT INTO + workspace_agent_memory_resource_monitors ( + agent_id, + enabled, + threshold, + created_at + ) + VALUES ( + '45e89705-e09d-4850-bcec-f9a937f5d78d', -- uuid + true, + 90, + '2024-01-01 00:00:00' + ); + +INSERT INTO + workspace_agent_volume_resource_monitors ( + agent_id, + path, + enabled, + threshold, + created_at + ) + VALUES ( + '45e89705-e09d-4850-bcec-f9a937f5d78d', -- uuid + '/', + true, + 90, + '2024-01-01 00:00:00' + ); + diff --git a/coderd/database/migrations/testdata/fixtures/000291_workspace_parameter_presets.up.sql b/coderd/database/migrations/testdata/fixtures/000291_workspace_parameter_presets.up.sql new file mode 100644 index 0000000000000..8eebf58e3f39c --- /dev/null +++ b/coderd/database/migrations/testdata/fixtures/000291_workspace_parameter_presets.up.sql @@ -0,0 +1,10 @@ +INSERT INTO public.organizations (id, name, description, created_at, updated_at, is_default, display_name, icon) VALUES ('20362772-802a-4a72-8e4f-3648b4bfd168', 'strange_hopper58', 'wizardly_stonebraker60', '2025-02-07 07:46:19.507551 +00:00', '2025-02-07 07:46:19.507552 +00:00', false, 'competent_rhodes59', ''); + +INSERT INTO public.users (id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at) VALUES ('6c353aac-20de-467b-bdfb-3c30a37adcd2', 'vigorous_murdock61', 'affectionate_hawking62', 'lqTu9C5363AwD7NVNH6noaGjp91XIuZJ', '2025-02-07 07:46:19.510861 +00:00', '2025-02-07 07:46:19.512949 +00:00', 'active', '{}', 'password', '', false, '0001-01-01 00:00:00.000000', '', '', 'vigilant_hugle63', null, null, null); + +INSERT INTO public.templates (id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level) VALUES ('6b298946-7a4f-47ac-9158-b03b08740a41', '2025-02-07 07:46:19.513317 +00:00', '2025-02-07 07:46:19.513317 +00:00', '20362772-802a-4a72-8e4f-3648b4bfd168', false, 'modest_leakey64', 'echo', 'e6cfa2a4-e4cf-4182-9e19-08b975682a28', 'upbeat_wright65', 604800000000000, '6c353aac-20de-467b-bdfb-3c30a37adcd2', 'nervous_keller66', '{}', '{"20362772-802a-4a72-8e4f-3648b4bfd168": ["read", "use"]}', 'determined_aryabhata67', false, true, true, 0, 0, 0, 0, 0, 0, false, '', 3600000000000, 'owner'); +INSERT INTO public.template_versions (id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, source_example_id) VALUES ('af58bd62-428c-4c33-849b-d43a3be07d93', '6b298946-7a4f-47ac-9158-b03b08740a41', '20362772-802a-4a72-8e4f-3648b4bfd168', '2025-02-07 07:46:19.514782 +00:00', '2025-02-07 07:46:19.514782 +00:00', 'distracted_shockley68', 'sleepy_turing69', 'f2e2ea1c-5aa3-4a1d-8778-2e5071efae59', '6c353aac-20de-467b-bdfb-3c30a37adcd2', '[]', '', false, null); + +INSERT INTO public.template_version_presets (id, template_version_id, name, created_at) VALUES ('28b42cc0-c4fe-4907-a0fe-e4d20f1e9bfe', 'af58bd62-428c-4c33-849b-d43a3be07d93', 'test', '0001-01-01 00:00:00.000000 +00:00'); + +INSERT INTO public.template_version_preset_parameters (id, template_version_preset_id, name, value) VALUES ('ea90ccd2-5024-459e-87e4-879afd24de0f', '28b42cc0-c4fe-4907-a0fe-e4d20f1e9bfe', 'test', 'test'); diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go index 63e03ccb27f40..803cfbf01ced2 100644 --- a/coderd/database/modelmethods.go +++ b/coderd/database/modelmethods.go @@ -277,8 +277,10 @@ func (p GetEligibleProvisionerDaemonsByProvisionerJobIDsRow) RBACObject() rbac.O return p.ProvisionerDaemon.RBACObject() } +// RBACObject for a provisioner key is the same as a provisioner daemon. +// Keys == provisioners from a RBAC perspective. func (p ProvisionerKey) RBACObject() rbac.Object { - return rbac.ResourceProvisionerKeys. + return rbac.ResourceProvisionerDaemon. WithID(p.ID). InOrg(p.OrganizationID) } @@ -527,3 +529,31 @@ func (k CryptoKey) CanVerify(now time.Time) bool { func (r GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow) RBACObject() rbac.Object { return r.ProvisionerJob.RBACObject() } + +func (m WorkspaceAgentMemoryResourceMonitor) Debounce( + by time.Duration, + now time.Time, + oldState, newState WorkspaceAgentMonitorState, +) (time.Time, bool) { + if now.After(m.DebouncedUntil) && + oldState == WorkspaceAgentMonitorStateOK && + newState == WorkspaceAgentMonitorStateNOK { + return now.Add(by), true + } + + return m.DebouncedUntil, false +} + +func (m WorkspaceAgentVolumeResourceMonitor) Debounce( + by time.Duration, + now time.Time, + oldState, newState WorkspaceAgentMonitorState, +) (debouncedUntil time.Time, shouldNotify bool) { + if now.After(m.DebouncedUntil) && + oldState == WorkspaceAgentMonitorStateOK && + newState == WorkspaceAgentMonitorStateNOK { + return now.Add(by), true + } + + return m.DebouncedUntil, false +} diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 78f6285e3c11a..4c323fd91c1de 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -467,6 +467,7 @@ func (q *sqlQuerier) GetAuthorizedAuditLogsOffset(ctx context.Context, arg GetAu arg.DateFrom, arg.DateTo, arg.BuildReason, + arg.RequestID, arg.OffsetOpt, arg.LimitOpt, ) diff --git a/coderd/database/models.go b/coderd/database/models.go index b0a487c192793..4e3353f844a02 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -147,6 +147,10 @@ const ( AuditActionLogout AuditAction = "logout" AuditActionRegister AuditAction = "register" AuditActionRequestPasswordReset AuditAction = "request_password_reset" + AuditActionConnect AuditAction = "connect" + AuditActionDisconnect AuditAction = "disconnect" + AuditActionOpen AuditAction = "open" + AuditActionClose AuditAction = "close" ) func (e *AuditAction) Scan(src interface{}) error { @@ -194,7 +198,11 @@ func (e AuditAction) Valid() bool { AuditActionLogin, AuditActionLogout, AuditActionRegister, - AuditActionRequestPasswordReset: + AuditActionRequestPasswordReset, + AuditActionConnect, + AuditActionDisconnect, + AuditActionOpen, + AuditActionClose: return true } return false @@ -211,6 +219,10 @@ func AllAuditActionValues() []AuditAction { AuditActionLogout, AuditActionRegister, AuditActionRequestPasswordReset, + AuditActionConnect, + AuditActionDisconnect, + AuditActionOpen, + AuditActionClose, } } @@ -1608,6 +1620,8 @@ const ( ResourceTypeIdpSyncSettingsOrganization ResourceType = "idp_sync_settings_organization" ResourceTypeIdpSyncSettingsGroup ResourceType = "idp_sync_settings_group" ResourceTypeIdpSyncSettingsRole ResourceType = "idp_sync_settings_role" + ResourceTypeWorkspaceAgent ResourceType = "workspace_agent" + ResourceTypeWorkspaceApp ResourceType = "workspace_app" ) func (e *ResourceType) Scan(src interface{}) error { @@ -1668,7 +1682,9 @@ func (e ResourceType) Valid() bool { ResourceTypeNotificationTemplate, ResourceTypeIdpSyncSettingsOrganization, ResourceTypeIdpSyncSettingsGroup, - ResourceTypeIdpSyncSettingsRole: + ResourceTypeIdpSyncSettingsRole, + ResourceTypeWorkspaceAgent, + ResourceTypeWorkspaceApp: return true } return false @@ -1698,6 +1714,8 @@ func AllResourceTypeValues() []ResourceType { ResourceTypeIdpSyncSettingsOrganization, ResourceTypeIdpSyncSettingsGroup, ResourceTypeIdpSyncSettingsRole, + ResourceTypeWorkspaceAgent, + ResourceTypeWorkspaceApp, } } @@ -1958,6 +1976,64 @@ func AllWorkspaceAgentLifecycleStateValues() []WorkspaceAgentLifecycleState { } } +type WorkspaceAgentMonitorState string + +const ( + WorkspaceAgentMonitorStateOK WorkspaceAgentMonitorState = "OK" + WorkspaceAgentMonitorStateNOK WorkspaceAgentMonitorState = "NOK" +) + +func (e *WorkspaceAgentMonitorState) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = WorkspaceAgentMonitorState(s) + case string: + *e = WorkspaceAgentMonitorState(s) + default: + return fmt.Errorf("unsupported scan type for WorkspaceAgentMonitorState: %T", src) + } + return nil +} + +type NullWorkspaceAgentMonitorState struct { + WorkspaceAgentMonitorState WorkspaceAgentMonitorState `json:"workspace_agent_monitor_state"` + Valid bool `json:"valid"` // Valid is true if WorkspaceAgentMonitorState is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullWorkspaceAgentMonitorState) Scan(value interface{}) error { + if value == nil { + ns.WorkspaceAgentMonitorState, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.WorkspaceAgentMonitorState.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullWorkspaceAgentMonitorState) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.WorkspaceAgentMonitorState), nil +} + +func (e WorkspaceAgentMonitorState) Valid() bool { + switch e { + case WorkspaceAgentMonitorStateOK, + WorkspaceAgentMonitorStateNOK: + return true + } + return false +} + +func AllWorkspaceAgentMonitorStateValues() []WorkspaceAgentMonitorState { + return []WorkspaceAgentMonitorState{ + WorkspaceAgentMonitorStateOK, + WorkspaceAgentMonitorStateNOK, + } +} + // What stage the script was ran in. type WorkspaceAgentScriptTimingStage string @@ -2599,6 +2675,7 @@ type Organization struct { IsDefault bool `db:"is_default" json:"is_default"` DisplayName string `db:"display_name" json:"display_name"` Icon string `db:"icon" json:"icon"` + Deleted bool `db:"deleted" json:"deleted"` } type OrganizationMember struct { @@ -2787,6 +2864,13 @@ type TailnetTunnel struct { UpdatedAt time.Time `db:"updated_at" json:"updated_at"` } +type TelemetryItem struct { + Key string `db:"key" json:"key"` + Value string `db:"value" json:"value"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` +} + // Joins in the display name information such as username, avatar, and organization name. type Template struct { ID uuid.UUID `db:"id" json:"id"` @@ -2947,6 +3031,20 @@ type TemplateVersionParameter struct { Ephemeral bool `db:"ephemeral" json:"ephemeral"` } +type TemplateVersionPreset struct { + ID uuid.UUID `db:"id" json:"id"` + TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` + Name string `db:"name" json:"name"` + CreatedAt time.Time `db:"created_at" json:"created_at"` +} + +type TemplateVersionPresetParameter struct { + ID uuid.UUID `db:"id" json:"id"` + TemplateVersionPresetID uuid.UUID `db:"template_version_preset_id" json:"template_version_preset_id"` + Name string `db:"name" json:"name"` + Value string `db:"value" json:"value"` +} + type TemplateVersionTable struct { ID uuid.UUID `db:"id" json:"id"` TemplateID uuid.NullUUID `db:"template_id" json:"template_id"` @@ -3145,6 +3243,16 @@ type WorkspaceAgentLogSource struct { Icon string `db:"icon" json:"icon"` } +type WorkspaceAgentMemoryResourceMonitor struct { + AgentID uuid.UUID `db:"agent_id" json:"agent_id"` + Enabled bool `db:"enabled" json:"enabled"` + Threshold int32 `db:"threshold" json:"threshold"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + State WorkspaceAgentMonitorState `db:"state" json:"state"` + DebouncedUntil time.Time `db:"debounced_until" json:"debounced_until"` +} + type WorkspaceAgentMetadatum struct { WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"` DisplayName string `db:"display_name" json:"display_name"` @@ -3212,6 +3320,17 @@ type WorkspaceAgentStat struct { Usage bool `db:"usage" json:"usage"` } +type WorkspaceAgentVolumeResourceMonitor struct { + AgentID uuid.UUID `db:"agent_id" json:"agent_id"` + Enabled bool `db:"enabled" json:"enabled"` + Threshold int32 `db:"threshold" json:"threshold"` + Path string `db:"path" json:"path"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + State WorkspaceAgentMonitorState `db:"state" json:"state"` + DebouncedUntil time.Time `db:"debounced_until" json:"debounced_until"` +} + type WorkspaceApp struct { ID uuid.UUID `db:"id" json:"id"` CreatedAt time.Time `db:"created_at" json:"created_at"` @@ -3261,22 +3380,23 @@ type WorkspaceAppStat struct { // Joins in the username + avatar url of the initiated by user. type WorkspaceBuild struct { - ID uuid.UUID `db:"id" json:"id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` - WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` - TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` - BuildNumber int32 `db:"build_number" json:"build_number"` - Transition WorkspaceTransition `db:"transition" json:"transition"` - InitiatorID uuid.UUID `db:"initiator_id" json:"initiator_id"` - ProvisionerState []byte `db:"provisioner_state" json:"provisioner_state"` - JobID uuid.UUID `db:"job_id" json:"job_id"` - Deadline time.Time `db:"deadline" json:"deadline"` - Reason BuildReason `db:"reason" json:"reason"` - DailyCost int32 `db:"daily_cost" json:"daily_cost"` - MaxDeadline time.Time `db:"max_deadline" json:"max_deadline"` - InitiatorByAvatarUrl string `db:"initiator_by_avatar_url" json:"initiator_by_avatar_url"` - InitiatorByUsername string `db:"initiator_by_username" json:"initiator_by_username"` + ID uuid.UUID `db:"id" json:"id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` + TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` + BuildNumber int32 `db:"build_number" json:"build_number"` + Transition WorkspaceTransition `db:"transition" json:"transition"` + InitiatorID uuid.UUID `db:"initiator_id" json:"initiator_id"` + ProvisionerState []byte `db:"provisioner_state" json:"provisioner_state"` + JobID uuid.UUID `db:"job_id" json:"job_id"` + Deadline time.Time `db:"deadline" json:"deadline"` + Reason BuildReason `db:"reason" json:"reason"` + DailyCost int32 `db:"daily_cost" json:"daily_cost"` + MaxDeadline time.Time `db:"max_deadline" json:"max_deadline"` + TemplateVersionPresetID uuid.NullUUID `db:"template_version_preset_id" json:"template_version_preset_id"` + InitiatorByAvatarUrl string `db:"initiator_by_avatar_url" json:"initiator_by_avatar_url"` + InitiatorByUsername string `db:"initiator_by_username" json:"initiator_by_username"` } type WorkspaceBuildParameter struct { @@ -3288,20 +3408,21 @@ type WorkspaceBuildParameter struct { } type WorkspaceBuildTable struct { - ID uuid.UUID `db:"id" json:"id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` - WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` - TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` - BuildNumber int32 `db:"build_number" json:"build_number"` - Transition WorkspaceTransition `db:"transition" json:"transition"` - InitiatorID uuid.UUID `db:"initiator_id" json:"initiator_id"` - ProvisionerState []byte `db:"provisioner_state" json:"provisioner_state"` - JobID uuid.UUID `db:"job_id" json:"job_id"` - Deadline time.Time `db:"deadline" json:"deadline"` - Reason BuildReason `db:"reason" json:"reason"` - DailyCost int32 `db:"daily_cost" json:"daily_cost"` - MaxDeadline time.Time `db:"max_deadline" json:"max_deadline"` + ID uuid.UUID `db:"id" json:"id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` + TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` + BuildNumber int32 `db:"build_number" json:"build_number"` + Transition WorkspaceTransition `db:"transition" json:"transition"` + InitiatorID uuid.UUID `db:"initiator_id" json:"initiator_id"` + ProvisionerState []byte `db:"provisioner_state" json:"provisioner_state"` + JobID uuid.UUID `db:"job_id" json:"job_id"` + Deadline time.Time `db:"deadline" json:"deadline"` + Reason BuildReason `db:"reason" json:"reason"` + DailyCost int32 `db:"daily_cost" json:"daily_cost"` + MaxDeadline time.Time `db:"max_deadline" json:"max_deadline"` + TemplateVersionPresetID uuid.NullUUID `db:"template_version_preset_id" json:"template_version_preset_id"` } type WorkspaceModule struct { diff --git a/coderd/database/pubsub/pubsub_linux_test.go b/coderd/database/pubsub/pubsub_linux_test.go index 016a6c9334c33..05bd76232e162 100644 --- a/coderd/database/pubsub/pubsub_linux_test.go +++ b/coderd/database/pubsub/pubsub_linux_test.go @@ -1,5 +1,3 @@ -//go:build linux - package pubsub_test import ( @@ -305,6 +303,9 @@ func TestMeasureLatency(t *testing.T) { require.NoError(t, err) db, err := sql.Open("postgres", connectionURL) require.NoError(t, err) + t.Cleanup(func() { + _ = db.Close() + }) ps, err := pubsub.New(ctx, logger, db, connectionURL) require.NoError(t, err) diff --git a/coderd/database/pubsub/pubsub_test.go b/coderd/database/pubsub/pubsub_test.go index 7dec4bc500dff..16227089682bb 100644 --- a/coderd/database/pubsub/pubsub_test.go +++ b/coderd/database/pubsub/pubsub_test.go @@ -137,6 +137,7 @@ func TestPGPubsubDriver(t *testing.T) { // use a separate subber and pubber so we can keep track of listener connections db, err := sql.Open("postgres", connectionURL) require.NoError(t, err) + defer db.Close() pubber, err := pubsub.New(ctx, logger, db, connectionURL) require.NoError(t, err) defer pubber.Close() @@ -147,6 +148,7 @@ func TestPGPubsubDriver(t *testing.T) { tconn, err := subDriver.Connector(connectionURL) require.NoError(t, err) tcdb := sql.OpenDB(tconn) + defer tcdb.Close() subber, err := pubsub.New(ctx, logger, tcdb, connectionURL) require.NoError(t, err) defer subber.Close() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 132a7aea75bdd..573cf0c24ad8f 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -94,7 +94,6 @@ type sqlcQuerier interface { // Logs can take up a lot of space, so it's important we clean up frequently. DeleteOldWorkspaceAgentLogs(ctx context.Context, threshold time.Time) error DeleteOldWorkspaceAgentStats(ctx context.Context) error - DeleteOrganization(ctx context.Context, id uuid.UUID) error DeleteOrganizationMember(ctx context.Context, arg DeleteOrganizationMemberParams) error DeleteProvisionerKey(ctx context.Context, id uuid.UUID) error DeleteReplicasUpdatedBefore(ctx context.Context, updatedAt time.Time) error @@ -112,8 +111,12 @@ type sqlcQuerier interface { DisableForeignKeysAndTriggers(ctx context.Context) error EnqueueNotificationMessage(ctx context.Context, arg EnqueueNotificationMessageParams) error FavoriteWorkspace(ctx context.Context, id uuid.UUID) error + FetchMemoryResourceMonitorsByAgentID(ctx context.Context, agentID uuid.UUID) (WorkspaceAgentMemoryResourceMonitor, error) + FetchMemoryResourceMonitorsUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]WorkspaceAgentMemoryResourceMonitor, error) // This is used to build up the notification_message's JSON payload. FetchNewMessageMetadata(ctx context.Context, arg FetchNewMessageMetadataParams) (FetchNewMessageMetadataRow, error) + FetchVolumesResourceMonitorsByAgentID(ctx context.Context, agentID uuid.UUID) ([]WorkspaceAgentVolumeResourceMonitor, error) + FetchVolumesResourceMonitorsUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]WorkspaceAgentVolumeResourceMonitor, error) GetAPIKeyByID(ctx context.Context, id string) (APIKey, error) // there is no unique constraint on empty token names GetAPIKeyByName(ctx context.Context, arg GetAPIKeyByNameParams) (APIKey, error) @@ -184,6 +187,7 @@ type sqlcQuerier interface { GetNotificationTemplateByID(ctx context.Context, id uuid.UUID) (NotificationTemplate, error) GetNotificationTemplatesByKind(ctx context.Context, kind NotificationTemplateKind) ([]NotificationTemplate, error) GetNotificationsSettings(ctx context.Context) (string, error) + GetOAuth2GithubDefaultEligible(ctx context.Context) (bool, error) GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderApp, error) GetOAuth2ProviderAppCodeByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderAppCode, error) GetOAuth2ProviderAppCodeByPrefix(ctx context.Context, secretPrefix []byte) (OAuth2ProviderAppCode, error) @@ -195,14 +199,19 @@ type sqlcQuerier interface { GetOAuth2ProviderAppsByUserID(ctx context.Context, userID uuid.UUID) ([]GetOAuth2ProviderAppsByUserIDRow, error) GetOAuthSigningKey(ctx context.Context) (string, error) GetOrganizationByID(ctx context.Context, id uuid.UUID) (Organization, error) - GetOrganizationByName(ctx context.Context, name string) (Organization, error) + GetOrganizationByName(ctx context.Context, arg GetOrganizationByNameParams) (Organization, error) GetOrganizationIDsByMemberIDs(ctx context.Context, ids []uuid.UUID) ([]GetOrganizationIDsByMemberIDsRow, error) GetOrganizations(ctx context.Context, arg GetOrganizationsParams) ([]Organization, error) - GetOrganizationsByUserID(ctx context.Context, userID uuid.UUID) ([]Organization, error) + GetOrganizationsByUserID(ctx context.Context, arg GetOrganizationsByUserIDParams) ([]Organization, error) GetParameterSchemasByJobID(ctx context.Context, jobID uuid.UUID) ([]ParameterSchema, error) + GetPresetByWorkspaceBuildID(ctx context.Context, workspaceBuildID uuid.UUID) (TemplateVersionPreset, error) + GetPresetParametersByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionPresetParameter, error) + GetPresetsByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionPreset, error) GetPreviousTemplateVersion(ctx context.Context, arg GetPreviousTemplateVersionParams) (TemplateVersion, error) GetProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error) GetProvisionerDaemonsByOrganization(ctx context.Context, arg GetProvisionerDaemonsByOrganizationParams) ([]ProvisionerDaemon, error) + // Current job information. + // Previous job information. GetProvisionerDaemonsWithStatusByOrganization(ctx context.Context, arg GetProvisionerDaemonsWithStatusByOrganizationParams) ([]GetProvisionerDaemonsWithStatusByOrganizationRow, error) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (ProvisionerJob, error) GetProvisionerJobTimingsByJobID(ctx context.Context, jobID uuid.UUID) ([]ProvisionerJobTiming, error) @@ -224,6 +233,8 @@ type sqlcQuerier interface { GetTailnetPeers(ctx context.Context, id uuid.UUID) ([]TailnetPeer, error) GetTailnetTunnelPeerBindings(ctx context.Context, srcID uuid.UUID) ([]GetTailnetTunnelPeerBindingsRow, error) GetTailnetTunnelPeerIDs(ctx context.Context, srcID uuid.UUID) ([]GetTailnetTunnelPeerIDsRow, error) + GetTelemetryItem(ctx context.Context, key string) (TelemetryItem, error) + GetTelemetryItems(ctx context.Context) ([]TelemetryItem, error) // GetTemplateAppInsights returns the aggregate usage of each app in a given // timeframe. The result can be filtered on template_ids, meaning only user data // from workspaces based on those templates will be included. @@ -388,6 +399,7 @@ type sqlcQuerier interface { InsertGroup(ctx context.Context, arg InsertGroupParams) (Group, error) InsertGroupMember(ctx context.Context, arg InsertGroupMemberParams) error InsertLicense(ctx context.Context, arg InsertLicenseParams) (License, error) + InsertMemoryResourceMonitor(ctx context.Context, arg InsertMemoryResourceMonitorParams) (WorkspaceAgentMemoryResourceMonitor, error) // Inserts any group by name that does not exist. All new groups are given // a random uuid, are inserted into the same organization. They have the default // values for avatar, display name, and quota allowance (all zero values). @@ -399,11 +411,14 @@ type sqlcQuerier interface { InsertOAuth2ProviderAppToken(ctx context.Context, arg InsertOAuth2ProviderAppTokenParams) (OAuth2ProviderAppToken, error) InsertOrganization(ctx context.Context, arg InsertOrganizationParams) (Organization, error) InsertOrganizationMember(ctx context.Context, arg InsertOrganizationMemberParams) (OrganizationMember, error) + InsertPreset(ctx context.Context, arg InsertPresetParams) (TemplateVersionPreset, error) + InsertPresetParameters(ctx context.Context, arg InsertPresetParametersParams) ([]TemplateVersionPresetParameter, error) InsertProvisionerJob(ctx context.Context, arg InsertProvisionerJobParams) (ProvisionerJob, error) InsertProvisionerJobLogs(ctx context.Context, arg InsertProvisionerJobLogsParams) ([]ProvisionerJobLog, error) InsertProvisionerJobTimings(ctx context.Context, arg InsertProvisionerJobTimingsParams) ([]ProvisionerJobTiming, error) InsertProvisionerKey(ctx context.Context, arg InsertProvisionerKeyParams) (ProvisionerKey, error) InsertReplica(ctx context.Context, arg InsertReplicaParams) (Replica, error) + InsertTelemetryItemIfNotExists(ctx context.Context, arg InsertTelemetryItemIfNotExistsParams) error InsertTemplate(ctx context.Context, arg InsertTemplateParams) error InsertTemplateVersion(ctx context.Context, arg InsertTemplateVersionParams) error InsertTemplateVersionParameter(ctx context.Context, arg InsertTemplateVersionParameterParams) (TemplateVersionParameter, error) @@ -416,6 +431,7 @@ type sqlcQuerier interface { // InsertUserGroupsByName adds a user to all provided groups, if they exist. InsertUserGroupsByName(ctx context.Context, arg InsertUserGroupsByNameParams) error InsertUserLink(ctx context.Context, arg InsertUserLinkParams) (UserLink, error) + InsertVolumeResourceMonitor(ctx context.Context, arg InsertVolumeResourceMonitorParams) (WorkspaceAgentVolumeResourceMonitor, error) InsertWorkspace(ctx context.Context, arg InsertWorkspaceParams) (WorkspaceTable, error) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspaceAgentParams) (WorkspaceAgent, error) InsertWorkspaceAgentLogSources(ctx context.Context, arg InsertWorkspaceAgentLogSourcesParams) ([]WorkspaceAgentLogSource, error) @@ -466,10 +482,12 @@ type sqlcQuerier interface { UpdateGroupByID(ctx context.Context, arg UpdateGroupByIDParams) (Group, error) UpdateInactiveUsersToDormant(ctx context.Context, arg UpdateInactiveUsersToDormantParams) ([]UpdateInactiveUsersToDormantRow, error) UpdateMemberRoles(ctx context.Context, arg UpdateMemberRolesParams) (OrganizationMember, error) + UpdateMemoryResourceMonitor(ctx context.Context, arg UpdateMemoryResourceMonitorParams) error UpdateNotificationTemplateMethodByID(ctx context.Context, arg UpdateNotificationTemplateMethodByIDParams) (NotificationTemplate, error) UpdateOAuth2ProviderAppByID(ctx context.Context, arg UpdateOAuth2ProviderAppByIDParams) (OAuth2ProviderApp, error) UpdateOAuth2ProviderAppSecretByID(ctx context.Context, arg UpdateOAuth2ProviderAppSecretByIDParams) (OAuth2ProviderAppSecret, error) UpdateOrganization(ctx context.Context, arg UpdateOrganizationParams) (Organization, error) + UpdateOrganizationDeletedByID(ctx context.Context, arg UpdateOrganizationDeletedByIDParams) error UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg UpdateProvisionerDaemonLastSeenAtParams) error UpdateProvisionerJobByID(ctx context.Context, arg UpdateProvisionerJobByIDParams) error UpdateProvisionerJobWithCancelByID(ctx context.Context, arg UpdateProvisionerJobWithCancelByIDParams) error @@ -500,6 +518,7 @@ type sqlcQuerier interface { UpdateUserQuietHoursSchedule(ctx context.Context, arg UpdateUserQuietHoursScheduleParams) (User, error) UpdateUserRoles(ctx context.Context, arg UpdateUserRolesParams) (User, error) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusParams) (User, error) + UpdateVolumeResourceMonitor(ctx context.Context, arg UpdateVolumeResourceMonitorParams) error UpdateWorkspace(ctx context.Context, arg UpdateWorkspaceParams) (WorkspaceTable, error) UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg UpdateWorkspaceAgentConnectionByIDParams) error UpdateWorkspaceAgentLifecycleStateByID(ctx context.Context, arg UpdateWorkspaceAgentLifecycleStateByIDParams) error @@ -537,6 +556,7 @@ type sqlcQuerier interface { // Insert or update notification report generator logs with recent activity. UpsertNotificationReportGeneratorLog(ctx context.Context, arg UpsertNotificationReportGeneratorLogParams) error UpsertNotificationsSettings(ctx context.Context, value string) error + UpsertOAuth2GithubDefaultEligible(ctx context.Context, eligible bool) error UpsertOAuthSigningKey(ctx context.Context, value string) error UpsertProvisionerDaemon(ctx context.Context, arg UpsertProvisionerDaemonParams) (ProvisionerDaemon, error) UpsertRuntimeConfig(ctx context.Context, arg UpsertRuntimeConfigParams) error @@ -546,6 +566,7 @@ type sqlcQuerier interface { UpsertTailnetCoordinator(ctx context.Context, id uuid.UUID) (TailnetCoordinator, error) UpsertTailnetPeer(ctx context.Context, arg UpsertTailnetPeerParams) (TailnetPeer, error) UpsertTailnetTunnel(ctx context.Context, arg UpsertTailnetTunnelParams) (TailnetTunnel, error) + UpsertTelemetryItem(ctx context.Context, arg UpsertTelemetryItemParams) error // This query aggregates the workspace_agent_stats and workspace_app_stats data // into a single table for efficient storage and querying. Half-hour buckets are // used to store the data, and the minutes are summed for each user and template diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index 00b189967f5a6..5d3e65bb518df 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -1,5 +1,3 @@ -//go:build linux - package database_test import ( @@ -21,6 +19,7 @@ import ( "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/db2sdk" "github.com/coder/coder/v2/coderd/database/dbauthz" + "github.com/coder/coder/v2/coderd/database/dbfake" "github.com/coder/coder/v2/coderd/database/dbgen" "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/coderd/database/dbtime" @@ -2916,6 +2915,136 @@ func TestGetUserStatusCounts(t *testing.T) { } } +func TestOrganizationDeleteTrigger(t *testing.T) { + t.Parallel() + + if !dbtestutil.WillUsePostgres() { + t.SkipNow() + } + + t.Run("WorkspaceExists", func(t *testing.T) { + t.Parallel() + db, _ := dbtestutil.NewDB(t) + + orgA := dbfake.Organization(t, db).Do() + + user := dbgen.User(t, db, database.User{}) + + dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OrganizationID: orgA.Org.ID, + OwnerID: user.ID, + }).Do() + + ctx := testutil.Context(t, testutil.WaitShort) + err := db.UpdateOrganizationDeletedByID(ctx, database.UpdateOrganizationDeletedByIDParams{ + UpdatedAt: dbtime.Now(), + ID: orgA.Org.ID, + }) + require.Error(t, err) + // cannot delete organization: organization has 1 workspaces and 1 templates that must be deleted first + require.ErrorContains(t, err, "cannot delete organization") + require.ErrorContains(t, err, "has 1 workspaces") + require.ErrorContains(t, err, "1 templates") + }) + + t.Run("TemplateExists", func(t *testing.T) { + t.Parallel() + db, _ := dbtestutil.NewDB(t) + + orgA := dbfake.Organization(t, db).Do() + + user := dbgen.User(t, db, database.User{}) + + dbgen.Template(t, db, database.Template{ + OrganizationID: orgA.Org.ID, + CreatedBy: user.ID, + }) + + ctx := testutil.Context(t, testutil.WaitShort) + err := db.UpdateOrganizationDeletedByID(ctx, database.UpdateOrganizationDeletedByIDParams{ + UpdatedAt: dbtime.Now(), + ID: orgA.Org.ID, + }) + require.Error(t, err) + // cannot delete organization: organization has 0 workspaces and 1 templates that must be deleted first + require.ErrorContains(t, err, "cannot delete organization") + require.ErrorContains(t, err, "has 0 workspaces") + require.ErrorContains(t, err, "1 templates") + }) + + t.Run("ProvisionerKeyExists", func(t *testing.T) { + t.Parallel() + db, _ := dbtestutil.NewDB(t) + + orgA := dbfake.Organization(t, db).Do() + + dbgen.ProvisionerKey(t, db, database.ProvisionerKey{ + OrganizationID: orgA.Org.ID, + }) + + ctx := testutil.Context(t, testutil.WaitShort) + err := db.UpdateOrganizationDeletedByID(ctx, database.UpdateOrganizationDeletedByIDParams{ + UpdatedAt: dbtime.Now(), + ID: orgA.Org.ID, + }) + require.Error(t, err) + // cannot delete organization: organization has 1 provisioner keys that must be deleted first + require.ErrorContains(t, err, "cannot delete organization") + require.ErrorContains(t, err, "1 provisioner keys") + }) + + t.Run("GroupExists", func(t *testing.T) { + t.Parallel() + db, _ := dbtestutil.NewDB(t) + + orgA := dbfake.Organization(t, db).Do() + + dbgen.Group(t, db, database.Group{ + OrganizationID: orgA.Org.ID, + }) + + ctx := testutil.Context(t, testutil.WaitShort) + err := db.UpdateOrganizationDeletedByID(ctx, database.UpdateOrganizationDeletedByIDParams{ + UpdatedAt: dbtime.Now(), + ID: orgA.Org.ID, + }) + require.Error(t, err) + // cannot delete organization: organization has 1 groups that must be deleted first + require.ErrorContains(t, err, "cannot delete organization") + require.ErrorContains(t, err, "has 1 groups") + }) + + t.Run("MemberExists", func(t *testing.T) { + t.Parallel() + db, _ := dbtestutil.NewDB(t) + + orgA := dbfake.Organization(t, db).Do() + + userA := dbgen.User(t, db, database.User{}) + userB := dbgen.User(t, db, database.User{}) + + dbgen.OrganizationMember(t, db, database.OrganizationMember{ + OrganizationID: orgA.Org.ID, + UserID: userA.ID, + }) + + dbgen.OrganizationMember(t, db, database.OrganizationMember{ + OrganizationID: orgA.Org.ID, + UserID: userB.ID, + }) + + ctx := testutil.Context(t, testutil.WaitShort) + err := db.UpdateOrganizationDeletedByID(ctx, database.UpdateOrganizationDeletedByIDParams{ + UpdatedAt: dbtime.Now(), + ID: orgA.Org.ID, + }) + require.Error(t, err) + // cannot delete organization: organization has 1 members that must be deleted first + require.ErrorContains(t, err, "cannot delete organization") + require.ErrorContains(t, err, "has 1 members") + }) +} + func requireUsersMatch(t testing.TB, expected []database.User, found []database.GetUsersRow, msg string) { t.Helper() require.ElementsMatch(t, expected, database.ConvertUserRows(found), msg) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 38dbf1fbfd0bb..2f28b563e98b8 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -558,6 +558,12 @@ WHERE workspace_builds.reason::text = $11 ELSE true END + -- Filter request_id + AND CASE + WHEN $12 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN + audit_logs.request_id = $12 + ELSE true + END -- Authorize Filter clause will be injected below in GetAuthorizedAuditLogsOffset -- @authorize_filter @@ -567,9 +573,9 @@ LIMIT -- a limit of 0 means "no limit". The audit log table is unbounded -- in size, and is expected to be quite large. Implement a default -- limit of 100 to prevent accidental excessively large queries. - COALESCE(NULLIF($13 :: int, 0), 100) + COALESCE(NULLIF($14 :: int, 0), 100) OFFSET - $12 + $13 ` type GetAuditLogsOffsetParams struct { @@ -584,6 +590,7 @@ type GetAuditLogsOffsetParams struct { DateFrom time.Time `db:"date_from" json:"date_from"` DateTo time.Time `db:"date_to" json:"date_to"` BuildReason string `db:"build_reason" json:"build_reason"` + RequestID uuid.UUID `db:"request_id" json:"request_id"` OffsetOpt int32 `db:"offset_opt" json:"offset_opt"` LimitOpt int32 `db:"limit_opt" json:"limit_opt"` } @@ -624,6 +631,7 @@ func (q *sqlQuerier) GetAuditLogsOffset(ctx context.Context, arg GetAuditLogsOff arg.DateFrom, arg.DateTo, arg.BuildReason, + arg.RequestID, arg.OffsetOpt, arg.LimitOpt, ) @@ -5058,28 +5066,15 @@ func (q *sqlQuerier) UpdateMemberRoles(ctx context.Context, arg UpdateMemberRole return i, err } -const deleteOrganization = `-- name: DeleteOrganization :exec -DELETE FROM - organizations -WHERE - id = $1 AND - is_default = false -` - -func (q *sqlQuerier) DeleteOrganization(ctx context.Context, id uuid.UUID) error { - _, err := q.db.ExecContext(ctx, deleteOrganization, id) - return err -} - const getDefaultOrganization = `-- name: GetDefaultOrganization :one SELECT - id, name, description, created_at, updated_at, is_default, display_name, icon + id, name, description, created_at, updated_at, is_default, display_name, icon, deleted FROM - organizations + organizations WHERE - is_default = true + is_default = true LIMIT - 1 + 1 ` func (q *sqlQuerier) GetDefaultOrganization(ctx context.Context) (Organization, error) { @@ -5094,17 +5089,18 @@ func (q *sqlQuerier) GetDefaultOrganization(ctx context.Context) (Organization, &i.IsDefault, &i.DisplayName, &i.Icon, + &i.Deleted, ) return i, err } const getOrganizationByID = `-- name: GetOrganizationByID :one SELECT - id, name, description, created_at, updated_at, is_default, display_name, icon + id, name, description, created_at, updated_at, is_default, display_name, icon, deleted FROM - organizations + organizations WHERE - id = $1 + id = $1 ` func (q *sqlQuerier) GetOrganizationByID(ctx context.Context, id uuid.UUID) (Organization, error) { @@ -5119,23 +5115,31 @@ func (q *sqlQuerier) GetOrganizationByID(ctx context.Context, id uuid.UUID) (Org &i.IsDefault, &i.DisplayName, &i.Icon, + &i.Deleted, ) return i, err } const getOrganizationByName = `-- name: GetOrganizationByName :one SELECT - id, name, description, created_at, updated_at, is_default, display_name, icon + id, name, description, created_at, updated_at, is_default, display_name, icon, deleted FROM - organizations + organizations WHERE - LOWER("name") = LOWER($1) + -- Optionally include deleted organizations + deleted = $1 AND + LOWER("name") = LOWER($2) LIMIT - 1 + 1 ` -func (q *sqlQuerier) GetOrganizationByName(ctx context.Context, name string) (Organization, error) { - row := q.db.QueryRowContext(ctx, getOrganizationByName, name) +type GetOrganizationByNameParams struct { + Deleted bool `db:"deleted" json:"deleted"` + Name string `db:"name" json:"name"` +} + +func (q *sqlQuerier) GetOrganizationByName(ctx context.Context, arg GetOrganizationByNameParams) (Organization, error) { + row := q.db.QueryRowContext(ctx, getOrganizationByName, arg.Deleted, arg.Name) var i Organization err := row.Scan( &i.ID, @@ -5146,37 +5150,40 @@ func (q *sqlQuerier) GetOrganizationByName(ctx context.Context, name string) (Or &i.IsDefault, &i.DisplayName, &i.Icon, + &i.Deleted, ) return i, err } const getOrganizations = `-- name: GetOrganizations :many SELECT - id, name, description, created_at, updated_at, is_default, display_name, icon + id, name, description, created_at, updated_at, is_default, display_name, icon, deleted FROM - organizations + organizations WHERE - true - -- Filter by ids - AND CASE - WHEN array_length($1 :: uuid[], 1) > 0 THEN - id = ANY($1) - ELSE true - END - AND CASE - WHEN $2::text != '' THEN - LOWER("name") = LOWER($2) - ELSE true - END + -- Optionally include deleted organizations + deleted = $1 + -- Filter by ids + AND CASE + WHEN array_length($2 :: uuid[], 1) > 0 THEN + id = ANY($2) + ELSE true + END + AND CASE + WHEN $3::text != '' THEN + LOWER("name") = LOWER($3) + ELSE true + END ` type GetOrganizationsParams struct { - IDs []uuid.UUID `db:"ids" json:"ids"` - Name string `db:"name" json:"name"` + Deleted bool `db:"deleted" json:"deleted"` + IDs []uuid.UUID `db:"ids" json:"ids"` + Name string `db:"name" json:"name"` } func (q *sqlQuerier) GetOrganizations(ctx context.Context, arg GetOrganizationsParams) ([]Organization, error) { - rows, err := q.db.QueryContext(ctx, getOrganizations, pq.Array(arg.IDs), arg.Name) + rows, err := q.db.QueryContext(ctx, getOrganizations, arg.Deleted, pq.Array(arg.IDs), arg.Name) if err != nil { return nil, err } @@ -5193,6 +5200,7 @@ func (q *sqlQuerier) GetOrganizations(ctx context.Context, arg GetOrganizationsP &i.IsDefault, &i.DisplayName, &i.Icon, + &i.Deleted, ); err != nil { return nil, err } @@ -5209,22 +5217,29 @@ func (q *sqlQuerier) GetOrganizations(ctx context.Context, arg GetOrganizationsP const getOrganizationsByUserID = `-- name: GetOrganizationsByUserID :many SELECT - id, name, description, created_at, updated_at, is_default, display_name, icon + id, name, description, created_at, updated_at, is_default, display_name, icon, deleted FROM - organizations + organizations WHERE - id = ANY( - SELECT - organization_id - FROM - organization_members - WHERE - user_id = $1 - ) + -- Optionally include deleted organizations + deleted = $2 AND + id = ANY( + SELECT + organization_id + FROM + organization_members + WHERE + user_id = $1 + ) ` -func (q *sqlQuerier) GetOrganizationsByUserID(ctx context.Context, userID uuid.UUID) ([]Organization, error) { - rows, err := q.db.QueryContext(ctx, getOrganizationsByUserID, userID) +type GetOrganizationsByUserIDParams struct { + UserID uuid.UUID `db:"user_id" json:"user_id"` + Deleted bool `db:"deleted" json:"deleted"` +} + +func (q *sqlQuerier) GetOrganizationsByUserID(ctx context.Context, arg GetOrganizationsByUserIDParams) ([]Organization, error) { + rows, err := q.db.QueryContext(ctx, getOrganizationsByUserID, arg.UserID, arg.Deleted) if err != nil { return nil, err } @@ -5241,6 +5256,7 @@ func (q *sqlQuerier) GetOrganizationsByUserID(ctx context.Context, userID uuid.U &i.IsDefault, &i.DisplayName, &i.Icon, + &i.Deleted, ); err != nil { return nil, err } @@ -5257,10 +5273,10 @@ func (q *sqlQuerier) GetOrganizationsByUserID(ctx context.Context, userID uuid.U const insertOrganization = `-- name: InsertOrganization :one INSERT INTO - organizations (id, "name", display_name, description, icon, created_at, updated_at, is_default) + organizations (id, "name", display_name, description, icon, created_at, updated_at, is_default) VALUES - -- If no organizations exist, and this is the first, make it the default. - ($1, $2, $3, $4, $5, $6, $7, (SELECT TRUE FROM organizations LIMIT 1) IS NULL) RETURNING id, name, description, created_at, updated_at, is_default, display_name, icon + -- If no organizations exist, and this is the first, make it the default. + ($1, $2, $3, $4, $5, $6, $7, (SELECT TRUE FROM organizations LIMIT 1) IS NULL) RETURNING id, name, description, created_at, updated_at, is_default, display_name, icon, deleted ` type InsertOrganizationParams struct { @@ -5293,22 +5309,23 @@ func (q *sqlQuerier) InsertOrganization(ctx context.Context, arg InsertOrganizat &i.IsDefault, &i.DisplayName, &i.Icon, + &i.Deleted, ) return i, err } const updateOrganization = `-- name: UpdateOrganization :one UPDATE - organizations + organizations SET - updated_at = $1, - name = $2, - display_name = $3, - description = $4, - icon = $5 + updated_at = $1, + name = $2, + display_name = $3, + description = $4, + icon = $5 WHERE - id = $6 -RETURNING id, name, description, created_at, updated_at, is_default, display_name, icon + id = $6 +RETURNING id, name, description, created_at, updated_at, is_default, display_name, icon, deleted ` type UpdateOrganizationParams struct { @@ -5339,10 +5356,31 @@ func (q *sqlQuerier) UpdateOrganization(ctx context.Context, arg UpdateOrganizat &i.IsDefault, &i.DisplayName, &i.Icon, + &i.Deleted, ) return i, err } +const updateOrganizationDeletedByID = `-- name: UpdateOrganizationDeletedByID :exec +UPDATE organizations +SET + deleted = true, + updated_at = $1 +WHERE + id = $2 AND + is_default = false +` + +type UpdateOrganizationDeletedByIDParams struct { + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + ID uuid.UUID `db:"id" json:"id"` +} + +func (q *sqlQuerier) UpdateOrganizationDeletedByID(ctx context.Context, arg UpdateOrganizationDeletedByIDParams) error { + _, err := q.db.ExecContext(ctx, updateOrganizationDeletedByID, arg.UpdatedAt, arg.ID) + return err +} + const getParameterSchemasByJobID = `-- name: GetParameterSchemasByJobID :many SELECT id, created_at, job_id, name, description, default_source_scheme, default_source_value, allow_override_source, default_destination_scheme, allow_override_destination, default_refresh, redisplay_value, validation_error, validation_condition, validation_type_system, validation_value_type, index @@ -5395,6 +5433,172 @@ func (q *sqlQuerier) GetParameterSchemasByJobID(ctx context.Context, jobID uuid. return items, nil } +const getPresetByWorkspaceBuildID = `-- name: GetPresetByWorkspaceBuildID :one +SELECT + template_version_presets.id, template_version_presets.template_version_id, template_version_presets.name, template_version_presets.created_at +FROM + template_version_presets + INNER JOIN workspace_builds ON workspace_builds.template_version_preset_id = template_version_presets.id +WHERE + workspace_builds.id = $1 +` + +func (q *sqlQuerier) GetPresetByWorkspaceBuildID(ctx context.Context, workspaceBuildID uuid.UUID) (TemplateVersionPreset, error) { + row := q.db.QueryRowContext(ctx, getPresetByWorkspaceBuildID, workspaceBuildID) + var i TemplateVersionPreset + err := row.Scan( + &i.ID, + &i.TemplateVersionID, + &i.Name, + &i.CreatedAt, + ) + return i, err +} + +const getPresetParametersByTemplateVersionID = `-- name: GetPresetParametersByTemplateVersionID :many +SELECT + template_version_preset_parameters.id, template_version_preset_parameters.template_version_preset_id, template_version_preset_parameters.name, template_version_preset_parameters.value +FROM + template_version_preset_parameters + INNER JOIN template_version_presets ON template_version_preset_parameters.template_version_preset_id = template_version_presets.id +WHERE + template_version_presets.template_version_id = $1 +` + +func (q *sqlQuerier) GetPresetParametersByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionPresetParameter, error) { + rows, err := q.db.QueryContext(ctx, getPresetParametersByTemplateVersionID, templateVersionID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []TemplateVersionPresetParameter + for rows.Next() { + var i TemplateVersionPresetParameter + if err := rows.Scan( + &i.ID, + &i.TemplateVersionPresetID, + &i.Name, + &i.Value, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getPresetsByTemplateVersionID = `-- name: GetPresetsByTemplateVersionID :many +SELECT + id, template_version_id, name, created_at +FROM + template_version_presets +WHERE + template_version_id = $1 +` + +func (q *sqlQuerier) GetPresetsByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionPreset, error) { + rows, err := q.db.QueryContext(ctx, getPresetsByTemplateVersionID, templateVersionID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []TemplateVersionPreset + for rows.Next() { + var i TemplateVersionPreset + if err := rows.Scan( + &i.ID, + &i.TemplateVersionID, + &i.Name, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const insertPreset = `-- name: InsertPreset :one +INSERT INTO + template_version_presets (template_version_id, name, created_at) +VALUES + ($1, $2, $3) RETURNING id, template_version_id, name, created_at +` + +type InsertPresetParams struct { + TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` + Name string `db:"name" json:"name"` + CreatedAt time.Time `db:"created_at" json:"created_at"` +} + +func (q *sqlQuerier) InsertPreset(ctx context.Context, arg InsertPresetParams) (TemplateVersionPreset, error) { + row := q.db.QueryRowContext(ctx, insertPreset, arg.TemplateVersionID, arg.Name, arg.CreatedAt) + var i TemplateVersionPreset + err := row.Scan( + &i.ID, + &i.TemplateVersionID, + &i.Name, + &i.CreatedAt, + ) + return i, err +} + +const insertPresetParameters = `-- name: InsertPresetParameters :many +INSERT INTO + template_version_preset_parameters (template_version_preset_id, name, value) +SELECT + $1, + unnest($2 :: TEXT[]), + unnest($3 :: TEXT[]) +RETURNING id, template_version_preset_id, name, value +` + +type InsertPresetParametersParams struct { + TemplateVersionPresetID uuid.UUID `db:"template_version_preset_id" json:"template_version_preset_id"` + Names []string `db:"names" json:"names"` + Values []string `db:"values" json:"values"` +} + +func (q *sqlQuerier) InsertPresetParameters(ctx context.Context, arg InsertPresetParametersParams) ([]TemplateVersionPresetParameter, error) { + rows, err := q.db.QueryContext(ctx, insertPresetParameters, arg.TemplateVersionPresetID, pq.Array(arg.Names), pq.Array(arg.Values)) + if err != nil { + return nil, err + } + defer rows.Close() + var items []TemplateVersionPresetParameter + for rows.Next() { + var i TemplateVersionPresetParameter + if err := rows.Scan( + &i.ID, + &i.TemplateVersionPresetID, + &i.Name, + &i.Value, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const deleteOldProvisionerDaemons = `-- name: DeleteOldProvisionerDaemons :exec DELETE FROM provisioner_daemons WHERE ( (created_at < (NOW() - INTERVAL '7 days') AND last_seen_at IS NULL) OR @@ -5576,7 +5780,13 @@ SELECT current_job.id AS current_job_id, current_job.job_status AS current_job_status, previous_job.id AS previous_job_id, - previous_job.job_status AS previous_job_status + previous_job.job_status AS previous_job_status, + COALESCE(current_template.name, ''::text) AS current_job_template_name, + COALESCE(current_template.display_name, ''::text) AS current_job_template_display_name, + COALESCE(current_template.icon, ''::text) AS current_job_template_icon, + COALESCE(previous_template.name, ''::text) AS previous_job_template_name, + COALESCE(previous_template.display_name, ''::text) AS previous_job_template_display_name, + COALESCE(previous_template.icon, ''::text) AS previous_job_template_icon FROM provisioner_daemons pd JOIN @@ -5584,6 +5794,7 @@ JOIN LEFT JOIN provisioner_jobs current_job ON ( current_job.worker_id = pd.id + AND current_job.organization_id = pd.organization_id AND current_job.completed_at IS NULL ) LEFT JOIN @@ -5595,11 +5806,39 @@ LEFT JOIN provisioner_jobs WHERE worker_id = pd.id + AND organization_id = pd.organization_id AND completed_at IS NOT NULL ORDER BY completed_at DESC LIMIT 1 ) + AND previous_job.organization_id = pd.organization_id + ) +LEFT JOIN + workspace_builds current_build ON current_build.id = CASE WHEN current_job.input ? 'workspace_build_id' THEN (current_job.input->>'workspace_build_id')::uuid END +LEFT JOIN + -- We should always have a template version, either explicitly or implicitly via workspace build. + template_versions current_version ON ( + current_version.id = CASE WHEN current_job.input ? 'template_version_id' THEN (current_job.input->>'template_version_id')::uuid ELSE current_build.template_version_id END + AND current_version.organization_id = pd.organization_id + ) +LEFT JOIN + templates current_template ON ( + current_template.id = current_version.template_id + AND current_template.organization_id = pd.organization_id + ) +LEFT JOIN + workspace_builds previous_build ON previous_build.id = CASE WHEN previous_job.input ? 'workspace_build_id' THEN (previous_job.input->>'workspace_build_id')::uuid END +LEFT JOIN + -- We should always have a template version, either explicitly or implicitly via workspace build. + template_versions previous_version ON ( + previous_version.id = CASE WHEN previous_job.input ? 'template_version_id' THEN (previous_job.input->>'template_version_id')::uuid ELSE previous_build.template_version_id END + AND previous_version.organization_id = pd.organization_id + ) +LEFT JOIN + templates previous_template ON ( + previous_template.id = previous_version.template_id + AND previous_template.organization_id = pd.organization_id ) WHERE pd.organization_id = $2::uuid @@ -5607,31 +5846,43 @@ WHERE AND ($4::tagset = 'null'::tagset OR provisioner_tagset_contains(pd.tags::tagset, $4::tagset)) ORDER BY pd.created_at ASC +LIMIT + $5::int ` type GetProvisionerDaemonsWithStatusByOrganizationParams struct { - StaleIntervalMS int64 `db:"stale_interval_ms" json:"stale_interval_ms"` - OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` - IDs []uuid.UUID `db:"ids" json:"ids"` - Tags StringMap `db:"tags" json:"tags"` + StaleIntervalMS int64 `db:"stale_interval_ms" json:"stale_interval_ms"` + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` + IDs []uuid.UUID `db:"ids" json:"ids"` + Tags StringMap `db:"tags" json:"tags"` + Limit sql.NullInt32 `db:"limit" json:"limit"` } type GetProvisionerDaemonsWithStatusByOrganizationRow struct { - ProvisionerDaemon ProvisionerDaemon `db:"provisioner_daemon" json:"provisioner_daemon"` - Status ProvisionerDaemonStatus `db:"status" json:"status"` - KeyName string `db:"key_name" json:"key_name"` - CurrentJobID uuid.NullUUID `db:"current_job_id" json:"current_job_id"` - CurrentJobStatus NullProvisionerJobStatus `db:"current_job_status" json:"current_job_status"` - PreviousJobID uuid.NullUUID `db:"previous_job_id" json:"previous_job_id"` - PreviousJobStatus NullProvisionerJobStatus `db:"previous_job_status" json:"previous_job_status"` -} - + ProvisionerDaemon ProvisionerDaemon `db:"provisioner_daemon" json:"provisioner_daemon"` + Status ProvisionerDaemonStatus `db:"status" json:"status"` + KeyName string `db:"key_name" json:"key_name"` + CurrentJobID uuid.NullUUID `db:"current_job_id" json:"current_job_id"` + CurrentJobStatus NullProvisionerJobStatus `db:"current_job_status" json:"current_job_status"` + PreviousJobID uuid.NullUUID `db:"previous_job_id" json:"previous_job_id"` + PreviousJobStatus NullProvisionerJobStatus `db:"previous_job_status" json:"previous_job_status"` + CurrentJobTemplateName string `db:"current_job_template_name" json:"current_job_template_name"` + CurrentJobTemplateDisplayName string `db:"current_job_template_display_name" json:"current_job_template_display_name"` + CurrentJobTemplateIcon string `db:"current_job_template_icon" json:"current_job_template_icon"` + PreviousJobTemplateName string `db:"previous_job_template_name" json:"previous_job_template_name"` + PreviousJobTemplateDisplayName string `db:"previous_job_template_display_name" json:"previous_job_template_display_name"` + PreviousJobTemplateIcon string `db:"previous_job_template_icon" json:"previous_job_template_icon"` +} + +// Current job information. +// Previous job information. func (q *sqlQuerier) GetProvisionerDaemonsWithStatusByOrganization(ctx context.Context, arg GetProvisionerDaemonsWithStatusByOrganizationParams) ([]GetProvisionerDaemonsWithStatusByOrganizationRow, error) { rows, err := q.db.QueryContext(ctx, getProvisionerDaemonsWithStatusByOrganization, arg.StaleIntervalMS, arg.OrganizationID, pq.Array(arg.IDs), arg.Tags, + arg.Limit, ) if err != nil { return nil, err @@ -5658,6 +5909,12 @@ func (q *sqlQuerier) GetProvisionerDaemonsWithStatusByOrganization(ctx context.C &i.CurrentJobStatus, &i.PreviousJobID, &i.PreviousJobStatus, + &i.CurrentJobTemplateName, + &i.CurrentJobTemplateDisplayName, + &i.CurrentJobTemplateIcon, + &i.PreviousJobTemplateName, + &i.PreviousJobTemplateDisplayName, + &i.PreviousJobTemplateIcon, ); err != nil { return nil, err } @@ -6265,39 +6522,81 @@ SELECT AND pj.organization_id = pd.organization_id AND pj.provisioner = ANY(pd.provisioners) AND provisioner_tagset_contains(pd.tags, pj.tags) - ) AS available_workers + ) AS available_workers, + -- Include template and workspace information. + COALESCE(tv.name, '') AS template_version_name, + t.id AS template_id, + COALESCE(t.name, '') AS template_name, + COALESCE(t.display_name, '') AS template_display_name, + COALESCE(t.icon, '') AS template_icon, + w.id AS workspace_id, + COALESCE(w.name, '') AS workspace_name FROM provisioner_jobs pj LEFT JOIN queue_position qp ON qp.id = pj.id LEFT JOIN queue_size qs ON TRUE +LEFT JOIN + workspace_builds wb ON wb.id = CASE WHEN pj.input ? 'workspace_build_id' THEN (pj.input->>'workspace_build_id')::uuid END +LEFT JOIN + workspaces w ON ( + w.id = wb.workspace_id + AND w.organization_id = pj.organization_id + ) +LEFT JOIN + -- We should always have a template version, either explicitly or implicitly via workspace build. + template_versions tv ON ( + tv.id = CASE WHEN pj.input ? 'template_version_id' THEN (pj.input->>'template_version_id')::uuid ELSE wb.template_version_id END + AND tv.organization_id = pj.organization_id + ) +LEFT JOIN + templates t ON ( + t.id = tv.template_id + AND t.organization_id = pj.organization_id + ) WHERE - ($1::uuid IS NULL OR pj.organization_id = $1) + pj.organization_id = $1::uuid AND (COALESCE(array_length($2::uuid[], 1), 0) = 0 OR pj.id = ANY($2::uuid[])) AND (COALESCE(array_length($3::provisioner_job_status[], 1), 0) = 0 OR pj.job_status = ANY($3::provisioner_job_status[])) + AND ($4::tagset = 'null'::tagset OR provisioner_tagset_contains(pj.tags::tagset, $4::tagset)) GROUP BY pj.id, qp.queue_position, - qs.count + qs.count, + tv.name, + t.id, + t.name, + t.display_name, + t.icon, + w.id, + w.name ORDER BY pj.created_at DESC LIMIT - $4::int + $5::int ` type GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams struct { - OrganizationID uuid.NullUUID `db:"organization_id" json:"organization_id"` + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` IDs []uuid.UUID `db:"ids" json:"ids"` Status []ProvisionerJobStatus `db:"status" json:"status"` + Tags StringMap `db:"tags" json:"tags"` Limit sql.NullInt32 `db:"limit" json:"limit"` } type GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow struct { - ProvisionerJob ProvisionerJob `db:"provisioner_job" json:"provisioner_job"` - QueuePosition int64 `db:"queue_position" json:"queue_position"` - QueueSize int64 `db:"queue_size" json:"queue_size"` - AvailableWorkers []uuid.UUID `db:"available_workers" json:"available_workers"` + ProvisionerJob ProvisionerJob `db:"provisioner_job" json:"provisioner_job"` + QueuePosition int64 `db:"queue_position" json:"queue_position"` + QueueSize int64 `db:"queue_size" json:"queue_size"` + AvailableWorkers []uuid.UUID `db:"available_workers" json:"available_workers"` + TemplateVersionName string `db:"template_version_name" json:"template_version_name"` + TemplateID uuid.NullUUID `db:"template_id" json:"template_id"` + TemplateName string `db:"template_name" json:"template_name"` + TemplateDisplayName string `db:"template_display_name" json:"template_display_name"` + TemplateIcon string `db:"template_icon" json:"template_icon"` + WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"` + WorkspaceName string `db:"workspace_name" json:"workspace_name"` } func (q *sqlQuerier) GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner(ctx context.Context, arg GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams) ([]GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow, error) { @@ -6305,6 +6604,7 @@ func (q *sqlQuerier) GetProvisionerJobsByOrganizationAndStatusWithQueuePositionA arg.OrganizationID, pq.Array(arg.IDs), pq.Array(arg.Status), + arg.Tags, arg.Limit, ) if err != nil { @@ -6337,6 +6637,13 @@ func (q *sqlQuerier) GetProvisionerJobsByOrganizationAndStatusWithQueuePositionA &i.QueuePosition, &i.QueueSize, pq.Array(&i.AvailableWorkers), + &i.TemplateVersionName, + &i.TemplateID, + &i.TemplateName, + &i.TemplateDisplayName, + &i.TemplateIcon, + &i.WorkspaceID, + &i.WorkspaceName, ); err != nil { return nil, err } @@ -7468,25 +7775,25 @@ SELECT FROM custom_roles WHERE - true - -- @lookup_roles will filter for exact (role_name, org_id) pairs - -- To do this manually in SQL, you can construct an array and cast it: - -- cast(ARRAY[('customrole','ece79dac-926e-44ca-9790-2ff7c5eb6e0c')] AS name_organization_pair[]) - AND CASE WHEN array_length($1 :: name_organization_pair[], 1) > 0 THEN - -- Using 'coalesce' to avoid troubles with null literals being an empty string. - (name, coalesce(organization_id, '00000000-0000-0000-0000-000000000000' ::uuid)) = ANY ($1::name_organization_pair[]) - ELSE true - END - -- This allows fetching all roles, or just site wide roles - AND CASE WHEN $2 :: boolean THEN - organization_id IS null + true + -- @lookup_roles will filter for exact (role_name, org_id) pairs + -- To do this manually in SQL, you can construct an array and cast it: + -- cast(ARRAY[('customrole','ece79dac-926e-44ca-9790-2ff7c5eb6e0c')] AS name_organization_pair[]) + AND CASE WHEN array_length($1 :: name_organization_pair[], 1) > 0 THEN + -- Using 'coalesce' to avoid troubles with null literals being an empty string. + (name, coalesce(organization_id, '00000000-0000-0000-0000-000000000000' ::uuid)) = ANY ($1::name_organization_pair[]) + ELSE true + END + -- This allows fetching all roles, or just site wide roles + AND CASE WHEN $2 :: boolean THEN + organization_id IS null ELSE true - END - -- Allows fetching all roles to a particular organization - AND CASE WHEN $3 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN - organization_id = $3 - ELSE true - END + END + -- Allows fetching all roles to a particular organization + AND CASE WHEN $3 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN + organization_id = $3 + ELSE true + END ` type CustomRolesParams struct { @@ -7559,16 +7866,16 @@ INSERT INTO updated_at ) VALUES ( - -- Always force lowercase names - lower($1), - $2, - $3, - $4, - $5, - $6, - now(), - now() - ) + -- Always force lowercase names + lower($1), + $2, + $3, + $4, + $5, + $6, + now(), + now() +) RETURNING name, display_name, site_permissions, org_permissions, user_permissions, created_at, updated_at, organization_id, id ` @@ -7793,6 +8100,23 @@ func (q *sqlQuerier) GetNotificationsSettings(ctx context.Context) (string, erro return notifications_settings, err } +const getOAuth2GithubDefaultEligible = `-- name: GetOAuth2GithubDefaultEligible :one +SELECT + CASE + WHEN value = 'true' THEN TRUE + ELSE FALSE + END +FROM site_configs +WHERE key = 'oauth2_github_default_eligible' +` + +func (q *sqlQuerier) GetOAuth2GithubDefaultEligible(ctx context.Context) (bool, error) { + row := q.db.QueryRowContext(ctx, getOAuth2GithubDefaultEligible) + var column_1 bool + err := row.Scan(&column_1) + return column_1, err +} + const getOAuthSigningKey = `-- name: GetOAuthSigningKey :one SELECT value FROM site_configs WHERE key = 'oauth_signing_key' ` @@ -7936,6 +8260,28 @@ func (q *sqlQuerier) UpsertNotificationsSettings(ctx context.Context, value stri return err } +const upsertOAuth2GithubDefaultEligible = `-- name: UpsertOAuth2GithubDefaultEligible :exec +INSERT INTO site_configs (key, value) +VALUES ( + 'oauth2_github_default_eligible', + CASE + WHEN $1::bool THEN 'true' + ELSE 'false' + END +) +ON CONFLICT (key) DO UPDATE +SET value = CASE + WHEN $1::bool THEN 'true' + ELSE 'false' +END +WHERE site_configs.key = 'oauth2_github_default_eligible' +` + +func (q *sqlQuerier) UpsertOAuth2GithubDefaultEligible(ctx context.Context, eligible bool) error { + _, err := q.db.ExecContext(ctx, upsertOAuth2GithubDefaultEligible, eligible) + return err +} + const upsertOAuthSigningKey = `-- name: UpsertOAuthSigningKey :exec INSERT INTO site_configs (key, value) VALUES ('oauth_signing_key', $1) ON CONFLICT (key) DO UPDATE set value = $1 WHERE site_configs.key = 'oauth_signing_key' @@ -8702,6 +9048,86 @@ func (q *sqlQuerier) UpsertTailnetTunnel(ctx context.Context, arg UpsertTailnetT return i, err } +const getTelemetryItem = `-- name: GetTelemetryItem :one +SELECT key, value, created_at, updated_at FROM telemetry_items WHERE key = $1 +` + +func (q *sqlQuerier) GetTelemetryItem(ctx context.Context, key string) (TelemetryItem, error) { + row := q.db.QueryRowContext(ctx, getTelemetryItem, key) + var i TelemetryItem + err := row.Scan( + &i.Key, + &i.Value, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const getTelemetryItems = `-- name: GetTelemetryItems :many +SELECT key, value, created_at, updated_at FROM telemetry_items +` + +func (q *sqlQuerier) GetTelemetryItems(ctx context.Context) ([]TelemetryItem, error) { + rows, err := q.db.QueryContext(ctx, getTelemetryItems) + if err != nil { + return nil, err + } + defer rows.Close() + var items []TelemetryItem + for rows.Next() { + var i TelemetryItem + if err := rows.Scan( + &i.Key, + &i.Value, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const insertTelemetryItemIfNotExists = `-- name: InsertTelemetryItemIfNotExists :exec +INSERT INTO telemetry_items (key, value) +VALUES ($1, $2) +ON CONFLICT (key) DO NOTHING +` + +type InsertTelemetryItemIfNotExistsParams struct { + Key string `db:"key" json:"key"` + Value string `db:"value" json:"value"` +} + +func (q *sqlQuerier) InsertTelemetryItemIfNotExists(ctx context.Context, arg InsertTelemetryItemIfNotExistsParams) error { + _, err := q.db.ExecContext(ctx, insertTelemetryItemIfNotExists, arg.Key, arg.Value) + return err +} + +const upsertTelemetryItem = `-- name: UpsertTelemetryItem :exec +INSERT INTO telemetry_items (key, value) +VALUES ($1, $2) +ON CONFLICT (key) DO UPDATE SET value = $2, updated_at = NOW() WHERE telemetry_items.key = $1 +` + +type UpsertTelemetryItemParams struct { + Key string `db:"key" json:"key"` + Value string `db:"value" json:"value"` +} + +func (q *sqlQuerier) UpsertTelemetryItem(ctx context.Context, arg UpsertTelemetryItemParams) error { + _, err := q.db.ExecContext(ctx, upsertTelemetryItem, arg.Key, arg.Value) + return err +} + const getTemplateAverageBuildTime = `-- name: GetTemplateAverageBuildTime :one WITH build_times AS ( SELECT @@ -11685,6 +12111,308 @@ func (q *sqlQuerier) UpsertWorkspaceAgentPortShare(ctx context.Context, arg Upse return i, err } +const fetchMemoryResourceMonitorsByAgentID = `-- name: FetchMemoryResourceMonitorsByAgentID :one +SELECT + agent_id, enabled, threshold, created_at, updated_at, state, debounced_until +FROM + workspace_agent_memory_resource_monitors +WHERE + agent_id = $1 +` + +func (q *sqlQuerier) FetchMemoryResourceMonitorsByAgentID(ctx context.Context, agentID uuid.UUID) (WorkspaceAgentMemoryResourceMonitor, error) { + row := q.db.QueryRowContext(ctx, fetchMemoryResourceMonitorsByAgentID, agentID) + var i WorkspaceAgentMemoryResourceMonitor + err := row.Scan( + &i.AgentID, + &i.Enabled, + &i.Threshold, + &i.CreatedAt, + &i.UpdatedAt, + &i.State, + &i.DebouncedUntil, + ) + return i, err +} + +const fetchMemoryResourceMonitorsUpdatedAfter = `-- name: FetchMemoryResourceMonitorsUpdatedAfter :many +SELECT + agent_id, enabled, threshold, created_at, updated_at, state, debounced_until +FROM + workspace_agent_memory_resource_monitors +WHERE + updated_at > $1 +` + +func (q *sqlQuerier) FetchMemoryResourceMonitorsUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]WorkspaceAgentMemoryResourceMonitor, error) { + rows, err := q.db.QueryContext(ctx, fetchMemoryResourceMonitorsUpdatedAfter, updatedAt) + if err != nil { + return nil, err + } + defer rows.Close() + var items []WorkspaceAgentMemoryResourceMonitor + for rows.Next() { + var i WorkspaceAgentMemoryResourceMonitor + if err := rows.Scan( + &i.AgentID, + &i.Enabled, + &i.Threshold, + &i.CreatedAt, + &i.UpdatedAt, + &i.State, + &i.DebouncedUntil, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const fetchVolumesResourceMonitorsByAgentID = `-- name: FetchVolumesResourceMonitorsByAgentID :many +SELECT + agent_id, enabled, threshold, path, created_at, updated_at, state, debounced_until +FROM + workspace_agent_volume_resource_monitors +WHERE + agent_id = $1 +` + +func (q *sqlQuerier) FetchVolumesResourceMonitorsByAgentID(ctx context.Context, agentID uuid.UUID) ([]WorkspaceAgentVolumeResourceMonitor, error) { + rows, err := q.db.QueryContext(ctx, fetchVolumesResourceMonitorsByAgentID, agentID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []WorkspaceAgentVolumeResourceMonitor + for rows.Next() { + var i WorkspaceAgentVolumeResourceMonitor + if err := rows.Scan( + &i.AgentID, + &i.Enabled, + &i.Threshold, + &i.Path, + &i.CreatedAt, + &i.UpdatedAt, + &i.State, + &i.DebouncedUntil, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const fetchVolumesResourceMonitorsUpdatedAfter = `-- name: FetchVolumesResourceMonitorsUpdatedAfter :many +SELECT + agent_id, enabled, threshold, path, created_at, updated_at, state, debounced_until +FROM + workspace_agent_volume_resource_monitors +WHERE + updated_at > $1 +` + +func (q *sqlQuerier) FetchVolumesResourceMonitorsUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]WorkspaceAgentVolumeResourceMonitor, error) { + rows, err := q.db.QueryContext(ctx, fetchVolumesResourceMonitorsUpdatedAfter, updatedAt) + if err != nil { + return nil, err + } + defer rows.Close() + var items []WorkspaceAgentVolumeResourceMonitor + for rows.Next() { + var i WorkspaceAgentVolumeResourceMonitor + if err := rows.Scan( + &i.AgentID, + &i.Enabled, + &i.Threshold, + &i.Path, + &i.CreatedAt, + &i.UpdatedAt, + &i.State, + &i.DebouncedUntil, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const insertMemoryResourceMonitor = `-- name: InsertMemoryResourceMonitor :one +INSERT INTO + workspace_agent_memory_resource_monitors ( + agent_id, + enabled, + state, + threshold, + created_at, + updated_at, + debounced_until + ) +VALUES + ($1, $2, $3, $4, $5, $6, $7) RETURNING agent_id, enabled, threshold, created_at, updated_at, state, debounced_until +` + +type InsertMemoryResourceMonitorParams struct { + AgentID uuid.UUID `db:"agent_id" json:"agent_id"` + Enabled bool `db:"enabled" json:"enabled"` + State WorkspaceAgentMonitorState `db:"state" json:"state"` + Threshold int32 `db:"threshold" json:"threshold"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + DebouncedUntil time.Time `db:"debounced_until" json:"debounced_until"` +} + +func (q *sqlQuerier) InsertMemoryResourceMonitor(ctx context.Context, arg InsertMemoryResourceMonitorParams) (WorkspaceAgentMemoryResourceMonitor, error) { + row := q.db.QueryRowContext(ctx, insertMemoryResourceMonitor, + arg.AgentID, + arg.Enabled, + arg.State, + arg.Threshold, + arg.CreatedAt, + arg.UpdatedAt, + arg.DebouncedUntil, + ) + var i WorkspaceAgentMemoryResourceMonitor + err := row.Scan( + &i.AgentID, + &i.Enabled, + &i.Threshold, + &i.CreatedAt, + &i.UpdatedAt, + &i.State, + &i.DebouncedUntil, + ) + return i, err +} + +const insertVolumeResourceMonitor = `-- name: InsertVolumeResourceMonitor :one +INSERT INTO + workspace_agent_volume_resource_monitors ( + agent_id, + path, + enabled, + state, + threshold, + created_at, + updated_at, + debounced_until + ) +VALUES + ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING agent_id, enabled, threshold, path, created_at, updated_at, state, debounced_until +` + +type InsertVolumeResourceMonitorParams struct { + AgentID uuid.UUID `db:"agent_id" json:"agent_id"` + Path string `db:"path" json:"path"` + Enabled bool `db:"enabled" json:"enabled"` + State WorkspaceAgentMonitorState `db:"state" json:"state"` + Threshold int32 `db:"threshold" json:"threshold"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + DebouncedUntil time.Time `db:"debounced_until" json:"debounced_until"` +} + +func (q *sqlQuerier) InsertVolumeResourceMonitor(ctx context.Context, arg InsertVolumeResourceMonitorParams) (WorkspaceAgentVolumeResourceMonitor, error) { + row := q.db.QueryRowContext(ctx, insertVolumeResourceMonitor, + arg.AgentID, + arg.Path, + arg.Enabled, + arg.State, + arg.Threshold, + arg.CreatedAt, + arg.UpdatedAt, + arg.DebouncedUntil, + ) + var i WorkspaceAgentVolumeResourceMonitor + err := row.Scan( + &i.AgentID, + &i.Enabled, + &i.Threshold, + &i.Path, + &i.CreatedAt, + &i.UpdatedAt, + &i.State, + &i.DebouncedUntil, + ) + return i, err +} + +const updateMemoryResourceMonitor = `-- name: UpdateMemoryResourceMonitor :exec +UPDATE workspace_agent_memory_resource_monitors +SET + updated_at = $2, + state = $3, + debounced_until = $4 +WHERE + agent_id = $1 +` + +type UpdateMemoryResourceMonitorParams struct { + AgentID uuid.UUID `db:"agent_id" json:"agent_id"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + State WorkspaceAgentMonitorState `db:"state" json:"state"` + DebouncedUntil time.Time `db:"debounced_until" json:"debounced_until"` +} + +func (q *sqlQuerier) UpdateMemoryResourceMonitor(ctx context.Context, arg UpdateMemoryResourceMonitorParams) error { + _, err := q.db.ExecContext(ctx, updateMemoryResourceMonitor, + arg.AgentID, + arg.UpdatedAt, + arg.State, + arg.DebouncedUntil, + ) + return err +} + +const updateVolumeResourceMonitor = `-- name: UpdateVolumeResourceMonitor :exec +UPDATE workspace_agent_volume_resource_monitors +SET + updated_at = $3, + state = $4, + debounced_until = $5 +WHERE + agent_id = $1 AND path = $2 +` + +type UpdateVolumeResourceMonitorParams struct { + AgentID uuid.UUID `db:"agent_id" json:"agent_id"` + Path string `db:"path" json:"path"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + State WorkspaceAgentMonitorState `db:"state" json:"state"` + DebouncedUntil time.Time `db:"debounced_until" json:"debounced_until"` +} + +func (q *sqlQuerier) UpdateVolumeResourceMonitor(ctx context.Context, arg UpdateVolumeResourceMonitorParams) error { + _, err := q.db.ExecContext(ctx, updateVolumeResourceMonitor, + arg.AgentID, + arg.Path, + arg.UpdatedAt, + arg.State, + arg.DebouncedUntil, + ) + return err +} + const deleteOldWorkspaceAgentLogs = `-- name: DeleteOldWorkspaceAgentLogs :exec WITH latest_builds AS ( @@ -11740,7 +12468,7 @@ const getWorkspaceAgentAndLatestBuildByAuthToken = `-- name: GetWorkspaceAgentAn SELECT workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at, workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order, - workspace_build_with_user.id, workspace_build_with_user.created_at, workspace_build_with_user.updated_at, workspace_build_with_user.workspace_id, workspace_build_with_user.template_version_id, workspace_build_with_user.build_number, workspace_build_with_user.transition, workspace_build_with_user.initiator_id, workspace_build_with_user.provisioner_state, workspace_build_with_user.job_id, workspace_build_with_user.deadline, workspace_build_with_user.reason, workspace_build_with_user.daily_cost, workspace_build_with_user.max_deadline, workspace_build_with_user.initiator_by_avatar_url, workspace_build_with_user.initiator_by_username + workspace_build_with_user.id, workspace_build_with_user.created_at, workspace_build_with_user.updated_at, workspace_build_with_user.workspace_id, workspace_build_with_user.template_version_id, workspace_build_with_user.build_number, workspace_build_with_user.transition, workspace_build_with_user.initiator_id, workspace_build_with_user.provisioner_state, workspace_build_with_user.job_id, workspace_build_with_user.deadline, workspace_build_with_user.reason, workspace_build_with_user.daily_cost, workspace_build_with_user.max_deadline, workspace_build_with_user.template_version_preset_id, workspace_build_with_user.initiator_by_avatar_url, workspace_build_with_user.initiator_by_username FROM workspace_agents JOIN @@ -11843,6 +12571,7 @@ func (q *sqlQuerier) GetWorkspaceAgentAndLatestBuildByAuthToken(ctx context.Cont &i.WorkspaceBuild.Reason, &i.WorkspaceBuild.DailyCost, &i.WorkspaceBuild.MaxDeadline, + &i.WorkspaceBuild.TemplateVersionPresetID, &i.WorkspaceBuild.InitiatorByAvatarUrl, &i.WorkspaceBuild.InitiatorByUsername, ) @@ -14046,7 +14775,7 @@ func (q *sqlQuerier) InsertWorkspaceBuildParameters(ctx context.Context, arg Ins } const getActiveWorkspaceBuildsByTemplateID = `-- name: GetActiveWorkspaceBuildsByTemplateID :many -SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.provisioner_state, wb.job_id, wb.deadline, wb.reason, wb.daily_cost, wb.max_deadline, wb.initiator_by_avatar_url, wb.initiator_by_username +SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.provisioner_state, wb.job_id, wb.deadline, wb.reason, wb.daily_cost, wb.max_deadline, wb.template_version_preset_id, wb.initiator_by_avatar_url, wb.initiator_by_username FROM ( SELECT workspace_id, MAX(build_number) as max_build_number @@ -14100,6 +14829,7 @@ func (q *sqlQuerier) GetActiveWorkspaceBuildsByTemplateID(ctx context.Context, t &i.Reason, &i.DailyCost, &i.MaxDeadline, + &i.TemplateVersionPresetID, &i.InitiatorByAvatarUrl, &i.InitiatorByUsername, ); err != nil { @@ -14195,7 +14925,7 @@ func (q *sqlQuerier) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, a const getLatestWorkspaceBuildByWorkspaceID = `-- name: GetLatestWorkspaceBuildByWorkspaceID :one SELECT - id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, initiator_by_avatar_url, initiator_by_username + id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, initiator_by_avatar_url, initiator_by_username FROM workspace_build_with_user AS workspace_builds WHERE @@ -14224,6 +14954,7 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, w &i.Reason, &i.DailyCost, &i.MaxDeadline, + &i.TemplateVersionPresetID, &i.InitiatorByAvatarUrl, &i.InitiatorByUsername, ) @@ -14231,7 +14962,7 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, w } const getLatestWorkspaceBuilds = `-- name: GetLatestWorkspaceBuilds :many -SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.provisioner_state, wb.job_id, wb.deadline, wb.reason, wb.daily_cost, wb.max_deadline, wb.initiator_by_avatar_url, wb.initiator_by_username +SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.provisioner_state, wb.job_id, wb.deadline, wb.reason, wb.daily_cost, wb.max_deadline, wb.template_version_preset_id, wb.initiator_by_avatar_url, wb.initiator_by_username FROM ( SELECT workspace_id, MAX(build_number) as max_build_number @@ -14269,6 +15000,7 @@ func (q *sqlQuerier) GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceB &i.Reason, &i.DailyCost, &i.MaxDeadline, + &i.TemplateVersionPresetID, &i.InitiatorByAvatarUrl, &i.InitiatorByUsername, ); err != nil { @@ -14286,7 +15018,7 @@ func (q *sqlQuerier) GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceB } const getLatestWorkspaceBuildsByWorkspaceIDs = `-- name: GetLatestWorkspaceBuildsByWorkspaceIDs :many -SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.provisioner_state, wb.job_id, wb.deadline, wb.reason, wb.daily_cost, wb.max_deadline, wb.initiator_by_avatar_url, wb.initiator_by_username +SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.provisioner_state, wb.job_id, wb.deadline, wb.reason, wb.daily_cost, wb.max_deadline, wb.template_version_preset_id, wb.initiator_by_avatar_url, wb.initiator_by_username FROM ( SELECT workspace_id, MAX(build_number) as max_build_number @@ -14326,6 +15058,7 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, &i.Reason, &i.DailyCost, &i.MaxDeadline, + &i.TemplateVersionPresetID, &i.InitiatorByAvatarUrl, &i.InitiatorByUsername, ); err != nil { @@ -14344,7 +15077,7 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, const getWorkspaceBuildByID = `-- name: GetWorkspaceBuildByID :one SELECT - id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, initiator_by_avatar_url, initiator_by_username + id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, initiator_by_avatar_url, initiator_by_username FROM workspace_build_with_user AS workspace_builds WHERE @@ -14371,6 +15104,7 @@ func (q *sqlQuerier) GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (W &i.Reason, &i.DailyCost, &i.MaxDeadline, + &i.TemplateVersionPresetID, &i.InitiatorByAvatarUrl, &i.InitiatorByUsername, ) @@ -14379,7 +15113,7 @@ func (q *sqlQuerier) GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (W const getWorkspaceBuildByJobID = `-- name: GetWorkspaceBuildByJobID :one SELECT - id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, initiator_by_avatar_url, initiator_by_username + id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, initiator_by_avatar_url, initiator_by_username FROM workspace_build_with_user AS workspace_builds WHERE @@ -14406,6 +15140,7 @@ func (q *sqlQuerier) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UU &i.Reason, &i.DailyCost, &i.MaxDeadline, + &i.TemplateVersionPresetID, &i.InitiatorByAvatarUrl, &i.InitiatorByUsername, ) @@ -14414,7 +15149,7 @@ func (q *sqlQuerier) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UU const getWorkspaceBuildByWorkspaceIDAndBuildNumber = `-- name: GetWorkspaceBuildByWorkspaceIDAndBuildNumber :one SELECT - id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, initiator_by_avatar_url, initiator_by_username + id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, initiator_by_avatar_url, initiator_by_username FROM workspace_build_with_user AS workspace_builds WHERE @@ -14445,6 +15180,7 @@ func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Co &i.Reason, &i.DailyCost, &i.MaxDeadline, + &i.TemplateVersionPresetID, &i.InitiatorByAvatarUrl, &i.InitiatorByUsername, ) @@ -14520,7 +15256,7 @@ func (q *sqlQuerier) GetWorkspaceBuildStatsByTemplates(ctx context.Context, sinc const getWorkspaceBuildsByWorkspaceID = `-- name: GetWorkspaceBuildsByWorkspaceID :many SELECT - id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, initiator_by_avatar_url, initiator_by_username + id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, initiator_by_avatar_url, initiator_by_username FROM workspace_build_with_user AS workspace_builds WHERE @@ -14590,6 +15326,7 @@ func (q *sqlQuerier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg Ge &i.Reason, &i.DailyCost, &i.MaxDeadline, + &i.TemplateVersionPresetID, &i.InitiatorByAvatarUrl, &i.InitiatorByUsername, ); err != nil { @@ -14607,7 +15344,7 @@ func (q *sqlQuerier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg Ge } const getWorkspaceBuildsCreatedAfter = `-- name: GetWorkspaceBuildsCreatedAfter :many -SELECT id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, initiator_by_avatar_url, initiator_by_username FROM workspace_build_with_user WHERE created_at > $1 +SELECT id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, initiator_by_avatar_url, initiator_by_username FROM workspace_build_with_user WHERE created_at > $1 ` func (q *sqlQuerier) GetWorkspaceBuildsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceBuild, error) { @@ -14634,6 +15371,7 @@ func (q *sqlQuerier) GetWorkspaceBuildsCreatedAfter(ctx context.Context, created &i.Reason, &i.DailyCost, &i.MaxDeadline, + &i.TemplateVersionPresetID, &i.InitiatorByAvatarUrl, &i.InitiatorByUsername, ); err != nil { @@ -14665,26 +15403,28 @@ INSERT INTO provisioner_state, deadline, max_deadline, - reason + reason, + template_version_preset_id ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) ` type InsertWorkspaceBuildParams struct { - ID uuid.UUID `db:"id" json:"id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` - WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` - TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` - BuildNumber int32 `db:"build_number" json:"build_number"` - Transition WorkspaceTransition `db:"transition" json:"transition"` - InitiatorID uuid.UUID `db:"initiator_id" json:"initiator_id"` - JobID uuid.UUID `db:"job_id" json:"job_id"` - ProvisionerState []byte `db:"provisioner_state" json:"provisioner_state"` - Deadline time.Time `db:"deadline" json:"deadline"` - MaxDeadline time.Time `db:"max_deadline" json:"max_deadline"` - Reason BuildReason `db:"reason" json:"reason"` + ID uuid.UUID `db:"id" json:"id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` + TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` + BuildNumber int32 `db:"build_number" json:"build_number"` + Transition WorkspaceTransition `db:"transition" json:"transition"` + InitiatorID uuid.UUID `db:"initiator_id" json:"initiator_id"` + JobID uuid.UUID `db:"job_id" json:"job_id"` + ProvisionerState []byte `db:"provisioner_state" json:"provisioner_state"` + Deadline time.Time `db:"deadline" json:"deadline"` + MaxDeadline time.Time `db:"max_deadline" json:"max_deadline"` + Reason BuildReason `db:"reason" json:"reason"` + TemplateVersionPresetID uuid.NullUUID `db:"template_version_preset_id" json:"template_version_preset_id"` } func (q *sqlQuerier) InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) error { @@ -14702,6 +15442,7 @@ func (q *sqlQuerier) InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspa arg.Deadline, arg.MaxDeadline, arg.Reason, + arg.TemplateVersionPresetID, ) return err } diff --git a/coderd/database/queries/auditlogs.sql b/coderd/database/queries/auditlogs.sql index 115bdcd4c8f6f..52efc40c73738 100644 --- a/coderd/database/queries/auditlogs.sql +++ b/coderd/database/queries/auditlogs.sql @@ -117,6 +117,12 @@ WHERE workspace_builds.reason::text = @build_reason ELSE true END + -- Filter request_id + AND CASE + WHEN @request_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN + audit_logs.request_id = @request_id + ELSE true + END -- Authorize Filter clause will be injected below in GetAuthorizedAuditLogsOffset -- @authorize_filter diff --git a/coderd/database/queries/organizations.sql b/coderd/database/queries/organizations.sql index 3a74170a913e1..822b51c0aa8ba 100644 --- a/coderd/database/queries/organizations.sql +++ b/coderd/database/queries/organizations.sql @@ -1,89 +1,97 @@ -- name: GetDefaultOrganization :one SELECT - * + * FROM - organizations + organizations WHERE - is_default = true + is_default = true LIMIT - 1; + 1; -- name: GetOrganizations :many SELECT - * + * FROM - organizations + organizations WHERE - true - -- Filter by ids - AND CASE - WHEN array_length(@ids :: uuid[], 1) > 0 THEN - id = ANY(@ids) - ELSE true - END - AND CASE - WHEN @name::text != '' THEN - LOWER("name") = LOWER(@name) - ELSE true - END + -- Optionally include deleted organizations + deleted = @deleted + -- Filter by ids + AND CASE + WHEN array_length(@ids :: uuid[], 1) > 0 THEN + id = ANY(@ids) + ELSE true + END + AND CASE + WHEN @name::text != '' THEN + LOWER("name") = LOWER(@name) + ELSE true + END ; -- name: GetOrganizationByID :one SELECT - * + * FROM - organizations + organizations WHERE - id = $1; + id = $1; -- name: GetOrganizationByName :one SELECT - * + * FROM - organizations + organizations WHERE - LOWER("name") = LOWER(@name) + -- Optionally include deleted organizations + deleted = @deleted AND + LOWER("name") = LOWER(@name) LIMIT - 1; + 1; -- name: GetOrganizationsByUserID :many SELECT - * + * FROM - organizations + organizations WHERE - id = ANY( - SELECT - organization_id - FROM - organization_members - WHERE - user_id = $1 - ); + -- Optionally include deleted organizations + deleted = @deleted AND + id = ANY( + SELECT + organization_id + FROM + organization_members + WHERE + user_id = $1 + ); -- name: InsertOrganization :one INSERT INTO - organizations (id, "name", display_name, description, icon, created_at, updated_at, is_default) + organizations (id, "name", display_name, description, icon, created_at, updated_at, is_default) VALUES - -- If no organizations exist, and this is the first, make it the default. - (@id, @name, @display_name, @description, @icon, @created_at, @updated_at, (SELECT TRUE FROM organizations LIMIT 1) IS NULL) RETURNING *; + -- If no organizations exist, and this is the first, make it the default. + (@id, @name, @display_name, @description, @icon, @created_at, @updated_at, (SELECT TRUE FROM organizations LIMIT 1) IS NULL) RETURNING *; -- name: UpdateOrganization :one UPDATE - organizations + organizations SET - updated_at = @updated_at, - name = @name, - display_name = @display_name, - description = @description, - icon = @icon + updated_at = @updated_at, + name = @name, + display_name = @display_name, + description = @description, + icon = @icon WHERE - id = @id + id = @id RETURNING *; --- name: DeleteOrganization :exec -DELETE FROM - organizations +-- name: UpdateOrganizationDeletedByID :exec +UPDATE organizations +SET + deleted = true, + updated_at = @updated_at WHERE - id = $1 AND - is_default = false; + id = @id AND + is_default = false; + diff --git a/coderd/database/queries/presets.sql b/coderd/database/queries/presets.sql new file mode 100644 index 0000000000000..8e648fce6ca88 --- /dev/null +++ b/coderd/database/queries/presets.sql @@ -0,0 +1,40 @@ +-- name: InsertPreset :one +INSERT INTO + template_version_presets (template_version_id, name, created_at) +VALUES + (@template_version_id, @name, @created_at) RETURNING *; + +-- name: InsertPresetParameters :many +INSERT INTO + template_version_preset_parameters (template_version_preset_id, name, value) +SELECT + @template_version_preset_id, + unnest(@names :: TEXT[]), + unnest(@values :: TEXT[]) +RETURNING *; + +-- name: GetPresetsByTemplateVersionID :many +SELECT + * +FROM + template_version_presets +WHERE + template_version_id = @template_version_id; + +-- name: GetPresetByWorkspaceBuildID :one +SELECT + template_version_presets.* +FROM + template_version_presets + INNER JOIN workspace_builds ON workspace_builds.template_version_preset_id = template_version_presets.id +WHERE + workspace_builds.id = @workspace_build_id; + +-- name: GetPresetParametersByTemplateVersionID :many +SELECT + template_version_preset_parameters.* +FROM + template_version_preset_parameters + INNER JOIN template_version_presets ON template_version_preset_parameters.template_version_preset_id = template_version_presets.id +WHERE + template_version_presets.template_version_id = @template_version_id; diff --git a/coderd/database/queries/provisionerdaemons.sql b/coderd/database/queries/provisionerdaemons.sql index abf490c9ab47f..ab1668e537d6c 100644 --- a/coderd/database/queries/provisionerdaemons.sql +++ b/coderd/database/queries/provisionerdaemons.sql @@ -44,7 +44,13 @@ SELECT current_job.id AS current_job_id, current_job.job_status AS current_job_status, previous_job.id AS previous_job_id, - previous_job.job_status AS previous_job_status + previous_job.job_status AS previous_job_status, + COALESCE(current_template.name, ''::text) AS current_job_template_name, + COALESCE(current_template.display_name, ''::text) AS current_job_template_display_name, + COALESCE(current_template.icon, ''::text) AS current_job_template_icon, + COALESCE(previous_template.name, ''::text) AS previous_job_template_name, + COALESCE(previous_template.display_name, ''::text) AS previous_job_template_display_name, + COALESCE(previous_template.icon, ''::text) AS previous_job_template_icon FROM provisioner_daemons pd JOIN @@ -52,6 +58,7 @@ JOIN LEFT JOIN provisioner_jobs current_job ON ( current_job.worker_id = pd.id + AND current_job.organization_id = pd.organization_id AND current_job.completed_at IS NULL ) LEFT JOIN @@ -63,18 +70,50 @@ LEFT JOIN provisioner_jobs WHERE worker_id = pd.id + AND organization_id = pd.organization_id AND completed_at IS NOT NULL ORDER BY completed_at DESC LIMIT 1 ) + AND previous_job.organization_id = pd.organization_id + ) +-- Current job information. +LEFT JOIN + workspace_builds current_build ON current_build.id = CASE WHEN current_job.input ? 'workspace_build_id' THEN (current_job.input->>'workspace_build_id')::uuid END +LEFT JOIN + -- We should always have a template version, either explicitly or implicitly via workspace build. + template_versions current_version ON ( + current_version.id = CASE WHEN current_job.input ? 'template_version_id' THEN (current_job.input->>'template_version_id')::uuid ELSE current_build.template_version_id END + AND current_version.organization_id = pd.organization_id + ) +LEFT JOIN + templates current_template ON ( + current_template.id = current_version.template_id + AND current_template.organization_id = pd.organization_id + ) +-- Previous job information. +LEFT JOIN + workspace_builds previous_build ON previous_build.id = CASE WHEN previous_job.input ? 'workspace_build_id' THEN (previous_job.input->>'workspace_build_id')::uuid END +LEFT JOIN + -- We should always have a template version, either explicitly or implicitly via workspace build. + template_versions previous_version ON ( + previous_version.id = CASE WHEN previous_job.input ? 'template_version_id' THEN (previous_job.input->>'template_version_id')::uuid ELSE previous_build.template_version_id END + AND previous_version.organization_id = pd.organization_id + ) +LEFT JOIN + templates previous_template ON ( + previous_template.id = previous_version.template_id + AND previous_template.organization_id = pd.organization_id ) WHERE pd.organization_id = @organization_id::uuid AND (COALESCE(array_length(@ids::uuid[], 1), 0) = 0 OR pd.id = ANY(@ids::uuid[])) AND (@tags::tagset = 'null'::tagset OR provisioner_tagset_contains(pd.tags::tagset, @tags::tagset)) ORDER BY - pd.created_at ASC; + pd.created_at ASC +LIMIT + sqlc.narg('limit')::int; -- name: DeleteOldProvisionerDaemons :exec -- Delete provisioner daemons that have been created at least a week ago diff --git a/coderd/database/queries/provisionerjobs.sql b/coderd/database/queries/provisionerjobs.sql index e7078dcfbff15..592b228af2806 100644 --- a/coderd/database/queries/provisionerjobs.sql +++ b/coderd/database/queries/provisionerjobs.sql @@ -130,21 +130,55 @@ SELECT AND pj.organization_id = pd.organization_id AND pj.provisioner = ANY(pd.provisioners) AND provisioner_tagset_contains(pd.tags, pj.tags) - ) AS available_workers + ) AS available_workers, + -- Include template and workspace information. + COALESCE(tv.name, '') AS template_version_name, + t.id AS template_id, + COALESCE(t.name, '') AS template_name, + COALESCE(t.display_name, '') AS template_display_name, + COALESCE(t.icon, '') AS template_icon, + w.id AS workspace_id, + COALESCE(w.name, '') AS workspace_name FROM provisioner_jobs pj LEFT JOIN queue_position qp ON qp.id = pj.id LEFT JOIN queue_size qs ON TRUE +LEFT JOIN + workspace_builds wb ON wb.id = CASE WHEN pj.input ? 'workspace_build_id' THEN (pj.input->>'workspace_build_id')::uuid END +LEFT JOIN + workspaces w ON ( + w.id = wb.workspace_id + AND w.organization_id = pj.organization_id + ) +LEFT JOIN + -- We should always have a template version, either explicitly or implicitly via workspace build. + template_versions tv ON ( + tv.id = CASE WHEN pj.input ? 'template_version_id' THEN (pj.input->>'template_version_id')::uuid ELSE wb.template_version_id END + AND tv.organization_id = pj.organization_id + ) +LEFT JOIN + templates t ON ( + t.id = tv.template_id + AND t.organization_id = pj.organization_id + ) WHERE - (sqlc.narg('organization_id')::uuid IS NULL OR pj.organization_id = @organization_id) + pj.organization_id = @organization_id::uuid AND (COALESCE(array_length(@ids::uuid[], 1), 0) = 0 OR pj.id = ANY(@ids::uuid[])) AND (COALESCE(array_length(@status::provisioner_job_status[], 1), 0) = 0 OR pj.job_status = ANY(@status::provisioner_job_status[])) + AND (@tags::tagset = 'null'::tagset OR provisioner_tagset_contains(pj.tags::tagset, @tags::tagset)) GROUP BY pj.id, qp.queue_position, - qs.count + qs.count, + tv.name, + t.id, + t.name, + t.display_name, + t.icon, + w.id, + w.name ORDER BY pj.created_at DESC LIMIT diff --git a/coderd/database/queries/roles.sql b/coderd/database/queries/roles.sql index 7246ddb6dee2d..ee5d35d91ab65 100644 --- a/coderd/database/queries/roles.sql +++ b/coderd/database/queries/roles.sql @@ -4,25 +4,25 @@ SELECT FROM custom_roles WHERE - true - -- @lookup_roles will filter for exact (role_name, org_id) pairs - -- To do this manually in SQL, you can construct an array and cast it: - -- cast(ARRAY[('customrole','ece79dac-926e-44ca-9790-2ff7c5eb6e0c')] AS name_organization_pair[]) - AND CASE WHEN array_length(@lookup_roles :: name_organization_pair[], 1) > 0 THEN - -- Using 'coalesce' to avoid troubles with null literals being an empty string. - (name, coalesce(organization_id, '00000000-0000-0000-0000-000000000000' ::uuid)) = ANY (@lookup_roles::name_organization_pair[]) - ELSE true - END - -- This allows fetching all roles, or just site wide roles - AND CASE WHEN @exclude_org_roles :: boolean THEN - organization_id IS null + true + -- @lookup_roles will filter for exact (role_name, org_id) pairs + -- To do this manually in SQL, you can construct an array and cast it: + -- cast(ARRAY[('customrole','ece79dac-926e-44ca-9790-2ff7c5eb6e0c')] AS name_organization_pair[]) + AND CASE WHEN array_length(@lookup_roles :: name_organization_pair[], 1) > 0 THEN + -- Using 'coalesce' to avoid troubles with null literals being an empty string. + (name, coalesce(organization_id, '00000000-0000-0000-0000-000000000000' ::uuid)) = ANY (@lookup_roles::name_organization_pair[]) ELSE true - END - -- Allows fetching all roles to a particular organization - AND CASE WHEN @organization_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN - organization_id = @organization_id - ELSE true - END + END + -- This allows fetching all roles, or just site wide roles + AND CASE WHEN @exclude_org_roles :: boolean THEN + organization_id IS null + ELSE true + END + -- Allows fetching all roles to a particular organization + AND CASE WHEN @organization_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN + organization_id = @organization_id + ELSE true + END ; -- name: DeleteCustomRole :exec @@ -46,16 +46,16 @@ INSERT INTO updated_at ) VALUES ( - -- Always force lowercase names - lower(@name), - @display_name, - @organization_id, - @site_permissions, - @org_permissions, - @user_permissions, - now(), - now() - ) + -- Always force lowercase names + lower(@name), + @display_name, + @organization_id, + @site_permissions, + @org_permissions, + @user_permissions, + now(), + now() +) RETURNING *; -- name: UpdateCustomRole :one diff --git a/coderd/database/queries/siteconfig.sql b/coderd/database/queries/siteconfig.sql index e8d02372e5a4f..ab9fda7969cea 100644 --- a/coderd/database/queries/siteconfig.sql +++ b/coderd/database/queries/siteconfig.sql @@ -107,3 +107,27 @@ ON CONFLICT (key) DO UPDATE SET value = $2 WHERE site_configs.key = $1; DELETE FROM site_configs WHERE site_configs.key = $1; +-- name: GetOAuth2GithubDefaultEligible :one +SELECT + CASE + WHEN value = 'true' THEN TRUE + ELSE FALSE + END +FROM site_configs +WHERE key = 'oauth2_github_default_eligible'; + +-- name: UpsertOAuth2GithubDefaultEligible :exec +INSERT INTO site_configs (key, value) +VALUES ( + 'oauth2_github_default_eligible', + CASE + WHEN sqlc.arg(eligible)::bool THEN 'true' + ELSE 'false' + END +) +ON CONFLICT (key) DO UPDATE +SET value = CASE + WHEN sqlc.arg(eligible)::bool THEN 'true' + ELSE 'false' +END +WHERE site_configs.key = 'oauth2_github_default_eligible'; diff --git a/coderd/database/queries/telemetryitems.sql b/coderd/database/queries/telemetryitems.sql new file mode 100644 index 0000000000000..7b7349db59943 --- /dev/null +++ b/coderd/database/queries/telemetryitems.sql @@ -0,0 +1,15 @@ +-- name: InsertTelemetryItemIfNotExists :exec +INSERT INTO telemetry_items (key, value) +VALUES ($1, $2) +ON CONFLICT (key) DO NOTHING; + +-- name: GetTelemetryItem :one +SELECT * FROM telemetry_items WHERE key = $1; + +-- name: UpsertTelemetryItem :exec +INSERT INTO telemetry_items (key, value) +VALUES ($1, $2) +ON CONFLICT (key) DO UPDATE SET value = $2, updated_at = NOW() WHERE telemetry_items.key = $1; + +-- name: GetTelemetryItems :many +SELECT * FROM telemetry_items; diff --git a/coderd/database/queries/workspaceagentresourcemonitors.sql b/coderd/database/queries/workspaceagentresourcemonitors.sql new file mode 100644 index 0000000000000..50e7e818f7c67 --- /dev/null +++ b/coderd/database/queries/workspaceagentresourcemonitors.sql @@ -0,0 +1,78 @@ +-- name: FetchVolumesResourceMonitorsUpdatedAfter :many +SELECT + * +FROM + workspace_agent_volume_resource_monitors +WHERE + updated_at > $1; + +-- name: FetchMemoryResourceMonitorsUpdatedAfter :many +SELECT + * +FROM + workspace_agent_memory_resource_monitors +WHERE + updated_at > $1; + +-- name: FetchMemoryResourceMonitorsByAgentID :one +SELECT + * +FROM + workspace_agent_memory_resource_monitors +WHERE + agent_id = $1; + +-- name: FetchVolumesResourceMonitorsByAgentID :many +SELECT + * +FROM + workspace_agent_volume_resource_monitors +WHERE + agent_id = $1; + +-- name: InsertMemoryResourceMonitor :one +INSERT INTO + workspace_agent_memory_resource_monitors ( + agent_id, + enabled, + state, + threshold, + created_at, + updated_at, + debounced_until + ) +VALUES + ($1, $2, $3, $4, $5, $6, $7) RETURNING *; + +-- name: InsertVolumeResourceMonitor :one +INSERT INTO + workspace_agent_volume_resource_monitors ( + agent_id, + path, + enabled, + state, + threshold, + created_at, + updated_at, + debounced_until + ) +VALUES + ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *; + +-- name: UpdateMemoryResourceMonitor :exec +UPDATE workspace_agent_memory_resource_monitors +SET + updated_at = $2, + state = $3, + debounced_until = $4 +WHERE + agent_id = $1; + +-- name: UpdateVolumeResourceMonitor :exec +UPDATE workspace_agent_volume_resource_monitors +SET + updated_at = $3, + state = $4, + debounced_until = $5 +WHERE + agent_id = $1 AND path = $2; diff --git a/coderd/database/queries/workspacebuilds.sql b/coderd/database/queries/workspacebuilds.sql index 7050b61644e86..da349fa1441b3 100644 --- a/coderd/database/queries/workspacebuilds.sql +++ b/coderd/database/queries/workspacebuilds.sql @@ -120,10 +120,11 @@ INSERT INTO provisioner_state, deadline, max_deadline, - reason + reason, + template_version_preset_id ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13); + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14); -- name: UpdateWorkspaceBuildCostByID :exec UPDATE diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go index f253aa98ec266..db68849777247 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -38,7 +38,6 @@ const ( UniqueOauth2ProviderAppsNameKey UniqueConstraint = "oauth2_provider_apps_name_key" // ALTER TABLE ONLY oauth2_provider_apps ADD CONSTRAINT oauth2_provider_apps_name_key UNIQUE (name); UniqueOauth2ProviderAppsPkey UniqueConstraint = "oauth2_provider_apps_pkey" // ALTER TABLE ONLY oauth2_provider_apps ADD CONSTRAINT oauth2_provider_apps_pkey PRIMARY KEY (id); UniqueOrganizationMembersPkey UniqueConstraint = "organization_members_pkey" // ALTER TABLE ONLY organization_members ADD CONSTRAINT organization_members_pkey PRIMARY KEY (organization_id, user_id); - UniqueOrganizationsName UniqueConstraint = "organizations_name" // ALTER TABLE ONLY organizations ADD CONSTRAINT organizations_name UNIQUE (name); UniqueOrganizationsPkey UniqueConstraint = "organizations_pkey" // ALTER TABLE ONLY organizations ADD CONSTRAINT organizations_pkey PRIMARY KEY (id); UniqueParameterSchemasJobIDNameKey UniqueConstraint = "parameter_schemas_job_id_name_key" // ALTER TABLE ONLY parameter_schemas ADD CONSTRAINT parameter_schemas_job_id_name_key UNIQUE (job_id, name); UniqueParameterSchemasPkey UniqueConstraint = "parameter_schemas_pkey" // ALTER TABLE ONLY parameter_schemas ADD CONSTRAINT parameter_schemas_pkey PRIMARY KEY (id); @@ -55,8 +54,11 @@ const ( UniqueTailnetCoordinatorsPkey UniqueConstraint = "tailnet_coordinators_pkey" // ALTER TABLE ONLY tailnet_coordinators ADD CONSTRAINT tailnet_coordinators_pkey PRIMARY KEY (id); UniqueTailnetPeersPkey UniqueConstraint = "tailnet_peers_pkey" // ALTER TABLE ONLY tailnet_peers ADD CONSTRAINT tailnet_peers_pkey PRIMARY KEY (id, coordinator_id); UniqueTailnetTunnelsPkey UniqueConstraint = "tailnet_tunnels_pkey" // ALTER TABLE ONLY tailnet_tunnels ADD CONSTRAINT tailnet_tunnels_pkey PRIMARY KEY (coordinator_id, src_id, dst_id); + UniqueTelemetryItemsPkey UniqueConstraint = "telemetry_items_pkey" // ALTER TABLE ONLY telemetry_items ADD CONSTRAINT telemetry_items_pkey PRIMARY KEY (key); UniqueTemplateUsageStatsPkey UniqueConstraint = "template_usage_stats_pkey" // ALTER TABLE ONLY template_usage_stats ADD CONSTRAINT template_usage_stats_pkey PRIMARY KEY (start_time, template_id, user_id); UniqueTemplateVersionParametersTemplateVersionIDNameKey UniqueConstraint = "template_version_parameters_template_version_id_name_key" // ALTER TABLE ONLY template_version_parameters ADD CONSTRAINT template_version_parameters_template_version_id_name_key UNIQUE (template_version_id, name); + UniqueTemplateVersionPresetParametersPkey UniqueConstraint = "template_version_preset_parameters_pkey" // ALTER TABLE ONLY template_version_preset_parameters ADD CONSTRAINT template_version_preset_parameters_pkey PRIMARY KEY (id); + UniqueTemplateVersionPresetsPkey UniqueConstraint = "template_version_presets_pkey" // ALTER TABLE ONLY template_version_presets ADD CONSTRAINT template_version_presets_pkey PRIMARY KEY (id); UniqueTemplateVersionVariablesTemplateVersionIDNameKey UniqueConstraint = "template_version_variables_template_version_id_name_key" // ALTER TABLE ONLY template_version_variables ADD CONSTRAINT template_version_variables_template_version_id_name_key UNIQUE (template_version_id, name); UniqueTemplateVersionWorkspaceTagsTemplateVersionIDKeyKey UniqueConstraint = "template_version_workspace_tags_template_version_id_key_key" // ALTER TABLE ONLY template_version_workspace_tags ADD CONSTRAINT template_version_workspace_tags_template_version_id_key_key UNIQUE (template_version_id, key); UniqueTemplateVersionsPkey UniqueConstraint = "template_versions_pkey" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_pkey PRIMARY KEY (id); @@ -67,11 +69,13 @@ const ( UniqueUserStatusChangesPkey UniqueConstraint = "user_status_changes_pkey" // ALTER TABLE ONLY user_status_changes ADD CONSTRAINT user_status_changes_pkey PRIMARY KEY (id); UniqueUsersPkey UniqueConstraint = "users_pkey" // ALTER TABLE ONLY users ADD CONSTRAINT users_pkey PRIMARY KEY (id); UniqueWorkspaceAgentLogSourcesPkey UniqueConstraint = "workspace_agent_log_sources_pkey" // ALTER TABLE ONLY workspace_agent_log_sources ADD CONSTRAINT workspace_agent_log_sources_pkey PRIMARY KEY (workspace_agent_id, id); + UniqueWorkspaceAgentMemoryResourceMonitorsPkey UniqueConstraint = "workspace_agent_memory_resource_monitors_pkey" // ALTER TABLE ONLY workspace_agent_memory_resource_monitors ADD CONSTRAINT workspace_agent_memory_resource_monitors_pkey PRIMARY KEY (agent_id); UniqueWorkspaceAgentMetadataPkey UniqueConstraint = "workspace_agent_metadata_pkey" // ALTER TABLE ONLY workspace_agent_metadata ADD CONSTRAINT workspace_agent_metadata_pkey PRIMARY KEY (workspace_agent_id, key); UniqueWorkspaceAgentPortSharePkey UniqueConstraint = "workspace_agent_port_share_pkey" // ALTER TABLE ONLY workspace_agent_port_share ADD CONSTRAINT workspace_agent_port_share_pkey PRIMARY KEY (workspace_id, agent_name, port); UniqueWorkspaceAgentScriptTimingsScriptIDStartedAtKey UniqueConstraint = "workspace_agent_script_timings_script_id_started_at_key" // ALTER TABLE ONLY workspace_agent_script_timings ADD CONSTRAINT workspace_agent_script_timings_script_id_started_at_key UNIQUE (script_id, started_at); UniqueWorkspaceAgentScriptsIDKey UniqueConstraint = "workspace_agent_scripts_id_key" // ALTER TABLE ONLY workspace_agent_scripts ADD CONSTRAINT workspace_agent_scripts_id_key UNIQUE (id); UniqueWorkspaceAgentStartupLogsPkey UniqueConstraint = "workspace_agent_startup_logs_pkey" // ALTER TABLE ONLY workspace_agent_logs ADD CONSTRAINT workspace_agent_startup_logs_pkey PRIMARY KEY (id); + UniqueWorkspaceAgentVolumeResourceMonitorsPkey UniqueConstraint = "workspace_agent_volume_resource_monitors_pkey" // ALTER TABLE ONLY workspace_agent_volume_resource_monitors ADD CONSTRAINT workspace_agent_volume_resource_monitors_pkey PRIMARY KEY (agent_id, path); UniqueWorkspaceAgentsPkey UniqueConstraint = "workspace_agents_pkey" // ALTER TABLE ONLY workspace_agents ADD CONSTRAINT workspace_agents_pkey PRIMARY KEY (id); UniqueWorkspaceAppStatsPkey UniqueConstraint = "workspace_app_stats_pkey" // ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_pkey PRIMARY KEY (id); UniqueWorkspaceAppStatsUserIDAgentIDSessionIDKey UniqueConstraint = "workspace_app_stats_user_id_agent_id_session_id_key" // ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_user_id_agent_id_session_id_key UNIQUE (user_id, agent_id, session_id); @@ -89,8 +93,7 @@ const ( UniqueWorkspacesPkey UniqueConstraint = "workspaces_pkey" // ALTER TABLE ONLY workspaces ADD CONSTRAINT workspaces_pkey PRIMARY KEY (id); UniqueIndexAPIKeyName UniqueConstraint = "idx_api_key_name" // CREATE UNIQUE INDEX idx_api_key_name ON api_keys USING btree (user_id, token_name) WHERE (login_type = 'token'::login_type); UniqueIndexCustomRolesNameLower UniqueConstraint = "idx_custom_roles_name_lower" // CREATE UNIQUE INDEX idx_custom_roles_name_lower ON custom_roles USING btree (lower(name)); - UniqueIndexOrganizationName UniqueConstraint = "idx_organization_name" // CREATE UNIQUE INDEX idx_organization_name ON organizations USING btree (name); - UniqueIndexOrganizationNameLower UniqueConstraint = "idx_organization_name_lower" // CREATE UNIQUE INDEX idx_organization_name_lower ON organizations USING btree (lower(name)); + UniqueIndexOrganizationNameLower UniqueConstraint = "idx_organization_name_lower" // CREATE UNIQUE INDEX idx_organization_name_lower ON organizations USING btree (lower(name)) WHERE (deleted = false); UniqueIndexProvisionerDaemonsOrgNameOwnerKey UniqueConstraint = "idx_provisioner_daemons_org_name_owner_key" // CREATE UNIQUE INDEX idx_provisioner_daemons_org_name_owner_key ON provisioner_daemons USING btree (organization_id, name, lower(COALESCE((tags ->> 'owner'::text), ''::text))); UniqueIndexUsersEmail UniqueConstraint = "idx_users_email" // CREATE UNIQUE INDEX idx_users_email ON users USING btree (email) WHERE (deleted = false); UniqueIndexUsersUsername UniqueConstraint = "idx_users_username" // CREATE UNIQUE INDEX idx_users_username ON users USING btree (username) WHERE (deleted = false); diff --git a/coderd/entitlements/entitlements.go b/coderd/entitlements/entitlements.go index b57135e984b8c..e141a861a9045 100644 --- a/coderd/entitlements/entitlements.go +++ b/coderd/entitlements/entitlements.go @@ -30,8 +30,8 @@ func New() *Set { // These will be updated when coderd is initialized. entitlements: codersdk.Entitlements{ Features: map[codersdk.FeatureName]codersdk.Feature{}, - Warnings: nil, - Errors: nil, + Warnings: []string{}, + Errors: []string{}, HasLicense: false, Trial: false, RequireTelemetry: false, @@ -39,13 +39,21 @@ func New() *Set { }, right2Update: make(chan struct{}, 1), } + // Ensure all features are present in the entitlements. Our frontend + // expects this. + for _, featureName := range codersdk.FeatureNames { + s.entitlements.AddFeature(featureName, codersdk.Feature{ + Entitlement: codersdk.EntitlementNotEntitled, + Enabled: false, + }) + } s.right2Update <- struct{}{} // one token, serialized updates return s } // ErrLicenseRequiresTelemetry is an error returned by a fetch passed to Update to indicate that the // fetched license cannot be used because it requires telemetry. -var ErrLicenseRequiresTelemetry = xerrors.New("License requires telemetry but telemetry is disabled") +var ErrLicenseRequiresTelemetry = xerrors.New(codersdk.LicenseTelemetryRequiredErrorText) func (l *Set) Update(ctx context.Context, fetch func(context.Context) (codersdk.Entitlements, error)) error { select { diff --git a/coderd/healthcheck/workspaceproxy.go b/coderd/healthcheck/workspaceproxy.go index d9fdfd5295d6f..65a3b439553b9 100644 --- a/coderd/healthcheck/workspaceproxy.go +++ b/coderd/healthcheck/workspaceproxy.go @@ -100,9 +100,9 @@ func (r *WorkspaceProxyReport) Run(ctx context.Context, opts *WorkspaceProxyRepo for _, err := range errs { switch r.Severity { case health.SeverityWarning, health.SeverityOK: - r.Warnings = append(r.Warnings, health.Messagef(health.CodeProxyUnhealthy, err)) + r.Warnings = append(r.Warnings, health.Messagef(health.CodeProxyUnhealthy, "%s", err)) case health.SeverityError: - r.appendError(*health.Errorf(health.CodeProxyUnhealthy, err)) + r.appendError(*health.Errorf(health.CodeProxyUnhealthy, "%s", err)) } } } diff --git a/coderd/httpapi/httpapi.go b/coderd/httpapi/httpapi.go index cd55a09d51525..d5895dcbf86f0 100644 --- a/coderd/httpapi/httpapi.go +++ b/coderd/httpapi/httpapi.go @@ -151,10 +151,13 @@ func ResourceNotFound(rw http.ResponseWriter) { Write(context.Background(), rw, http.StatusNotFound, ResourceNotFoundResponse) } +var ResourceForbiddenResponse = codersdk.Response{ + Message: "Forbidden.", + Detail: "You don't have permission to view this content. If you believe this is a mistake, please contact your administrator or try signing in with different credentials.", +} + func Forbidden(rw http.ResponseWriter) { - Write(context.Background(), rw, http.StatusForbidden, codersdk.Response{ - Message: "Forbidden.", - }) + Write(context.Background(), rw, http.StatusForbidden, ResourceForbiddenResponse) } func InternalServerError(rw http.ResponseWriter, err error) { diff --git a/coderd/httpapi/httpapi_test.go b/coderd/httpapi/httpapi_test.go index 635ed2bdc1e29..eb3f23e6ca346 100644 --- a/coderd/httpapi/httpapi_test.go +++ b/coderd/httpapi/httpapi_test.go @@ -143,7 +143,7 @@ func TestWebsocketCloseMsg(t *testing.T) { t.Parallel() msg := strings.Repeat("d", 255) - trunc := httpapi.WebsocketCloseSprintf(msg) + trunc := httpapi.WebsocketCloseSprintf("%s", msg) assert.Equal(t, len(trunc), 123) }) @@ -151,7 +151,7 @@ func TestWebsocketCloseMsg(t *testing.T) { t.Parallel() msg := strings.Repeat("こんにちは", 10) - trunc := httpapi.WebsocketCloseSprintf(msg) + trunc := httpapi.WebsocketCloseSprintf("%s", msg) assert.Equal(t, len(trunc), 123) }) } diff --git a/coderd/httpapi/queryparams.go b/coderd/httpapi/queryparams.go index 15a67caa651a8..9eb5325eca53e 100644 --- a/coderd/httpapi/queryparams.go +++ b/coderd/httpapi/queryparams.go @@ -2,6 +2,7 @@ package httpapi import ( "database/sql" + "encoding/json" "errors" "fmt" "net/url" @@ -257,6 +258,23 @@ func (p *QueryParamParser) Strings(vals url.Values, def []string, queryParam str }) } +func (p *QueryParamParser) JSONStringMap(vals url.Values, def map[string]string, queryParam string) map[string]string { + v, err := parseQueryParam(p, vals, func(v string) (map[string]string, error) { + var m map[string]string + if err := json.NewDecoder(strings.NewReader(v)).Decode(&m); err != nil { + return nil, err + } + return m, nil + }, def, queryParam) + if err != nil { + p.Errors = append(p.Errors, codersdk.ValidationError{ + Field: queryParam, + Detail: fmt.Sprintf("Query param %q must be a valid JSON object: %s", queryParam, err.Error()), + }) + } + return v +} + // ValidEnum represents an enum that can be parsed and validated. type ValidEnum interface { // Add more types as needed (avoid importing large dependency trees). diff --git a/coderd/httpapi/queryparams_test.go b/coderd/httpapi/queryparams_test.go index 16cf805534b05..e95ce292404b2 100644 --- a/coderd/httpapi/queryparams_test.go +++ b/coderd/httpapi/queryparams_test.go @@ -473,6 +473,70 @@ func TestParseQueryParams(t *testing.T) { testQueryParams(t, expParams, parser, parser.UUIDs) }) + t.Run("JSONStringMap", func(t *testing.T) { + t.Parallel() + + expParams := []queryParamTestCase[map[string]string]{ + { + QueryParam: "valid_map", + Value: `{"key1": "value1", "key2": "value2"}`, + Expected: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + }, + { + QueryParam: "empty", + Value: "{}", + Default: map[string]string{}, + Expected: map[string]string{}, + }, + { + QueryParam: "no_value", + NoSet: true, + Default: map[string]string{}, + Expected: map[string]string{}, + }, + { + QueryParam: "default", + NoSet: true, + Default: map[string]string{"key": "value"}, + Expected: map[string]string{"key": "value"}, + }, + { + QueryParam: "null", + Value: "null", + Expected: map[string]string(nil), + }, + { + QueryParam: "undefined", + Value: "undefined", + Expected: map[string]string(nil), + }, + { + QueryParam: "invalid_map", + Value: `{"key1": "value1", "key2": "value2"`, // missing closing brace + Expected: map[string]string(nil), + Default: map[string]string{}, + ExpectedErrorContains: `Query param "invalid_map" must be a valid JSON object: unexpected EOF`, + }, + { + QueryParam: "incorrect_type", + Value: `{"key1": 1, "key2": true}`, + Expected: map[string]string(nil), + ExpectedErrorContains: `Query param "incorrect_type" must be a valid JSON object: json: cannot unmarshal number into Go value of type string`, + }, + { + QueryParam: "multiple_keys", + Values: []string{`{"key1": "value1"}`, `{"key2": "value2"}`}, + Expected: map[string]string(nil), + ExpectedErrorContains: `Query param "multiple_keys" provided more than once, found 2 times.`, + }, + } + parser := httpapi.NewQueryParamParser() + testQueryParams(t, expParams, parser, parser.JSONStringMap) + }) + t.Run("Required", func(t *testing.T) { t.Parallel() diff --git a/coderd/httpmw/oauth2.go b/coderd/httpmw/oauth2.go index 7afa622d97af6..49e98da685e0f 100644 --- a/coderd/httpmw/oauth2.go +++ b/coderd/httpmw/oauth2.go @@ -167,9 +167,16 @@ func ExtractOAuth2(config promoauth.OAuth2Config, client *http.Client, authURLOp oauthToken, err := config.Exchange(ctx, code) if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error exchanging Oauth code.", - Detail: err.Error(), + errorCode := http.StatusInternalServerError + detail := err.Error() + if detail == "authorization_pending" { + // In the device flow, the token may not be immediately + // available. This is expected, and the client will retry. + errorCode = http.StatusBadRequest + } + httpapi.Write(ctx, rw, errorCode, codersdk.Response{ + Message: "Failed exchanging Oauth code.", + Detail: detail, }) return } diff --git a/coderd/httpmw/organizationparam.go b/coderd/httpmw/organizationparam.go index a72b361b90d71..2eba0dcedf5b8 100644 --- a/coderd/httpmw/organizationparam.go +++ b/coderd/httpmw/organizationparam.go @@ -73,7 +73,10 @@ func ExtractOrganizationParam(db database.Store) func(http.Handler) http.Handler if err == nil { organization, dbErr = db.GetOrganizationByID(ctx, id) } else { - organization, dbErr = db.GetOrganizationByName(ctx, arg) + organization, dbErr = db.GetOrganizationByName(ctx, database.GetOrganizationByNameParams{ + Name: arg, + Deleted: false, + }) } } if httpapi.Is404Error(dbErr) { diff --git a/coderd/idpsync/group.go b/coderd/idpsync/group.go index c14b7655e7e20..4524284260359 100644 --- a/coderd/idpsync/group.go +++ b/coderd/idpsync/group.go @@ -30,7 +30,7 @@ func (AGPLIDPSync) GroupSyncEntitled() bool { return false } -func (s AGPLIDPSync) UpdateGroupSettings(ctx context.Context, orgID uuid.UUID, db database.Store, settings GroupSyncSettings) error { +func (s AGPLIDPSync) UpdateGroupSyncSettings(ctx context.Context, orgID uuid.UUID, db database.Store, settings GroupSyncSettings) error { orgResolver := s.Manager.OrganizationResolver(db, orgID) err := s.SyncSettings.Group.SetRuntimeValue(ctx, orgResolver, &settings) if err != nil { diff --git a/coderd/idpsync/idpsync.go b/coderd/idpsync/idpsync.go index e936bada73752..4da101635bd23 100644 --- a/coderd/idpsync/idpsync.go +++ b/coderd/idpsync/idpsync.go @@ -26,7 +26,7 @@ import ( type IDPSync interface { OrganizationSyncEntitled() bool OrganizationSyncSettings(ctx context.Context, db database.Store) (*OrganizationSyncSettings, error) - UpdateOrganizationSettings(ctx context.Context, db database.Store, settings OrganizationSyncSettings) error + UpdateOrganizationSyncSettings(ctx context.Context, db database.Store, settings OrganizationSyncSettings) error // OrganizationSyncEnabled returns true if all OIDC users are assigned // to organizations via org sync settings. // This is used to know when to disable manual org membership assignment. @@ -48,7 +48,7 @@ type IDPSync interface { // on the settings used by IDPSync. This entry is thread safe and can be // accessed concurrently. The settings are stored in the database. GroupSyncSettings(ctx context.Context, orgID uuid.UUID, db database.Store) (*GroupSyncSettings, error) - UpdateGroupSettings(ctx context.Context, orgID uuid.UUID, db database.Store, settings GroupSyncSettings) error + UpdateGroupSyncSettings(ctx context.Context, orgID uuid.UUID, db database.Store, settings GroupSyncSettings) error // RoleSyncEntitled returns true if the deployment is entitled to role syncing. RoleSyncEntitled() bool @@ -61,7 +61,7 @@ type IDPSync interface { // RoleSyncSettings is similar to GroupSyncSettings. See GroupSyncSettings for // rational. RoleSyncSettings(ctx context.Context, orgID uuid.UUID, db database.Store) (*RoleSyncSettings, error) - UpdateRoleSettings(ctx context.Context, orgID uuid.UUID, db database.Store, settings RoleSyncSettings) error + UpdateRoleSyncSettings(ctx context.Context, orgID uuid.UUID, db database.Store, settings RoleSyncSettings) error // ParseRoleClaims takes claims from an OIDC provider, and returns the params // for role syncing. Most of the logic happens in SyncRoles. ParseRoleClaims(ctx context.Context, mergedClaims jwt.MapClaims) (RoleParams, *HTTPError) @@ -70,6 +70,9 @@ type IDPSync interface { SyncRoles(ctx context.Context, db database.Store, user database.User, params RoleParams) error } +// AGPLIDPSync implements the IDPSync interface +var _ IDPSync = AGPLIDPSync{} + // AGPLIDPSync is the configuration for syncing user information from an external // IDP. All related code to syncing user information should be in this package. type AGPLIDPSync struct { diff --git a/coderd/idpsync/organization.go b/coderd/idpsync/organization.go index 12d79bc047776..87fd9af5e935d 100644 --- a/coderd/idpsync/organization.go +++ b/coderd/idpsync/organization.go @@ -34,7 +34,7 @@ func (AGPLIDPSync) OrganizationSyncEnabled(_ context.Context, _ database.Store) return false } -func (s AGPLIDPSync) UpdateOrganizationSettings(ctx context.Context, db database.Store, settings OrganizationSyncSettings) error { +func (s AGPLIDPSync) UpdateOrganizationSyncSettings(ctx context.Context, db database.Store, settings OrganizationSyncSettings) error { rlv := s.Manager.Resolver(db) err := s.SyncSettings.Organization.SetRuntimeValue(ctx, rlv, &settings) if err != nil { @@ -45,6 +45,8 @@ func (s AGPLIDPSync) UpdateOrganizationSettings(ctx context.Context, db database } func (s AGPLIDPSync) OrganizationSyncSettings(ctx context.Context, db database.Store) (*OrganizationSyncSettings, error) { + // If this logic is ever updated, make sure to update the corresponding + // checkIDPOrgSync in coderd/telemetry/telemetry.go. rlv := s.Manager.Resolver(db) orgSettings, err := s.SyncSettings.Organization.Resolve(ctx, rlv) if err != nil { @@ -95,7 +97,10 @@ func (s AGPLIDPSync) SyncOrganizations(ctx context.Context, tx database.Store, u return xerrors.Errorf("organization claims: %w", err) } - existingOrgs, err := tx.GetOrganizationsByUserID(ctx, user.ID) + existingOrgs, err := tx.GetOrganizationsByUserID(ctx, database.GetOrganizationsByUserIDParams{ + UserID: user.ID, + Deleted: false, + }) if err != nil { return xerrors.Errorf("failed to get user organizations: %w", err) } diff --git a/coderd/idpsync/role.go b/coderd/idpsync/role.go index cf768ee0eb05d..5cb0ac172581c 100644 --- a/coderd/idpsync/role.go +++ b/coderd/idpsync/role.go @@ -42,7 +42,7 @@ func (AGPLIDPSync) SiteRoleSyncEnabled() bool { return false } -func (s AGPLIDPSync) UpdateRoleSettings(ctx context.Context, orgID uuid.UUID, db database.Store, settings RoleSyncSettings) error { +func (s AGPLIDPSync) UpdateRoleSyncSettings(ctx context.Context, orgID uuid.UUID, db database.Store, settings RoleSyncSettings) error { orgResolver := s.Manager.OrganizationResolver(db, orgID) err := s.SyncSettings.Role.SetRuntimeValue(ctx, orgResolver, &settings) if err != nil { diff --git a/coderd/members.go b/coderd/members.go index 97950b19e9137..c89b4c9c09c1a 100644 --- a/coderd/members.go +++ b/coderd/members.go @@ -323,7 +323,7 @@ func convertOrganizationMembers(ctx context.Context, db database.Store, mems []d customRoles, err := db.CustomRoles(ctx, database.CustomRolesParams{ LookupRoles: roleLookup, ExcludeOrgRoles: false, - OrganizationID: uuid.UUID{}, + OrganizationID: uuid.Nil, }) if err != nil { // We are missing the display names, but that is not absolutely required. So just diff --git a/coderd/notifications.go b/coderd/notifications.go index 32f035a076b43..812d8cd3e450b 100644 --- a/coderd/notifications.go +++ b/coderd/notifications.go @@ -11,9 +11,12 @@ import ( "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/notifications" "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/coderd/rbac/policy" "github.com/coder/coder/v2/codersdk" ) @@ -163,6 +166,53 @@ func (api *API) notificationDispatchMethods(rw http.ResponseWriter, r *http.Requ }) } +// @Summary Send a test notification +// @ID send-a-test-notification +// @Security CoderSessionToken +// @Tags Notifications +// @Success 200 +// @Router /notifications/test [post] +func (api *API) postTestNotification(rw http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + key = httpmw.APIKey(r) + ) + + if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceDeploymentConfig) { + httpapi.Forbidden(rw) + return + } + + if _, err := api.NotificationsEnqueuer.EnqueueWithData( + //nolint:gocritic // We need to be notifier to send the notification. + dbauthz.AsNotifier(ctx), + key.UserID, + notifications.TemplateTestNotification, + map[string]string{}, + map[string]any{ + // NOTE(DanielleMaywood): + // When notifications are enqueued, they are checked to be + // unique within a single day. This means that if we attempt + // to send two test notifications to the same user on + // the same day, the enqueuer will prevent us from sending + // a second one. We are injecting a timestamp to make the + // notifications appear different enough to circumvent this + // deduplication logic. + "timestamp": api.Clock.Now(), + }, + "send-test-notification", + ); err != nil { + api.Logger.Error(ctx, "send notification", slog.Error(err)) + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Failed to send test notification", + Detail: err.Error(), + }) + return + } + + rw.WriteHeader(http.StatusNoContent) +} + // @Summary Get user notification preferences // @ID get-user-notification-preferences // @Security CoderSessionToken diff --git a/coderd/notifications/dispatch/webhook.go b/coderd/notifications/dispatch/webhook.go index 1322996db10e1..65d6ed030af98 100644 --- a/coderd/notifications/dispatch/webhook.go +++ b/coderd/notifications/dispatch/webhook.go @@ -106,7 +106,7 @@ func (w *WebhookHandler) dispatch(msgPayload types.MessagePayload, titlePlaintex return true, xerrors.Errorf("non-2xx response (%d), read body: %w", resp.StatusCode, err) } w.log.Warn(ctx, "unsuccessful delivery", slog.F("status_code", resp.StatusCode), - slog.F("response", respBody[:n]), slog.F("msg_id", msgID)) + slog.F("response", string(respBody[:n])), slog.F("msg_id", msgID)) return true, xerrors.Errorf("non-2xx response (%d)", resp.StatusCode) } diff --git a/coderd/notifications/events.go b/coderd/notifications/events.go index 754d2e5c7f745..3399da96cf28a 100644 --- a/coderd/notifications/events.go +++ b/coderd/notifications/events.go @@ -15,6 +15,8 @@ var ( TemplateWorkspaceAutoUpdated = uuid.MustParse("c34a0c09-0704-4cac-bd1c-0c0146811c2b") TemplateWorkspaceMarkedForDeletion = uuid.MustParse("51ce2fdf-c9ca-4be1-8d70-628674f9bc42") TemplateWorkspaceManualBuildFailed = uuid.MustParse("2faeee0f-26cb-4e96-821c-85ccb9f71513") + TemplateWorkspaceOutOfMemory = uuid.MustParse("a9d027b4-ac49-4fb1-9f6d-45af15f64e7a") + TemplateWorkspaceOutOfDisk = uuid.MustParse("f047f6a3-5713-40f7-85aa-0394cce9fa3a") ) // Account-related events. @@ -37,3 +39,8 @@ var ( TemplateWorkspaceBuildsFailedReport = uuid.MustParse("34a20db2-e9cc-4a93-b0e4-8569699d7a00") ) + +// Notification-related events. +var ( + TemplateTestNotification = uuid.MustParse("c425f63e-716a-4bf4-ae24-78348f706c3f") +) diff --git a/coderd/notifications/notifications_test.go b/coderd/notifications/notifications_test.go index 62fa50f453cfa..f6287993a3a91 100644 --- a/coderd/notifications/notifications_test.go +++ b/coderd/notifications/notifications_test.go @@ -388,7 +388,7 @@ func TestBackpressure(t *testing.T) { }, testutil.WaitShort, testutil.IntervalFast) } } - t.Logf("done advancing") + t.Log("done advancing") // The batch completes w.MustWait(ctx) @@ -1064,6 +1064,77 @@ func TestNotificationTemplates_Golden(t *testing.T) { }, }, }, + { + name: "TemplateWorkspaceOutOfMemory", + id: notifications.TemplateWorkspaceOutOfMemory, + payload: types.MessagePayload{ + UserName: "Bobby", + UserEmail: "bobby@coder.com", + UserUsername: "bobby", + Labels: map[string]string{ + "workspace": "bobby-workspace", + "threshold": "90%", + }, + }, + }, + { + name: "TemplateWorkspaceOutOfDisk", + id: notifications.TemplateWorkspaceOutOfDisk, + payload: types.MessagePayload{ + UserName: "Bobby", + UserEmail: "bobby@coder.com", + UserUsername: "bobby", + Labels: map[string]string{ + "workspace": "bobby-workspace", + }, + Data: map[string]any{ + "volumes": []map[string]any{ + { + "path": "/home/coder", + "threshold": "90%", + }, + }, + }, + }, + }, + { + name: "TemplateWorkspaceOutOfDisk_MultipleVolumes", + id: notifications.TemplateWorkspaceOutOfDisk, + payload: types.MessagePayload{ + UserName: "Bobby", + UserEmail: "bobby@coder.com", + UserUsername: "bobby", + Labels: map[string]string{ + "workspace": "bobby-workspace", + }, + Data: map[string]any{ + "volumes": []map[string]any{ + { + "path": "/home/coder", + "threshold": "90%", + }, + { + "path": "/dev/coder", + "threshold": "80%", + }, + { + "path": "/etc/coder", + "threshold": "95%", + }, + }, + }, + }, + }, + { + name: "TemplateTestNotification", + id: notifications.TemplateTestNotification, + payload: types.MessagePayload{ + UserName: "Bobby", + UserEmail: "bobby@coder.com", + UserUsername: "bobby", + Labels: map[string]string{}, + }, + }, } // We must have a test case for every notification_template. This is enforced below: diff --git a/coderd/notifications/testdata/rendered-templates/smtp/TemplateTestNotification.html.golden b/coderd/notifications/testdata/rendered-templates/smtp/TemplateTestNotification.html.golden new file mode 100644 index 0000000000000..c7e5641c37fa5 --- /dev/null +++ b/coderd/notifications/testdata/rendered-templates/smtp/TemplateTestNotification.html.golden @@ -0,0 +1,79 @@ +From: system@coder.com +To: bobby@coder.com +Subject: A test notification +Message-Id: 02ee4935-73be-4fa1-a290-ff9999026b13@blush-whale-48 +Date: Fri, 11 Oct 2024 09:03:06 +0000 +Content-Type: multipart/alternative; boundary=bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +MIME-Version: 1.0 + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; charset=UTF-8 + +Hi Bobby, + +This is a test notification. + + +View notification settings: http://test.com/deployment/notifications?tab=3D= +settings + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/html; charset=UTF-8 + +<!doctype html> +<html lang=3D"en"> + <head> + <meta charset=3D"UTF-8" /> + <meta name=3D"viewport" content=3D"width=3Ddevice-width, initial-scale= +=3D1.0" /> + <title>A test notification + + +
+
+ 3D"Cod= +
+

+ A test notification +

+
+

Hi Bobby,

+ +

This is a test notification.

+
+
+ =20 + + View notification settings + + =20 +
+
+

© 2024 Coder. All rights reserved - h= +ttp://test.com

+

Click here to manage your notification = +settings

+

Stop receiving emails like this

+
+
+ + + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4-- diff --git a/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfDisk.html.golden b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfDisk.html.golden new file mode 100644 index 0000000000000..f217fc0f85c97 --- /dev/null +++ b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfDisk.html.golden @@ -0,0 +1,78 @@ +From: system@coder.com +To: bobby@coder.com +Subject: Your workspace "bobby-workspace" is low on volume space +Message-Id: 02ee4935-73be-4fa1-a290-ff9999026b13@blush-whale-48 +Date: Fri, 11 Oct 2024 09:03:06 +0000 +Content-Type: multipart/alternative; boundary=bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +MIME-Version: 1.0 + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; charset=UTF-8 + +Hi Bobby, + +Volume /home/coder is over 90% full in workspace bobby-workspace. + + +View workspace: http://test.com/@bobby/bobby-workspace + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/html; charset=UTF-8 + + + + + + + Your workspace "bobby-workspace" is low on volume space + + +
+
+ 3D"Cod= +
+

+ Your workspace "bobby-workspace" is low on volume space +

+
+

Hi Bobby,

+ +

Volume /home/coder is over 90% full in wor= +kspace bobby-workspace.

+
+
+ =20 + + View workspace + + =20 +
+
+

© 2024 Coder. All rights reserved - h= +ttp://test.com

+

Click here to manage your notification = +settings

+

Stop receiving emails like this

+
+
+ + + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4-- diff --git a/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfDisk_MultipleVolumes.html.golden b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfDisk_MultipleVolumes.html.golden new file mode 100644 index 0000000000000..87e5dec07cdaf --- /dev/null +++ b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfDisk_MultipleVolumes.html.golden @@ -0,0 +1,91 @@ +From: system@coder.com +To: bobby@coder.com +Subject: Your workspace "bobby-workspace" is low on volume space +Message-Id: 02ee4935-73be-4fa1-a290-ff9999026b13@blush-whale-48 +Date: Fri, 11 Oct 2024 09:03:06 +0000 +Content-Type: multipart/alternative; boundary=bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +MIME-Version: 1.0 + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; charset=UTF-8 + +Hi Bobby, + +The following volumes are nearly full in workspace bobby-workspace + +/home/coder is over 90% full +/dev/coder is over 80% full +/etc/coder is over 95% full + + +View workspace: http://test.com/@bobby/bobby-workspace + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/html; charset=UTF-8 + + + + + + + Your workspace "bobby-workspace" is low on volume space + + +
+
+ 3D"Cod= +
+

+ Your workspace "bobby-workspace" is low on volume space +

+
+

Hi Bobby,

+ +

The following volumes are nearly full in workspace bobby-workspa= +ce

+ + +
+
+ =20 + + View workspace + + =20 +
+
+

© 2024 Coder. All rights reserved - h= +ttp://test.com

+

Click here to manage your notification = +settings

+

Stop receiving emails like this

+
+
+ + + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4-- diff --git a/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfMemory.html.golden b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfMemory.html.golden new file mode 100644 index 0000000000000..1aa27cb4cce89 --- /dev/null +++ b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfMemory.html.golden @@ -0,0 +1,79 @@ +From: system@coder.com +To: bobby@coder.com +Subject: Your workspace "bobby-workspace" is low on memory +Message-Id: 02ee4935-73be-4fa1-a290-ff9999026b13@blush-whale-48 +Date: Fri, 11 Oct 2024 09:03:06 +0000 +Content-Type: multipart/alternative; boundary=bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +MIME-Version: 1.0 + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; charset=UTF-8 + +Hi Bobby, + +Your workspace bobby-workspace has reached the memory usage threshold set a= +t 90%. + + +View workspace: http://test.com/@bobby/bobby-workspace + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/html; charset=UTF-8 + + + + + + + Your workspace "bobby-workspace" is low on memory + + +
+
+ 3D"Cod= +
+

+ Your workspace "bobby-workspace" is low on memory +

+
+

Hi Bobby,

+ +

Your workspace bobby-workspace has reached the memory u= +sage threshold set at 90%.

+
+
+ =20 + + View workspace + + =20 +
+
+

© 2024 Coder. All rights reserved - h= +ttp://test.com

+

Click here to manage your notification = +settings

+

Stop receiving emails like this

+
+
+ + + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4-- diff --git a/coderd/notifications/testdata/rendered-templates/webhook/TemplateTestNotification.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/TemplateTestNotification.json.golden new file mode 100644 index 0000000000000..a941faff134c2 --- /dev/null +++ b/coderd/notifications/testdata/rendered-templates/webhook/TemplateTestNotification.json.golden @@ -0,0 +1,25 @@ +{ + "_version": "1.1", + "msg_id": "00000000-0000-0000-0000-000000000000", + "payload": { + "_version": "1.1", + "notification_name": "Test Notification", + "notification_template_id": "00000000-0000-0000-0000-000000000000", + "user_id": "00000000-0000-0000-0000-000000000000", + "user_email": "bobby@coder.com", + "user_name": "Bobby", + "user_username": "bobby", + "actions": [ + { + "label": "View notification settings", + "url": "http://test.com/deployment/notifications?tab=settings" + } + ], + "labels": {}, + "data": null + }, + "title": "A test notification", + "title_markdown": "A test notification", + "body": "Hi Bobby,\n\nThis is a test notification.", + "body_markdown": "Hi Bobby,\n\nThis is a test notification." +} \ No newline at end of file diff --git a/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk.json.golden new file mode 100644 index 0000000000000..1bc671f52b6f9 --- /dev/null +++ b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk.json.golden @@ -0,0 +1,34 @@ +{ + "_version": "1.1", + "msg_id": "00000000-0000-0000-0000-000000000000", + "payload": { + "_version": "1.1", + "notification_name": "Workspace Out Of Disk", + "notification_template_id": "00000000-0000-0000-0000-000000000000", + "user_id": "00000000-0000-0000-0000-000000000000", + "user_email": "bobby@coder.com", + "user_name": "Bobby", + "user_username": "bobby", + "actions": [ + { + "label": "View workspace", + "url": "http://test.com/@bobby/bobby-workspace" + } + ], + "labels": { + "workspace": "bobby-workspace" + }, + "data": { + "volumes": [ + { + "path": "/home/coder", + "threshold": "90%" + } + ] + } + }, + "title": "Your workspace \"bobby-workspace\" is low on volume space", + "title_markdown": "Your workspace \"bobby-workspace\" is low on volume space", + "body": "Hi Bobby,\n\nVolume /home/coder is over 90% full in workspace bobby-workspace.", + "body_markdown": "Hi Bobby,\n\nVolume **`/home/coder`** is over 90% full in workspace **bobby-workspace**." +} \ No newline at end of file diff --git a/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk_MultipleVolumes.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk_MultipleVolumes.json.golden new file mode 100644 index 0000000000000..c876fb1754dd1 --- /dev/null +++ b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk_MultipleVolumes.json.golden @@ -0,0 +1,42 @@ +{ + "_version": "1.1", + "msg_id": "00000000-0000-0000-0000-000000000000", + "payload": { + "_version": "1.1", + "notification_name": "Workspace Out Of Disk", + "notification_template_id": "00000000-0000-0000-0000-000000000000", + "user_id": "00000000-0000-0000-0000-000000000000", + "user_email": "bobby@coder.com", + "user_name": "Bobby", + "user_username": "bobby", + "actions": [ + { + "label": "View workspace", + "url": "http://test.com/@bobby/bobby-workspace" + } + ], + "labels": { + "workspace": "bobby-workspace" + }, + "data": { + "volumes": [ + { + "path": "/home/coder", + "threshold": "90%" + }, + { + "path": "/dev/coder", + "threshold": "80%" + }, + { + "path": "/etc/coder", + "threshold": "95%" + } + ] + } + }, + "title": "Your workspace \"bobby-workspace\" is low on volume space", + "title_markdown": "Your workspace \"bobby-workspace\" is low on volume space", + "body": "Hi Bobby,\n\nThe following volumes are nearly full in workspace bobby-workspace\n\n/home/coder is over 90% full\n/dev/coder is over 80% full\n/etc/coder is over 95% full", + "body_markdown": "Hi Bobby,\n\nThe following volumes are nearly full in workspace **bobby-workspace**\n\n- **`/home/coder`** is over 90% full\n- **`/dev/coder`** is over 80% full\n- **`/etc/coder`** is over 95% full\n" +} \ No newline at end of file diff --git a/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfMemory.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfMemory.json.golden new file mode 100644 index 0000000000000..a0fce437e3c56 --- /dev/null +++ b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfMemory.json.golden @@ -0,0 +1,28 @@ +{ + "_version": "1.1", + "msg_id": "00000000-0000-0000-0000-000000000000", + "payload": { + "_version": "1.1", + "notification_name": "Workspace Out Of Memory", + "notification_template_id": "00000000-0000-0000-0000-000000000000", + "user_id": "00000000-0000-0000-0000-000000000000", + "user_email": "bobby@coder.com", + "user_name": "Bobby", + "user_username": "bobby", + "actions": [ + { + "label": "View workspace", + "url": "http://test.com/@bobby/bobby-workspace" + } + ], + "labels": { + "threshold": "90%", + "workspace": "bobby-workspace" + }, + "data": null + }, + "title": "Your workspace \"bobby-workspace\" is low on memory", + "title_markdown": "Your workspace \"bobby-workspace\" is low on memory", + "body": "Hi Bobby,\n\nYour workspace bobby-workspace has reached the memory usage threshold set at 90%.", + "body_markdown": "Hi Bobby,\n\nYour workspace **bobby-workspace** has reached the memory usage threshold set at **90%**." +} \ No newline at end of file diff --git a/coderd/notifications_test.go b/coderd/notifications_test.go index c4f0a551d4914..2e8d851522744 100644 --- a/coderd/notifications_test.go +++ b/coderd/notifications_test.go @@ -12,6 +12,7 @@ import ( "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/notifications" + "github.com/coder/coder/v2/coderd/notifications/notificationstest" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/testutil" ) @@ -317,3 +318,58 @@ func TestNotificationDispatchMethods(t *testing.T) { }) } } + +func TestNotificationTest(t *testing.T) { + t.Parallel() + + t.Run("OwnerCanSendTestNotification", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitShort) + + notifyEnq := ¬ificationstest.FakeEnqueuer{} + ownerClient := coderdtest.New(t, &coderdtest.Options{ + DeploymentValues: coderdtest.DeploymentValues(t), + NotificationsEnqueuer: notifyEnq, + }) + + // Given: A user with owner permissions. + _ = coderdtest.CreateFirstUser(t, ownerClient) + + // When: They attempt to send a test notification. + err := ownerClient.PostTestNotification(ctx) + require.NoError(t, err) + + // Then: We expect a notification to have been sent. + sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateTestNotification)) + require.Len(t, sent, 1) + }) + + t.Run("MemberCannotSendTestNotification", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitShort) + + notifyEnq := ¬ificationstest.FakeEnqueuer{} + ownerClient := coderdtest.New(t, &coderdtest.Options{ + DeploymentValues: coderdtest.DeploymentValues(t), + NotificationsEnqueuer: notifyEnq, + }) + + // Given: A user without owner permissions. + ownerUser := coderdtest.CreateFirstUser(t, ownerClient) + memberClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, ownerUser.OrganizationID) + + // When: They attempt to send a test notification. + err := memberClient.PostTestNotification(ctx) + + // Then: We expect a forbidden error with no notifications sent + var sdkError *codersdk.Error + require.Error(t, err) + require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error") + require.Equal(t, http.StatusForbidden, sdkError.StatusCode()) + + sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateTestNotification)) + require.Len(t, sent, 0) + }) +} diff --git a/coderd/oauthpki/okidcpki_test.go b/coderd/oauthpki/okidcpki_test.go index 144cb32901088..509da563a9145 100644 --- a/coderd/oauthpki/okidcpki_test.go +++ b/coderd/oauthpki/okidcpki_test.go @@ -13,6 +13,7 @@ import ( "github.com/coreos/go-oidc/v3/oidc" "github.com/golang-jwt/jwt/v4" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/oauth2" @@ -169,6 +170,7 @@ func TestAzureAKPKIWithCoderd(t *testing.T) { const email = "alice@coder.com" claims := jwt.MapClaims{ "email": email, + "sub": uuid.NewString(), } helper := oidctest.NewLoginHelper(owner, fake) user, _ := helper.Login(t, claims) diff --git a/coderd/presets.go b/coderd/presets.go new file mode 100644 index 0000000000000..1b5f646438339 --- /dev/null +++ b/coderd/presets.go @@ -0,0 +1,61 @@ +package coderd + +import ( + "net/http" + + "github.com/coder/coder/v2/coderd/httpapi" + "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/codersdk" +) + +// @Summary Get template version presets +// @ID get-template-version-presets +// @Security CoderSessionToken +// @Produce json +// @Tags Templates +// @Param templateversion path string true "Template version ID" format(uuid) +// @Success 200 {array} codersdk.Preset +// @Router /templateversions/{templateversion}/presets [get] +func (api *API) templateVersionPresets(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + templateVersion := httpmw.TemplateVersionParam(r) + + presets, err := api.Database.GetPresetsByTemplateVersionID(ctx, templateVersion.ID) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching template version presets.", + Detail: err.Error(), + }) + return + } + + presetParams, err := api.Database.GetPresetParametersByTemplateVersionID(ctx, templateVersion.ID) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching template version presets.", + Detail: err.Error(), + }) + return + } + + var res []codersdk.Preset + for _, preset := range presets { + sdkPreset := codersdk.Preset{ + ID: preset.ID, + Name: preset.Name, + } + for _, presetParam := range presetParams { + if presetParam.TemplateVersionPresetID != preset.ID { + continue + } + + sdkPreset.Parameters = append(sdkPreset.Parameters, codersdk.PresetParameter{ + Name: presetParam.Name, + Value: presetParam.Value, + }) + } + res = append(res, sdkPreset) + } + + httpapi.Write(ctx, rw, http.StatusOK, res) +} diff --git a/coderd/presets_test.go b/coderd/presets_test.go new file mode 100644 index 0000000000000..08ff7c76f24f5 --- /dev/null +++ b/coderd/presets_test.go @@ -0,0 +1,144 @@ +package coderd_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbauthz" + "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/testutil" +) + +func TestTemplateVersionPresets(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + presets []codersdk.Preset + }{ + { + name: "no presets", + presets: []codersdk.Preset{}, + }, + { + name: "single preset with parameters", + presets: []codersdk.Preset{ + { + Name: "My Preset", + Parameters: []codersdk.PresetParameter{ + { + Name: "preset_param1", + Value: "A1B2C3", + }, + { + Name: "preset_param2", + Value: "D4E5F6", + }, + }, + }, + }, + }, + { + name: "multiple presets with overlapping parameters", + presets: []codersdk.Preset{ + { + Name: "Preset 1", + Parameters: []codersdk.PresetParameter{ + { + Name: "shared_param", + Value: "value1", + }, + { + Name: "unique_param1", + Value: "unique1", + }, + }, + }, + { + Name: "Preset 2", + Parameters: []codersdk.PresetParameter{ + { + Name: "shared_param", + Value: "value2", + }, + { + Name: "unique_param2", + Value: "unique2", + }, + }, + }, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitShort) + + client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + + // nolint:gocritic // This is a test + provisionerCtx := dbauthz.AsProvisionerd(ctx) + + // Insert all presets for this test case + for _, givenPreset := range tc.presets { + dbPreset, err := db.InsertPreset(provisionerCtx, database.InsertPresetParams{ + Name: givenPreset.Name, + TemplateVersionID: version.ID, + }) + require.NoError(t, err) + + if len(givenPreset.Parameters) > 0 { + var presetParameterNames []string + var presetParameterValues []string + for _, presetParameter := range givenPreset.Parameters { + presetParameterNames = append(presetParameterNames, presetParameter.Name) + presetParameterValues = append(presetParameterValues, presetParameter.Value) + } + _, err = db.InsertPresetParameters(provisionerCtx, database.InsertPresetParametersParams{ + TemplateVersionPresetID: dbPreset.ID, + Names: presetParameterNames, + Values: presetParameterValues, + }) + require.NoError(t, err) + } + } + + userSubject, _, err := httpmw.UserRBACSubject(ctx, db, user.UserID, rbac.ScopeAll) + require.NoError(t, err) + userCtx := dbauthz.As(ctx, userSubject) + + gotPresets, err := client.TemplateVersionPresets(userCtx, version.ID) + require.NoError(t, err) + + require.Equal(t, len(tc.presets), len(gotPresets)) + + for _, expectedPreset := range tc.presets { + found := false + for _, gotPreset := range gotPresets { + if gotPreset.Name == expectedPreset.Name { + found = true + + // verify not only that we get the right number of parameters, but that we get the right parameters + // This ensures that we don't get extra parameters from other presets + require.Equal(t, len(expectedPreset.Parameters), len(gotPreset.Parameters)) + for _, expectedParam := range expectedPreset.Parameters { + require.Contains(t, gotPreset.Parameters, expectedParam) + } + break + } + } + require.True(t, found, "Expected preset %s not found in results", expectedPreset.Name) + } + }) + } +} diff --git a/coderd/provisionerdaemons.go b/coderd/provisionerdaemons.go index 30add82e3e287..332ae3b352e0a 100644 --- a/coderd/provisionerdaemons.go +++ b/coderd/provisionerdaemons.go @@ -1,6 +1,7 @@ package coderd import ( + "database/sql" "net/http" "github.com/coder/coder/v2/coderd/database" @@ -8,6 +9,8 @@ import ( "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/coderd/provisionerdserver" + "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/coderd/rbac/policy" "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/codersdk" ) @@ -18,22 +21,35 @@ import ( // @Produce json // @Tags Provisioning // @Param organization path string true "Organization ID" format(uuid) +// @Param limit query int false "Page limit" +// @Param ids query []string false "Filter results by job IDs" format(uuid) +// @Param status query codersdk.ProvisionerJobStatus false "Filter results by status" enums(pending,running,succeeded,canceling,canceled,failed) // @Param tags query object false "Provisioner tags to filter by (JSON of the form {'tag1':'value1','tag2':'value2'})" // @Success 200 {array} codersdk.ProvisionerDaemon // @Router /organizations/{organization}/provisionerdaemons [get] func (api *API) provisionerDaemons(rw http.ResponseWriter, r *http.Request) { var ( - ctx = r.Context() - org = httpmw.OrganizationParam(r) - tagParam = r.URL.Query().Get("tags") - tags = database.StringMap{} - err = tags.Scan([]byte(tagParam)) + ctx = r.Context() + org = httpmw.OrganizationParam(r) ) - if tagParam != "" && err != nil { + // This endpoint returns information about provisioner jobs. + // For now, only owners and template admins can access provisioner jobs. + if !api.Authorize(r, policy.ActionRead, rbac.ResourceProvisionerJobs.InOrg(org.ID)) { + httpapi.ResourceNotFound(rw) + return + } + + qp := r.URL.Query() + p := httpapi.NewQueryParamParser() + limit := p.PositiveInt32(qp, 50, "limit") + ids := p.UUIDs(qp, nil, "ids") + tags := p.JSONStringMap(qp, database.StringMap{}, "tags") + p.ErrorExcessParams(qp) + if len(p.Errors) > 0 { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Invalid tags query parameter", - Detail: err.Error(), + Message: "Invalid query parameters.", + Validations: p.Errors, }) return } @@ -43,6 +59,8 @@ func (api *API) provisionerDaemons(rw http.ResponseWriter, r *http.Request) { database.GetProvisionerDaemonsWithStatusByOrganizationParams{ OrganizationID: org.ID, StaleIntervalMS: provisionerdserver.StaleInterval.Milliseconds(), + Limit: sql.NullInt32{Int32: limit, Valid: limit > 0}, + IDs: ids, Tags: tags, }, ) @@ -59,14 +77,20 @@ func (api *API) provisionerDaemons(rw http.ResponseWriter, r *http.Request) { var currentJob, previousJob *codersdk.ProvisionerDaemonJob if dbDaemon.CurrentJobID.Valid { currentJob = &codersdk.ProvisionerDaemonJob{ - ID: dbDaemon.CurrentJobID.UUID, - Status: codersdk.ProvisionerJobStatus(dbDaemon.CurrentJobStatus.ProvisionerJobStatus), + ID: dbDaemon.CurrentJobID.UUID, + Status: codersdk.ProvisionerJobStatus(dbDaemon.CurrentJobStatus.ProvisionerJobStatus), + TemplateName: dbDaemon.CurrentJobTemplateName, + TemplateIcon: dbDaemon.CurrentJobTemplateIcon, + TemplateDisplayName: dbDaemon.CurrentJobTemplateDisplayName, } } if dbDaemon.PreviousJobID.Valid { previousJob = &codersdk.ProvisionerDaemonJob{ - ID: dbDaemon.PreviousJobID.UUID, - Status: codersdk.ProvisionerJobStatus(dbDaemon.PreviousJobStatus.ProvisionerJobStatus), + ID: dbDaemon.PreviousJobID.UUID, + Status: codersdk.ProvisionerJobStatus(dbDaemon.PreviousJobStatus.ProvisionerJobStatus), + TemplateName: dbDaemon.PreviousJobTemplateName, + TemplateIcon: dbDaemon.PreviousJobTemplateIcon, + TemplateDisplayName: dbDaemon.PreviousJobTemplateDisplayName, } } diff --git a/coderd/provisionerdaemons_test.go b/coderd/provisionerdaemons_test.go index 243a24add021f..d6d1138f7a912 100644 --- a/coderd/provisionerdaemons_test.go +++ b/coderd/provisionerdaemons_test.go @@ -1,27 +1,254 @@ package coderd_test import ( + "database/sql" + "encoding/json" + "strconv" "testing" + "time" + "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbgen" + "github.com/coder/coder/v2/coderd/database/dbtestutil" + "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/testutil" ) -func TestGetProvisionerDaemons(t *testing.T) { +func TestProvisionerDaemons(t *testing.T) { t.Parallel() - t.Run("OK", func(t *testing.T) { + db, ps := dbtestutil.NewDB(t, + dbtestutil.WithDumpOnFailure(), + //nolint:gocritic // Use UTC for consistent timestamp length in golden files. + dbtestutil.WithTimezone("UTC"), + ) + client, _, coderdAPI := coderdtest.NewWithAPI(t, &coderdtest.Options{ + IncludeProvisionerDaemon: false, + Database: db, + Pubsub: ps, + }) + owner := coderdtest.CreateFirstUser(t, client) + templateAdminClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.ScopedRoleOrgTemplateAdmin(owner.OrganizationID)) + memberClient, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) + + // Create initial resources with a running provisioner. + firstProvisioner := coderdtest.NewTaggedProvisionerDaemon(t, coderdAPI, "default-provisioner", map[string]string{"owner": "", "scope": "organization"}) + t.Cleanup(func() { _ = firstProvisioner.Close() }) + version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID) + + workspace := coderdtest.CreateWorkspace(t, client, template.ID) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) + + // Stop the provisioner so it doesn't grab any more jobs. + firstProvisioner.Close() + + // Create a provisioner that's working on a job. + pd1 := dbgen.ProvisionerDaemon(t, coderdAPI.Database, database.ProvisionerDaemon{ + Name: "provisioner-1", + CreatedAt: dbtime.Now().Add(1 * time.Second), + LastSeenAt: sql.NullTime{Time: coderdAPI.Clock.Now().Add(time.Hour), Valid: true}, // Stale interval can't be adjusted, keep online. + KeyID: codersdk.ProvisionerKeyUUIDBuiltIn, + Tags: database.StringMap{"owner": "", "scope": "organization", "foo": "bar"}, + }) + w1 := dbgen.Workspace(t, coderdAPI.Database, database.WorkspaceTable{ + OwnerID: member.ID, + TemplateID: template.ID, + }) + wb1ID := uuid.MustParse("00000000-0000-0000-dddd-000000000001") + job1 := dbgen.ProvisionerJob(t, db, coderdAPI.Pubsub, database.ProvisionerJob{ + WorkerID: uuid.NullUUID{UUID: pd1.ID, Valid: true}, + Input: json.RawMessage(`{"workspace_build_id":"` + wb1ID.String() + `"}`), + CreatedAt: dbtime.Now().Add(2 * time.Second), + StartedAt: sql.NullTime{Time: coderdAPI.Clock.Now(), Valid: true}, + Tags: database.StringMap{"owner": "", "scope": "organization", "foo": "bar"}, + }) + dbgen.WorkspaceBuild(t, coderdAPI.Database, database.WorkspaceBuild{ + ID: wb1ID, + JobID: job1.ID, + WorkspaceID: w1.ID, + TemplateVersionID: version.ID, + }) + + // Create a provisioner that completed a job previously and is offline. + pd2 := dbgen.ProvisionerDaemon(t, coderdAPI.Database, database.ProvisionerDaemon{ + Name: "provisioner-2", + CreatedAt: dbtime.Now().Add(2 * time.Second), + LastSeenAt: sql.NullTime{Time: coderdAPI.Clock.Now().Add(-time.Hour), Valid: true}, + KeyID: codersdk.ProvisionerKeyUUIDBuiltIn, + Tags: database.StringMap{"owner": "", "scope": "organization"}, + }) + w2 := dbgen.Workspace(t, coderdAPI.Database, database.WorkspaceTable{ + OwnerID: member.ID, + TemplateID: template.ID, + }) + wb2ID := uuid.MustParse("00000000-0000-0000-dddd-000000000002") + job2 := dbgen.ProvisionerJob(t, db, coderdAPI.Pubsub, database.ProvisionerJob{ + WorkerID: uuid.NullUUID{UUID: pd2.ID, Valid: true}, + Input: json.RawMessage(`{"workspace_build_id":"` + wb2ID.String() + `"}`), + CreatedAt: dbtime.Now().Add(3 * time.Second), + StartedAt: sql.NullTime{Time: coderdAPI.Clock.Now().Add(-2 * time.Hour), Valid: true}, + CompletedAt: sql.NullTime{Time: coderdAPI.Clock.Now().Add(-time.Hour), Valid: true}, + Tags: database.StringMap{"owner": "", "scope": "organization"}, + }) + dbgen.WorkspaceBuild(t, coderdAPI.Database, database.WorkspaceBuild{ + ID: wb2ID, + JobID: job2.ID, + WorkspaceID: w2.ID, + TemplateVersionID: version.ID, + }) + + // Create a pending job. + w3 := dbgen.Workspace(t, coderdAPI.Database, database.WorkspaceTable{ + OwnerID: member.ID, + TemplateID: template.ID, + }) + wb3ID := uuid.MustParse("00000000-0000-0000-dddd-000000000003") + job3 := dbgen.ProvisionerJob(t, db, coderdAPI.Pubsub, database.ProvisionerJob{ + Input: json.RawMessage(`{"workspace_build_id":"` + wb3ID.String() + `"}`), + CreatedAt: dbtime.Now().Add(4 * time.Second), + Tags: database.StringMap{"owner": "", "scope": "organization"}, + }) + dbgen.WorkspaceBuild(t, coderdAPI.Database, database.WorkspaceBuild{ + ID: wb3ID, + JobID: job3.ID, + WorkspaceID: w3.ID, + TemplateVersionID: version.ID, + }) + + // Create a provisioner that is idle. + pd3 := dbgen.ProvisionerDaemon(t, coderdAPI.Database, database.ProvisionerDaemon{ + Name: "provisioner-3", + CreatedAt: dbtime.Now().Add(3 * time.Second), + LastSeenAt: sql.NullTime{Time: coderdAPI.Clock.Now().Add(time.Hour), Valid: true}, + KeyID: codersdk.ProvisionerKeyUUIDBuiltIn, + Tags: database.StringMap{"owner": "", "scope": "organization"}, + }) + + // Add more provisioners than the default limit. + var userDaemons []database.ProvisionerDaemon + for i := range 50 { + userDaemons = append(userDaemons, dbgen.ProvisionerDaemon(t, coderdAPI.Database, database.ProvisionerDaemon{ + Name: "user-provisioner-" + strconv.Itoa(i), + CreatedAt: dbtime.Now().Add(3 * time.Second), + KeyID: codersdk.ProvisionerKeyUUIDUserAuth, + Tags: database.StringMap{"count": strconv.Itoa(i)}, + })) + } + + t.Run("Default limit", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) + daemons, err := templateAdminClient.OrganizationProvisionerDaemons(ctx, owner.OrganizationID, nil) + require.NoError(t, err) + require.Len(t, daemons, 50) + }) + + t.Run("IDs", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) + daemons, err := templateAdminClient.OrganizationProvisionerDaemons(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerDaemonsOptions{ + IDs: []uuid.UUID{pd1.ID, pd2.ID}, + }) + require.NoError(t, err) + require.Len(t, daemons, 2) + require.Equal(t, pd1.ID, daemons[0].ID) + require.Equal(t, pd2.ID, daemons[1].ID) + }) + + t.Run("Tags", func(t *testing.T) { t.Parallel() - client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) - owner := coderdtest.CreateFirstUser(t, client) - memberClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) + ctx := testutil.Context(t, testutil.WaitMedium) + daemons, err := templateAdminClient.OrganizationProvisionerDaemons(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerDaemonsOptions{ + Tags: map[string]string{"count": "1"}, + }) + require.NoError(t, err) + require.Len(t, daemons, 1) + require.Equal(t, userDaemons[1].ID, daemons[0].ID) + }) + t.Run("Limit", func(t *testing.T) { + t.Parallel() ctx := testutil.Context(t, testutil.WaitMedium) + daemons, err := templateAdminClient.OrganizationProvisionerDaemons(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerDaemonsOptions{ + Limit: 1, + }) + require.NoError(t, err) + require.Len(t, daemons, 1) + }) - daemons, err := memberClient.ProvisionerDaemons(ctx) + t.Run("Busy", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) + daemons, err := templateAdminClient.OrganizationProvisionerDaemons(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerDaemonsOptions{ + IDs: []uuid.UUID{pd1.ID}, + }) require.NoError(t, err) require.Len(t, daemons, 1) + // Verify status. + require.NotNil(t, daemons[0].Status) + require.Equal(t, codersdk.ProvisionerDaemonBusy, *daemons[0].Status) + require.NotNil(t, daemons[0].CurrentJob) + require.Nil(t, daemons[0].PreviousJob) + // Verify job. + require.Equal(t, job1.ID, daemons[0].CurrentJob.ID) + require.Equal(t, codersdk.ProvisionerJobRunning, daemons[0].CurrentJob.Status) + require.Equal(t, template.Name, daemons[0].CurrentJob.TemplateName) + require.Equal(t, template.DisplayName, daemons[0].CurrentJob.TemplateDisplayName) + require.Equal(t, template.Icon, daemons[0].CurrentJob.TemplateIcon) + }) + + t.Run("Offline", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) + daemons, err := templateAdminClient.OrganizationProvisionerDaemons(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerDaemonsOptions{ + IDs: []uuid.UUID{pd2.ID}, + }) + require.NoError(t, err) + require.Len(t, daemons, 1) + // Verify status. + require.NotNil(t, daemons[0].Status) + require.Equal(t, codersdk.ProvisionerDaemonOffline, *daemons[0].Status) + require.Nil(t, daemons[0].CurrentJob) + require.NotNil(t, daemons[0].PreviousJob) + // Verify job. + require.Equal(t, job2.ID, daemons[0].PreviousJob.ID) + require.Equal(t, codersdk.ProvisionerJobSucceeded, daemons[0].PreviousJob.Status) + require.Equal(t, template.Name, daemons[0].PreviousJob.TemplateName) + require.Equal(t, template.DisplayName, daemons[0].PreviousJob.TemplateDisplayName) + require.Equal(t, template.Icon, daemons[0].PreviousJob.TemplateIcon) + }) + + t.Run("Idle", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) + daemons, err := templateAdminClient.OrganizationProvisionerDaemons(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerDaemonsOptions{ + IDs: []uuid.UUID{pd3.ID}, + }) + require.NoError(t, err) + require.Len(t, daemons, 1) + // Verify status. + require.NotNil(t, daemons[0].Status) + require.Equal(t, codersdk.ProvisionerDaemonIdle, *daemons[0].Status) + require.Nil(t, daemons[0].CurrentJob) + require.Nil(t, daemons[0].PreviousJob) + }) + + // For now, this is not allowed even though the member has created a + // workspace. Once member-level permissions for jobs are supported + // by RBAC, this test should be updated. + t.Run("MemberDenied", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) + daemons, err := memberClient.OrganizationProvisionerDaemons(ctx, owner.OrganizationID, nil) + require.Error(t, err) + require.Len(t, daemons, 0) }) } diff --git a/coderd/provisionerdserver/acquirer_test.go b/coderd/provisionerdserver/acquirer_test.go index bc8fc3d6f5869..6e4d6a4ff7e03 100644 --- a/coderd/provisionerdserver/acquirer_test.go +++ b/coderd/provisionerdserver/acquirer_test.go @@ -547,8 +547,8 @@ func TestAcquirer_MatchTags(t *testing.T) { s := fmt.Sprintf("| %s | %s | %s | %s |", kvs(tt.acquireJobTags), kvs(tt.provisionerJobTags), sameOrg, acquire) lines = append(lines, s) } - t.Logf("You can paste this into docs/admin/provisioners.md") - t.Logf(strings.Join(lines, "\n")) + t.Log("You can paste this into docs/admin/provisioners.md") + t.Log(strings.Join(lines, "\n")) }) } diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index ddc3ac56170cd..f431805a350a1 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -1340,6 +1340,11 @@ func (s *server) CompleteJob(ctx context.Context, completed *proto.CompletedJob) } } + err = InsertWorkspacePresetsAndParameters(ctx, s.Logger, s.Database, jobID, input.TemplateVersionID, jobType.TemplateImport.Presets, s.timeNow()) + if err != nil { + return nil, xerrors.Errorf("insert workspace presets and parameters: %w", err) + } + var completedError sql.NullString for _, externalAuthProvider := range jobType.TemplateImport.ExternalAuthProviders { @@ -1809,6 +1814,52 @@ func InsertWorkspaceModule(ctx context.Context, db database.Store, jobID uuid.UU return nil } +func InsertWorkspacePresetsAndParameters(ctx context.Context, logger slog.Logger, db database.Store, jobID uuid.UUID, templateVersionID uuid.UUID, protoPresets []*sdkproto.Preset, t time.Time) error { + for _, preset := range protoPresets { + logger.Info(ctx, "inserting template import job preset", + slog.F("job_id", jobID.String()), + slog.F("preset_name", preset.Name), + ) + if err := InsertWorkspacePresetAndParameters(ctx, db, templateVersionID, preset, t); err != nil { + return xerrors.Errorf("insert workspace preset: %w", err) + } + } + return nil +} + +func InsertWorkspacePresetAndParameters(ctx context.Context, db database.Store, templateVersionID uuid.UUID, protoPreset *sdkproto.Preset, t time.Time) error { + err := db.InTx(func(tx database.Store) error { + dbPreset, err := tx.InsertPreset(ctx, database.InsertPresetParams{ + TemplateVersionID: templateVersionID, + Name: protoPreset.Name, + CreatedAt: t, + }) + if err != nil { + return xerrors.Errorf("insert preset: %w", err) + } + + var presetParameterNames []string + var presetParameterValues []string + for _, parameter := range protoPreset.Parameters { + presetParameterNames = append(presetParameterNames, parameter.Name) + presetParameterValues = append(presetParameterValues, parameter.Value) + } + _, err = tx.InsertPresetParameters(ctx, database.InsertPresetParametersParams{ + TemplateVersionPresetID: dbPreset.ID, + Names: presetParameterNames, + Values: presetParameterValues, + }) + if err != nil { + return xerrors.Errorf("insert preset parameters: %w", err) + } + return nil + }, nil) + if err != nil { + return xerrors.Errorf("insert preset and parameters: %w", err) + } + return nil +} + func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.UUID, transition database.WorkspaceTransition, protoResource *sdkproto.Resource, snapshot *telemetry.Snapshot) error { resource, err := db.InsertWorkspaceResource(ctx, database.InsertWorkspaceResourceParams{ ID: uuid.New(), @@ -1840,10 +1891,25 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. appSlugs = make(map[string]struct{}) ) for _, prAgent := range protoResource.Agents { - if _, ok := agentNames[prAgent.Name]; ok { + // Similar logic is duplicated in terraform/resources.go. + if prAgent.Name == "" { + return xerrors.Errorf("agent name cannot be empty") + } + // In 2025-02 we removed support for underscores in agent names. To + // provide a nicer error message, we check the regex first and check + // for underscores if it fails. + if !provisioner.AgentNameRegex.MatchString(prAgent.Name) { + if strings.Contains(prAgent.Name, "_") { + return xerrors.Errorf("agent name %q contains underscores which are no longer supported, please use hyphens instead (regex: %q)", prAgent.Name, provisioner.AgentNameRegex.String()) + } + return xerrors.Errorf("agent name %q does not match regex %q", prAgent.Name, provisioner.AgentNameRegex.String()) + } + // Agent names must be case-insensitive-unique, to be unambiguous in + // `coder_app`s and CoderVPN DNS names. + if _, ok := agentNames[strings.ToLower(prAgent.Name)]; ok { return xerrors.Errorf("duplicate agent name %q", prAgent.Name) } - agentNames[prAgent.Name] = struct{}{} + agentNames[strings.ToLower(prAgent.Name)] = struct{}{} var instanceID sql.NullString if prAgent.GetInstanceId() != "" { @@ -1927,6 +1993,38 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. } } + if prAgent.ResourcesMonitoring != nil { + if prAgent.ResourcesMonitoring.Memory != nil { + _, err = db.InsertMemoryResourceMonitor(ctx, database.InsertMemoryResourceMonitorParams{ + AgentID: agentID, + Enabled: prAgent.ResourcesMonitoring.Memory.Enabled, + Threshold: prAgent.ResourcesMonitoring.Memory.Threshold, + State: database.WorkspaceAgentMonitorStateOK, + CreatedAt: dbtime.Now(), + UpdatedAt: dbtime.Now(), + DebouncedUntil: time.Time{}, + }) + if err != nil { + return xerrors.Errorf("failed to insert agent memory resource monitor into db: %w", err) + } + } + for _, volume := range prAgent.ResourcesMonitoring.Volumes { + _, err = db.InsertVolumeResourceMonitor(ctx, database.InsertVolumeResourceMonitorParams{ + AgentID: agentID, + Path: volume.Path, + Enabled: volume.Enabled, + Threshold: volume.Threshold, + State: database.WorkspaceAgentMonitorStateOK, + CreatedAt: dbtime.Now(), + UpdatedAt: dbtime.Now(), + DebouncedUntil: time.Time{}, + }) + if err != nil { + return xerrors.Errorf("failed to insert agent volume resource monitor into db: %w", err) + } + } + } + logSourceIDs := make([]uuid.UUID, 0, len(prAgent.Scripts)) logSourceDisplayNames := make([]string, 0, len(prAgent.Scripts)) logSourceIcons := make([]string, 0, len(prAgent.Scripts)) @@ -1985,10 +2083,13 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. } for _, app := range prAgent.Apps { + // Similar logic is duplicated in terraform/resources.go. slug := app.Slug if slug == "" { return xerrors.Errorf("app must have a slug or name set") } + // Contrary to agent names above, app slugs were never permitted to + // contain uppercase letters or underscores. if !provisioner.AppSlugRegex.MatchString(slug) { return xerrors.Errorf("app slug %q does not match regex %q", slug, provisioner.AppSlugRegex.String()) } diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index 325e639947f86..cc73089e82b63 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -30,6 +30,7 @@ import ( "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbgen" "github.com/coder/coder/v2/coderd/database/dbmem" + "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/database/pubsub" "github.com/coder/coder/v2/coderd/externalauth" @@ -101,7 +102,7 @@ func TestHeartbeat(t *testing.T) { ctx := testutil.Context(t, testutil.WaitShort) heartbeatChan := make(chan struct{}) heartbeatFn := func(hbCtx context.Context) error { - t.Logf("heartbeat") + t.Log("heartbeat") select { case <-hbCtx.Done(): return hbCtx.Err() @@ -1708,6 +1709,155 @@ func TestCompleteJob(t *testing.T) { }) } +func TestInsertWorkspacePresetsAndParameters(t *testing.T) { + t.Parallel() + + type testCase struct { + name string + givenPresets []*sdkproto.Preset + } + + testCases := []testCase{ + { + name: "no presets", + }, + { + name: "one preset with no parameters", + givenPresets: []*sdkproto.Preset{ + { + Name: "preset1", + }, + }, + }, + { + name: "one preset with multiple parameters", + givenPresets: []*sdkproto.Preset{ + { + Name: "preset1", + Parameters: []*sdkproto.PresetParameter{ + { + Name: "param1", + Value: "value1", + }, + { + Name: "param2", + Value: "value2", + }, + }, + }, + }, + }, + { + name: "multiple presets with parameters", + givenPresets: []*sdkproto.Preset{ + { + Name: "preset1", + Parameters: []*sdkproto.PresetParameter{ + { + Name: "param1", + Value: "value1", + }, + { + Name: "param2", + Value: "value2", + }, + }, + }, + { + Name: "preset2", + Parameters: []*sdkproto.PresetParameter{ + { + Name: "param3", + Value: "value3", + }, + { + Name: "param4", + Value: "value4", + }, + }, + }, + }, + }, + } + + for _, c := range testCases { + c := c + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitLong) + logger := testutil.Logger(t) + db, ps := dbtestutil.NewDB(t) + org := dbgen.Organization(t, db, database.Organization{}) + user := dbgen.User(t, db, database.User{}) + job := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{ + Type: database.ProvisionerJobTypeWorkspaceBuild, + OrganizationID: org.ID, + }) + templateVersion := dbgen.TemplateVersion(t, db, database.TemplateVersion{ + JobID: job.ID, + OrganizationID: org.ID, + CreatedBy: user.ID, + }) + + err := provisionerdserver.InsertWorkspacePresetsAndParameters( + ctx, + logger, + db, + job.ID, + templateVersion.ID, + c.givenPresets, + time.Now(), + ) + require.NoError(t, err) + + gotPresets, err := db.GetPresetsByTemplateVersionID(ctx, templateVersion.ID) + require.NoError(t, err) + require.Len(t, gotPresets, len(c.givenPresets)) + + for _, givenPreset := range c.givenPresets { + foundMatch := false + for _, gotPreset := range gotPresets { + if givenPreset.Name == gotPreset.Name { + foundMatch = true + break + } + } + require.True(t, foundMatch, "preset %s not found in parameters", givenPreset.Name) + } + + gotPresetParameters, err := db.GetPresetParametersByTemplateVersionID(ctx, templateVersion.ID) + require.NoError(t, err) + + for _, givenPreset := range c.givenPresets { + for _, givenParameter := range givenPreset.Parameters { + foundMatch := false + for _, gotParameter := range gotPresetParameters { + nameMatches := givenParameter.Name == gotParameter.Name + valueMatches := givenParameter.Value == gotParameter.Value + + // ensure that preset parameters are matched to the correct preset: + var gotPreset database.TemplateVersionPreset + for _, preset := range gotPresets { + if preset.ID == gotParameter.TemplateVersionPresetID { + gotPreset = preset + break + } + } + presetMatches := gotPreset.Name == givenPreset.Name + + if nameMatches && valueMatches && presetMatches { + foundMatch = true + break + } + } + require.True(t, foundMatch, "preset parameter %s not found in presets", givenParameter.Name) + } + } + }) + } +} + func TestInsertWorkspaceResource(t *testing.T) { t.Parallel() ctx := context.Background() @@ -1733,6 +1883,7 @@ func TestInsertWorkspaceResource(t *testing.T) { Name: "something", Type: "aws_instance", Agents: []*sdkproto.Agent{{ + Name: "dev", Auth: &sdkproto.Agent_Token{ Token: "bananas", }, @@ -1746,6 +1897,7 @@ func TestInsertWorkspaceResource(t *testing.T) { Name: "something", Type: "aws_instance", Agents: []*sdkproto.Agent{{ + Name: "dev", Apps: []*sdkproto.App{{ Slug: "a", }, { @@ -1753,7 +1905,116 @@ func TestInsertWorkspaceResource(t *testing.T) { }}, }}, }) - require.ErrorContains(t, err, "duplicate app slug") + require.ErrorContains(t, err, `duplicate app slug, must be unique per template: "a"`) + err = insert(dbmem.New(), uuid.New(), &sdkproto.Resource{ + Name: "something", + Type: "aws_instance", + Agents: []*sdkproto.Agent{{ + Name: "dev1", + Apps: []*sdkproto.App{{ + Slug: "a", + }}, + }, { + Name: "dev2", + Apps: []*sdkproto.App{{ + Slug: "a", + }}, + }}, + }) + require.ErrorContains(t, err, `duplicate app slug, must be unique per template: "a"`) + }) + t.Run("AppSlugInvalid", func(t *testing.T) { + t.Parallel() + db := dbmem.New() + job := uuid.New() + err := insert(db, job, &sdkproto.Resource{ + Name: "something", + Type: "aws_instance", + Agents: []*sdkproto.Agent{{ + Name: "dev", + Apps: []*sdkproto.App{{ + Slug: "dev_1", + }}, + }}, + }) + require.ErrorContains(t, err, `app slug "dev_1" does not match regex`) + err = insert(db, job, &sdkproto.Resource{ + Name: "something", + Type: "aws_instance", + Agents: []*sdkproto.Agent{{ + Name: "dev", + Apps: []*sdkproto.App{{ + Slug: "dev--1", + }}, + }}, + }) + require.ErrorContains(t, err, `app slug "dev--1" does not match regex`) + err = insert(db, job, &sdkproto.Resource{ + Name: "something", + Type: "aws_instance", + Agents: []*sdkproto.Agent{{ + Name: "dev", + Apps: []*sdkproto.App{{ + Slug: "Dev", + }}, + }}, + }) + require.ErrorContains(t, err, `app slug "Dev" does not match regex`) + }) + t.Run("DuplicateAgentNames", func(t *testing.T) { + t.Parallel() + db := dbmem.New() + job := uuid.New() + // case-insensitive-unique + err := insert(db, job, &sdkproto.Resource{ + Name: "something", + Type: "aws_instance", + Agents: []*sdkproto.Agent{{ + Name: "dev", + }, { + Name: "Dev", + }}, + }) + require.ErrorContains(t, err, "duplicate agent name") + err = insert(db, job, &sdkproto.Resource{ + Name: "something", + Type: "aws_instance", + Agents: []*sdkproto.Agent{{ + Name: "dev", + }, { + Name: "dev", + }}, + }) + require.ErrorContains(t, err, "duplicate agent name") + }) + t.Run("AgentNameInvalid", func(t *testing.T) { + t.Parallel() + db := dbmem.New() + job := uuid.New() + err := insert(db, job, &sdkproto.Resource{ + Name: "something", + Type: "aws_instance", + Agents: []*sdkproto.Agent{{ + Name: "Dev", + }}, + }) + require.NoError(t, err) // uppercase is still allowed + err = insert(db, job, &sdkproto.Resource{ + Name: "something", + Type: "aws_instance", + Agents: []*sdkproto.Agent{{ + Name: "dev_1", + }}, + }) + require.ErrorContains(t, err, `agent name "dev_1" contains underscores`) // custom error for underscores + err = insert(db, job, &sdkproto.Resource{ + Name: "something", + Type: "aws_instance", + Agents: []*sdkproto.Agent{{ + Name: "dev--1", + }}, + }) + require.ErrorContains(t, err, `agent name "dev--1" does not match regex`) }) t.Run("Success", func(t *testing.T) { t.Parallel() @@ -1831,6 +2092,7 @@ func TestInsertWorkspaceResource(t *testing.T) { Name: "something", Type: "aws_instance", Agents: []*sdkproto.Agent{{ + Name: "dev", DisplayApps: &sdkproto.DisplayApps{ Vscode: true, VscodeInsiders: true, @@ -1859,6 +2121,7 @@ func TestInsertWorkspaceResource(t *testing.T) { Name: "something", Type: "aws_instance", Agents: []*sdkproto.Agent{{ + Name: "dev", DisplayApps: &sdkproto.DisplayApps{}, }}, }) @@ -1874,6 +2137,58 @@ func TestInsertWorkspaceResource(t *testing.T) { // that all apps are disabled. require.Equal(t, []database.DisplayApp{}, agent.DisplayApps) }) + + t.Run("ResourcesMonitoring", func(t *testing.T) { + t.Parallel() + db := dbmem.New() + job := uuid.New() + err := insert(db, job, &sdkproto.Resource{ + Name: "something", + Type: "aws_instance", + Agents: []*sdkproto.Agent{{ + Name: "dev", + DisplayApps: &sdkproto.DisplayApps{}, + ResourcesMonitoring: &sdkproto.ResourcesMonitoring{ + Memory: &sdkproto.MemoryResourceMonitor{ + Enabled: true, + Threshold: 80, + }, + Volumes: []*sdkproto.VolumeResourceMonitor{ + { + Path: "/volume1", + Enabled: true, + Threshold: 90, + }, + { + Path: "/volume2", + Enabled: true, + Threshold: 50, + }, + }, + }, + }}, + }) + require.NoError(t, err) + resources, err := db.GetWorkspaceResourcesByJobID(ctx, job) + require.NoError(t, err) + require.Len(t, resources, 1) + agents, err := db.GetWorkspaceAgentsByResourceIDs(ctx, []uuid.UUID{resources[0].ID}) + require.NoError(t, err) + require.Len(t, agents, 1) + + agent := agents[0] + memMonitor, err := db.FetchMemoryResourceMonitorsByAgentID(ctx, agent.ID) + require.NoError(t, err) + volMonitors, err := db.FetchVolumesResourceMonitorsByAgentID(ctx, agent.ID) + require.NoError(t, err) + + require.Equal(t, int32(80), memMonitor.Threshold) + require.Len(t, volMonitors, 2) + require.Equal(t, int32(90), volMonitors[0].Threshold) + require.Equal(t, "/volume1", volMonitors[0].Path) + require.Equal(t, int32(50), volMonitors[1].Threshold) + require.Equal(t, "/volume2", volMonitors[1].Path) + }) } func TestNotifications(t *testing.T) { @@ -2272,7 +2587,7 @@ func setup(t *testing.T, ignoreLogErrors bool, ov *overrides) (proto.DRPCProvisi Version: buildinfo.Version(), APIVersion: proto.CurrentVersion.String(), OrganizationID: defOrg.ID, - KeyID: uuid.MustParse(codersdk.ProvisionerKeyIDBuiltIn), + KeyID: codersdk.ProvisionerKeyUUIDBuiltIn, }) require.NoError(t, err) diff --git a/coderd/provisionerjobs.go b/coderd/provisionerjobs.go index 591c60855a65e..47963798f4d32 100644 --- a/coderd/provisionerjobs.go +++ b/coderd/provisionerjobs.go @@ -72,7 +72,9 @@ func (api *API) provisionerJob(rw http.ResponseWriter, r *http.Request) { // @Tags Organizations // @Param organization path string true "Organization ID" format(uuid) // @Param limit query int false "Page limit" +// @Param ids query []string false "Filter results by job IDs" format(uuid) // @Param status query codersdk.ProvisionerJobStatus false "Filter results by status" enums(pending,running,succeeded,canceling,canceled,failed) +// @Param tags query object false "Provisioner tags to filter by (JSON of the form {'tag1':'value1','tag2':'value2'})" // @Success 200 {array} codersdk.ProvisionerJob // @Router /organizations/{organization}/provisionerjobs [get] func (api *API) provisionerJobs(rw http.ResponseWriter, r *http.Request) { @@ -101,8 +103,12 @@ func (api *API) handleAuthAndFetchProvisionerJobs(rw http.ResponseWriter, r *htt qp := r.URL.Query() p := httpapi.NewQueryParamParser() - limit := p.PositiveInt32(qp, 0, "limit") + limit := p.PositiveInt32(qp, 50, "limit") status := p.Strings(qp, nil, "status") + if ids == nil { + ids = p.UUIDs(qp, nil, "ids") + } + tags := p.JSONStringMap(qp, database.StringMap{}, "tags") p.ErrorExcessParams(qp) if len(p.Errors) > 0 { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ @@ -113,10 +119,11 @@ func (api *API) handleAuthAndFetchProvisionerJobs(rw http.ResponseWriter, r *htt } jobs, err := api.Database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner(ctx, database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams{ - OrganizationID: uuid.NullUUID{UUID: org.ID, Valid: true}, + OrganizationID: org.ID, Status: slice.StringEnums[database.ProvisionerJobStatus](status), Limit: sql.NullInt32{Int32: limit, Valid: limit > 0}, IDs: ids, + Tags: tags, }) if err != nil { if httpapi.Is404Error(err) { @@ -388,6 +395,17 @@ func convertProvisionerJobWithQueuePosition(pj database.GetProvisionerJobsByOrga QueueSize: pj.QueueSize, }) job.AvailableWorkers = pj.AvailableWorkers + job.Metadata = codersdk.ProvisionerJobMetadata{ + TemplateVersionName: pj.TemplateVersionName, + TemplateID: pj.TemplateID.UUID, + TemplateName: pj.TemplateName, + TemplateDisplayName: pj.TemplateDisplayName, + TemplateIcon: pj.TemplateIcon, + WorkspaceName: pj.WorkspaceName, + } + if pj.WorkspaceID.Valid { + job.Metadata.WorkspaceID = &pj.WorkspaceID.UUID + } return job } diff --git a/coderd/provisionerjobs_test.go b/coderd/provisionerjobs_test.go index a8fd4f2a968f2..6ec8959102fa5 100644 --- a/coderd/provisionerjobs_test.go +++ b/coderd/provisionerjobs_test.go @@ -4,10 +4,12 @@ import ( "context" "database/sql" "encoding/json" + "strconv" "testing" "time" "github.com/google/uuid" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/coder/coder/v2/coderd/coderdtest" @@ -63,15 +65,57 @@ func TestProvisionerJobs(t *testing.T) { TemplateVersionID: version.ID, }) + // Add more jobs than the default limit. + for i := range 60 { + dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{ + OrganizationID: owner.OrganizationID, + Tags: database.StringMap{"count": strconv.Itoa(i)}, + }) + } + t.Run("Single", func(t *testing.T) { t.Parallel() - t.Run("OK", func(t *testing.T) { + t.Run("Workspace", func(t *testing.T) { t.Parallel() - ctx := testutil.Context(t, testutil.WaitMedium) - // Note this calls the single job endpoint. - job2, err := templateAdminClient.OrganizationProvisionerJob(ctx, owner.OrganizationID, job.ID) - require.NoError(t, err) - require.Equal(t, job.ID, job2.ID) + t.Run("OK", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) + // Note this calls the single job endpoint. + job2, err := templateAdminClient.OrganizationProvisionerJob(ctx, owner.OrganizationID, job.ID) + require.NoError(t, err) + require.Equal(t, job.ID, job2.ID) + + // Verify that job metadata is correct. + assert.Equal(t, job2.Metadata, codersdk.ProvisionerJobMetadata{ + TemplateVersionName: version.Name, + TemplateID: template.ID, + TemplateName: template.Name, + TemplateDisplayName: template.DisplayName, + TemplateIcon: template.Icon, + WorkspaceID: &w.ID, + WorkspaceName: w.Name, + }) + }) + }) + t.Run("Template Import", func(t *testing.T) { + t.Parallel() + t.Run("OK", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) + // Note this calls the single job endpoint. + job2, err := templateAdminClient.OrganizationProvisionerJob(ctx, owner.OrganizationID, version.Job.ID) + require.NoError(t, err) + require.Equal(t, version.Job.ID, job2.ID) + + // Verify that job metadata is correct. + assert.Equal(t, job2.Metadata, codersdk.ProvisionerJobMetadata{ + TemplateVersionName: version.Name, + TemplateID: template.ID, + TemplateName: template.Name, + TemplateDisplayName: template.DisplayName, + TemplateIcon: template.Icon, + }) + }) }) t.Run("Missing", func(t *testing.T) { t.Parallel() @@ -82,12 +126,22 @@ func TestProvisionerJobs(t *testing.T) { }) }) - t.Run("All", func(t *testing.T) { + t.Run("Default limit", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitMedium) jobs, err := templateAdminClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, nil) require.NoError(t, err) - require.Len(t, jobs, 3) + require.Len(t, jobs, 50) + }) + + t.Run("IDs", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) + jobs, err := templateAdminClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerJobsOptions{ + IDs: []uuid.UUID{workspace.LatestBuild.Job.ID, version.Job.ID}, + }) + require.NoError(t, err) + require.Len(t, jobs, 2) }) t.Run("Status", func(t *testing.T) { @@ -100,6 +154,16 @@ func TestProvisionerJobs(t *testing.T) { require.Len(t, jobs, 1) }) + t.Run("Tags", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) + jobs, err := templateAdminClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerJobsOptions{ + Tags: map[string]string{"count": "1"}, + }) + require.NoError(t, err) + require.Len(t, jobs, 1) + }) + t.Run("Limit", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitMedium) diff --git a/coderd/rbac/object_gen.go b/coderd/rbac/object_gen.go index 42e9e16c50279..86faa5f9456dc 100644 --- a/coderd/rbac/object_gen.go +++ b/coderd/rbac/object_gen.go @@ -27,22 +27,21 @@ var ( // ResourceAssignOrgRole // Valid Actions - // - "ActionAssign" :: ability to assign org scoped roles - // - "ActionCreate" :: ability to create/delete custom roles within an organization - // - "ActionDelete" :: ability to delete org scoped roles - // - "ActionRead" :: view what roles are assignable - // - "ActionUpdate" :: ability to edit custom roles within an organization + // - "ActionAssign" :: assign org scoped roles + // - "ActionCreate" :: create/delete custom roles within an organization + // - "ActionDelete" :: delete roles within an organization + // - "ActionRead" :: view what roles are assignable within an organization + // - "ActionUnassign" :: unassign org scoped roles + // - "ActionUpdate" :: edit custom roles within an organization ResourceAssignOrgRole = Object{ Type: "assign_org_role", } // ResourceAssignRole // Valid Actions - // - "ActionAssign" :: ability to assign roles - // - "ActionCreate" :: ability to create/delete/edit custom roles - // - "ActionDelete" :: ability to unassign roles + // - "ActionAssign" :: assign user roles // - "ActionRead" :: view what roles are assignable - // - "ActionUpdate" :: ability to edit custom roles + // - "ActionUnassign" :: unassign user roles ResourceAssignRole = Object{ Type: "assign_role", } @@ -206,8 +205,8 @@ var ( // ResourceProvisionerDaemon // Valid Actions - // - "ActionCreate" :: create a provisioner daemon - // - "ActionDelete" :: delete a provisioner daemon + // - "ActionCreate" :: create a provisioner daemon/key + // - "ActionDelete" :: delete a provisioner daemon/key // - "ActionRead" :: read provisioner daemon // - "ActionUpdate" :: update a provisioner daemon ResourceProvisionerDaemon = Object{ @@ -221,15 +220,6 @@ var ( Type: "provisioner_jobs", } - // ResourceProvisionerKeys - // Valid Actions - // - "ActionCreate" :: create a provisioner key - // - "ActionDelete" :: delete a provisioner key - // - "ActionRead" :: read provisioner keys - ResourceProvisionerKeys = Object{ - Type: "provisioner_keys", - } - // ResourceReplicas // Valid Actions // - "ActionRead" :: read replicas @@ -295,6 +285,15 @@ var ( Type: "workspace", } + // ResourceWorkspaceAgentResourceMonitor + // Valid Actions + // - "ActionCreate" :: create workspace agent resource monitor + // - "ActionRead" :: read workspace agent resource monitor + // - "ActionUpdate" :: update workspace agent resource monitor + ResourceWorkspaceAgentResourceMonitor = Object{ + Type: "workspace_agent_resource_monitor", + } + // ResourceWorkspaceDormant // Valid Actions // - "ActionApplicationConnect" :: connect to workspace apps via browser @@ -346,13 +345,13 @@ func AllResources() []Objecter { ResourceOrganizationMember, ResourceProvisionerDaemon, ResourceProvisionerJobs, - ResourceProvisionerKeys, ResourceReplicas, ResourceSystem, ResourceTailnetCoordinator, ResourceTemplate, ResourceUser, ResourceWorkspace, + ResourceWorkspaceAgentResourceMonitor, ResourceWorkspaceDormant, ResourceWorkspaceProxy, } @@ -367,6 +366,7 @@ func AllActions() []policy.Action { policy.ActionRead, policy.ActionReadPersonal, policy.ActionSSH, + policy.ActionUnassign, policy.ActionUpdate, policy.ActionUpdatePersonal, policy.ActionUse, diff --git a/coderd/rbac/policy/policy.go b/coderd/rbac/policy/policy.go index e57c2eaa234f7..0988401e3849c 100644 --- a/coderd/rbac/policy/policy.go +++ b/coderd/rbac/policy/policy.go @@ -19,7 +19,8 @@ const ( ActionWorkspaceStart Action = "start" ActionWorkspaceStop Action = "stop" - ActionAssign Action = "assign" + ActionAssign Action = "assign" + ActionUnassign Action = "unassign" ActionReadPersonal Action = "read_personal" ActionUpdatePersonal Action = "update_personal" @@ -162,11 +163,11 @@ var RBACPermissions = map[string]PermissionDefinition{ }, "provisioner_daemon": { Actions: map[Action]ActionDefinition{ - ActionCreate: actDef("create a provisioner daemon"), + ActionCreate: actDef("create a provisioner daemon/key"), // TODO: Move to use? ActionRead: actDef("read provisioner daemon"), ActionUpdate: actDef("update a provisioner daemon"), - ActionDelete: actDef("delete a provisioner daemon"), + ActionDelete: actDef("delete a provisioner daemon/key"), }, }, "provisioner_jobs": { @@ -174,13 +175,6 @@ var RBACPermissions = map[string]PermissionDefinition{ ActionRead: actDef("read provisioner jobs"), }, }, - "provisioner_keys": { - Actions: map[Action]ActionDefinition{ - ActionCreate: actDef("create a provisioner key"), - ActionRead: actDef("read provisioner keys"), - ActionDelete: actDef("delete a provisioner key"), - }, - }, "organization": { Actions: map[Action]ActionDefinition{ ActionCreate: actDef("create an organization"), @@ -228,20 +222,19 @@ var RBACPermissions = map[string]PermissionDefinition{ }, "assign_role": { Actions: map[Action]ActionDefinition{ - ActionAssign: actDef("ability to assign roles"), - ActionRead: actDef("view what roles are assignable"), - ActionDelete: actDef("ability to unassign roles"), - ActionCreate: actDef("ability to create/delete/edit custom roles"), - ActionUpdate: actDef("ability to edit custom roles"), + ActionAssign: actDef("assign user roles"), + ActionUnassign: actDef("unassign user roles"), + ActionRead: actDef("view what roles are assignable"), }, }, "assign_org_role": { Actions: map[Action]ActionDefinition{ - ActionAssign: actDef("ability to assign org scoped roles"), - ActionRead: actDef("view what roles are assignable"), - ActionDelete: actDef("ability to delete org scoped roles"), - ActionCreate: actDef("ability to create/delete custom roles within an organization"), - ActionUpdate: actDef("ability to edit custom roles within an organization"), + ActionAssign: actDef("assign org scoped roles"), + ActionUnassign: actDef("unassign org scoped roles"), + ActionCreate: actDef("create/delete custom roles within an organization"), + ActionRead: actDef("view what roles are assignable within an organization"), + ActionUpdate: actDef("edit custom roles within an organization"), + ActionDelete: actDef("delete roles within an organization"), }, }, "oauth2_app": { @@ -302,4 +295,11 @@ var RBACPermissions = map[string]PermissionDefinition{ ActionUpdate: actDef("update IdP sync settings"), }, }, + "workspace_agent_resource_monitor": { + Actions: map[Action]ActionDefinition{ + ActionRead: actDef("read workspace agent resource monitor"), + ActionCreate: actDef("create workspace agent resource monitor"), + ActionUpdate: actDef("update workspace agent resource monitor"), + }, + }, } diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go index 7fb141e557e96..6b99cb4e871a2 100644 --- a/coderd/rbac/roles.go +++ b/coderd/rbac/roles.go @@ -27,11 +27,12 @@ const ( customSiteRole string = "custom-site-role" customOrganizationRole string = "custom-organization-role" - orgAdmin string = "organization-admin" - orgMember string = "organization-member" - orgAuditor string = "organization-auditor" - orgUserAdmin string = "organization-user-admin" - orgTemplateAdmin string = "organization-template-admin" + orgAdmin string = "organization-admin" + orgMember string = "organization-member" + orgAuditor string = "organization-auditor" + orgUserAdmin string = "organization-user-admin" + orgTemplateAdmin string = "organization-template-admin" + orgWorkspaceCreationBan string = "organization-workspace-creation-ban" ) func init() { @@ -159,6 +160,10 @@ func RoleOrgTemplateAdmin() string { return orgTemplateAdmin } +func RoleOrgWorkspaceCreationBan() string { + return orgWorkspaceCreationBan +} + // ScopedRoleOrgAdmin is the org role with the organization ID func ScopedRoleOrgAdmin(organizationID uuid.UUID) RoleIdentifier { return RoleIdentifier{Name: RoleOrgAdmin(), OrganizationID: organizationID} @@ -181,6 +186,10 @@ func ScopedRoleOrgTemplateAdmin(organizationID uuid.UUID) RoleIdentifier { return RoleIdentifier{Name: RoleOrgTemplateAdmin(), OrganizationID: organizationID} } +func ScopedRoleOrgWorkspaceCreationBan(organizationID uuid.UUID) RoleIdentifier { + return RoleIdentifier{Name: RoleOrgWorkspaceCreationBan(), OrganizationID: organizationID} +} + func allPermsExcept(excepts ...Objecter) []Permission { resources := AllResources() var perms []Permission @@ -283,10 +292,11 @@ func ReloadBuiltinRoles(opts *RoleOptions) { Permissions(map[string][]policy.Action{ // Reduced permission set on dormant workspaces. No build, ssh, or exec ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop}, - // Users cannot do create/update/delete on themselves, but they // can read their own details. ResourceUser.Type: {policy.ActionRead, policy.ActionReadPersonal, policy.ActionUpdatePersonal}, + // Can read their own organization member record + ResourceOrganizationMember.Type: {policy.ActionRead}, // Users can create provisioner daemons scoped to themselves. ResourceProvisionerDaemon.Type: {policy.ActionRead, policy.ActionCreate, policy.ActionRead, policy.ActionUpdate}, })..., @@ -297,18 +307,18 @@ func ReloadBuiltinRoles(opts *RoleOptions) { Identifier: RoleAuditor(), DisplayName: "Auditor", Site: Permissions(map[string][]policy.Action{ - // Should be able to read all template details, even in orgs they - // are not in. - ResourceTemplate.Type: {policy.ActionRead, policy.ActionViewInsights}, - ResourceAuditLog.Type: {policy.ActionRead}, - ResourceUser.Type: {policy.ActionRead}, - ResourceGroup.Type: {policy.ActionRead}, - ResourceGroupMember.Type: {policy.ActionRead}, + ResourceAssignOrgRole.Type: {policy.ActionRead}, + ResourceAuditLog.Type: {policy.ActionRead}, + // Allow auditors to see the resources that audit logs reflect. + ResourceTemplate.Type: {policy.ActionRead, policy.ActionViewInsights}, + ResourceUser.Type: {policy.ActionRead}, + ResourceGroup.Type: {policy.ActionRead}, + ResourceGroupMember.Type: {policy.ActionRead}, + ResourceOrganization.Type: {policy.ActionRead}, + ResourceOrganizationMember.Type: {policy.ActionRead}, // Allow auditors to query deployment stats and insights. ResourceDeploymentStats.Type: {policy.ActionRead}, ResourceDeploymentConfig.Type: {policy.ActionRead}, - // Org roles are not really used yet, so grant the perm at the site level. - ResourceOrganizationMember.Type: {policy.ActionRead}, }), Org: map[string][]Permission{}, User: []Permission{}, @@ -318,18 +328,18 @@ func ReloadBuiltinRoles(opts *RoleOptions) { Identifier: RoleTemplateAdmin(), DisplayName: "Template Admin", Site: Permissions(map[string][]policy.Action{ - ResourceTemplate.Type: ResourceTemplate.AvailableActions(), + ResourceAssignOrgRole.Type: {policy.ActionRead}, + ResourceTemplate.Type: ResourceTemplate.AvailableActions(), // CRUD all files, even those they did not upload. ResourceFile.Type: {policy.ActionCreate, policy.ActionRead}, ResourceWorkspace.Type: {policy.ActionRead}, // CRUD to provisioner daemons for now. ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, // Needs to read all organizations since - ResourceOrganization.Type: {policy.ActionRead}, - ResourceUser.Type: {policy.ActionRead}, - ResourceGroup.Type: {policy.ActionRead}, - ResourceGroupMember.Type: {policy.ActionRead}, - // Org roles are not really used yet, so grant the perm at the site level. + ResourceUser.Type: {policy.ActionRead}, + ResourceGroup.Type: {policy.ActionRead}, + ResourceGroupMember.Type: {policy.ActionRead}, + ResourceOrganization.Type: {policy.ActionRead}, ResourceOrganizationMember.Type: {policy.ActionRead}, }), Org: map[string][]Permission{}, @@ -340,18 +350,19 @@ func ReloadBuiltinRoles(opts *RoleOptions) { Identifier: RoleUserAdmin(), DisplayName: "User Admin", Site: Permissions(map[string][]policy.Action{ - ResourceAssignRole.Type: {policy.ActionAssign, policy.ActionDelete, policy.ActionRead}, + ResourceAssignRole.Type: {policy.ActionAssign, policy.ActionUnassign, policy.ActionRead}, // Need organization assign as well to create users. At present, creating a user // will always assign them to some organization. - ResourceAssignOrgRole.Type: {policy.ActionAssign, policy.ActionDelete, policy.ActionRead}, + ResourceAssignOrgRole.Type: {policy.ActionAssign, policy.ActionUnassign, policy.ActionRead}, ResourceUser.Type: { policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete, policy.ActionUpdatePersonal, policy.ActionReadPersonal, }, + ResourceGroup.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, + ResourceGroupMember.Type: {policy.ActionRead}, + ResourceOrganization.Type: {policy.ActionRead}, // Full perms to manage org members ResourceOrganizationMember.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, - ResourceGroup.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, - ResourceGroupMember.Type: {policy.ActionRead}, // Manage org membership based on OIDC claims ResourceIdpsyncSettings.Type: {policy.ActionRead, policy.ActionUpdate}, }), @@ -424,12 +435,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) { ResourceAssignOrgRole.Type: {policy.ActionRead}, }), }, - User: []Permission{ - { - ResourceType: ResourceOrganizationMember.Type, - Action: policy.ActionRead, - }, - }, + User: []Permission{}, } }, orgAuditor: func(organizationID uuid.UUID) Role { @@ -440,6 +446,12 @@ func ReloadBuiltinRoles(opts *RoleOptions) { Org: map[string][]Permission{ organizationID.String(): Permissions(map[string][]policy.Action{ ResourceAuditLog.Type: {policy.ActionRead}, + // Allow auditors to see the resources that audit logs reflect. + ResourceTemplate.Type: {policy.ActionRead, policy.ActionViewInsights}, + ResourceGroup.Type: {policy.ActionRead}, + ResourceGroupMember.Type: {policy.ActionRead}, + ResourceOrganization.Type: {policy.ActionRead}, + ResourceOrganizationMember.Type: {policy.ActionRead}, }), }, User: []Permission{}, @@ -458,7 +470,8 @@ func ReloadBuiltinRoles(opts *RoleOptions) { Org: map[string][]Permission{ organizationID.String(): Permissions(map[string][]policy.Action{ // Assign, remove, and read roles in the organization. - ResourceAssignOrgRole.Type: {policy.ActionAssign, policy.ActionDelete, policy.ActionRead}, + ResourceAssignOrgRole.Type: {policy.ActionAssign, policy.ActionUnassign, policy.ActionRead}, + ResourceOrganization.Type: {policy.ActionRead}, ResourceOrganizationMember.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, ResourceGroup.Type: ResourceGroup.AvailableActions(), ResourceGroupMember.Type: ResourceGroupMember.AvailableActions(), @@ -480,15 +493,45 @@ func ReloadBuiltinRoles(opts *RoleOptions) { ResourceFile.Type: {policy.ActionCreate, policy.ActionRead}, ResourceWorkspace.Type: {policy.ActionRead}, // Assigning template perms requires this permission. + ResourceOrganization.Type: {policy.ActionRead}, ResourceOrganizationMember.Type: {policy.ActionRead}, ResourceGroup.Type: {policy.ActionRead}, ResourceGroupMember.Type: {policy.ActionRead}, - ResourceProvisionerJobs.Type: {policy.ActionRead}, + // Since templates have to correlate with provisioners, + // the ability to create templates and provisioners has + // a lot of overlap. + ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, + ResourceProvisionerJobs.Type: {policy.ActionRead}, }), }, User: []Permission{}, } }, + // orgWorkspaceCreationBan prevents creating & deleting workspaces. This + // overrides any permissions granted by the org or user level. It accomplishes + // this by using negative permissions. + orgWorkspaceCreationBan: func(organizationID uuid.UUID) Role { + return Role{ + Identifier: RoleIdentifier{Name: orgWorkspaceCreationBan, OrganizationID: organizationID}, + DisplayName: "Organization Workspace Creation Ban", + Site: []Permission{}, + Org: map[string][]Permission{ + organizationID.String(): { + { + Negate: true, + ResourceType: ResourceWorkspace.Type, + Action: policy.ActionCreate, + }, + { + Negate: true, + ResourceType: ResourceWorkspace.Type, + Action: policy.ActionDelete, + }, + }, + }, + User: []Permission{}, + } + }, } } @@ -499,44 +542,47 @@ func ReloadBuiltinRoles(opts *RoleOptions) { // map[actor_role][assign_role] var assignRoles = map[string]map[string]bool{ "system": { - owner: true, - auditor: true, - member: true, - orgAdmin: true, - orgMember: true, - orgAuditor: true, - orgUserAdmin: true, - orgTemplateAdmin: true, - templateAdmin: true, - userAdmin: true, - customSiteRole: true, - customOrganizationRole: true, + owner: true, + auditor: true, + member: true, + orgAdmin: true, + orgMember: true, + orgAuditor: true, + orgUserAdmin: true, + orgTemplateAdmin: true, + orgWorkspaceCreationBan: true, + templateAdmin: true, + userAdmin: true, + customSiteRole: true, + customOrganizationRole: true, }, owner: { - owner: true, - auditor: true, - member: true, - orgAdmin: true, - orgMember: true, - orgAuditor: true, - orgUserAdmin: true, - orgTemplateAdmin: true, - templateAdmin: true, - userAdmin: true, - customSiteRole: true, - customOrganizationRole: true, + owner: true, + auditor: true, + member: true, + orgAdmin: true, + orgMember: true, + orgAuditor: true, + orgUserAdmin: true, + orgTemplateAdmin: true, + orgWorkspaceCreationBan: true, + templateAdmin: true, + userAdmin: true, + customSiteRole: true, + customOrganizationRole: true, }, userAdmin: { member: true, orgMember: true, }, orgAdmin: { - orgAdmin: true, - orgMember: true, - orgAuditor: true, - orgUserAdmin: true, - orgTemplateAdmin: true, - customOrganizationRole: true, + orgAdmin: true, + orgMember: true, + orgAuditor: true, + orgUserAdmin: true, + orgTemplateAdmin: true, + orgWorkspaceCreationBan: true, + customOrganizationRole: true, }, orgUserAdmin: { orgMember: true, diff --git a/coderd/rbac/roles_test.go b/coderd/rbac/roles_test.go index 6d42b1b05361c..51eb15def9739 100644 --- a/coderd/rbac/roles_test.go +++ b/coderd/rbac/roles_test.go @@ -112,11 +112,13 @@ func TestRolePermissions(t *testing.T) { // Subjects to user memberMe := authSubject{Name: "member_me", Actor: rbac.Subject{ID: currentUser.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember()}}} orgMemberMe := authSubject{Name: "org_member_me", Actor: rbac.Subject{ID: currentUser.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgMember(orgID)}}} + orgMemberMeBanWorkspace := authSubject{Name: "org_member_me_workspace_ban", Actor: rbac.Subject{ID: currentUser.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgMember(orgID), rbac.ScopedRoleOrgWorkspaceCreationBan(orgID)}}} groupMemberMe := authSubject{Name: "group_member_me", Actor: rbac.Subject{ID: currentUser.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgMember(orgID)}, Groups: []string{groupID.String()}}} owner := authSubject{Name: "owner", Actor: rbac.Subject{ID: adminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.RoleOwner()}}} templateAdmin := authSubject{Name: "template-admin", Actor: rbac.Subject{ID: templateAdminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.RoleTemplateAdmin()}}} userAdmin := authSubject{Name: "user-admin", Actor: rbac.Subject{ID: userAdminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.RoleUserAdmin()}}} + auditor := authSubject{Name: "auditor", Actor: rbac.Subject{ID: auditorID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.RoleAuditor()}}} orgAdmin := authSubject{Name: "org_admin", Actor: rbac.Subject{ID: adminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgMember(orgID), rbac.ScopedRoleOrgAdmin(orgID)}}} orgAuditor := authSubject{Name: "org_auditor", Actor: rbac.Subject{ID: auditorID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgMember(orgID), rbac.ScopedRoleOrgAuditor(orgID)}}} @@ -180,20 +182,30 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, orgMemberMe, orgAdmin, templateAdmin, orgTemplateAdmin}, + true: {owner, orgMemberMe, orgAdmin, templateAdmin, orgTemplateAdmin, orgMemberMeBanWorkspace}, false: {setOtherOrg, memberMe, userAdmin, orgAuditor, orgUserAdmin}, }, }, { - Name: "C_RDMyWorkspaceInOrg", + Name: "UpdateMyWorkspaceInOrg", // When creating the WithID won't be set, but it does not change the result. - Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, + Actions: []policy.Action{policy.ActionUpdate}, Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgMemberMe, orgAdmin}, false: {setOtherOrg, memberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor}, }, }, + { + Name: "CreateDeleteMyWorkspaceInOrg", + // When creating the WithID won't be set, but it does not change the result. + Actions: []policy.Action{policy.ActionCreate, policy.ActionDelete}, + Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()), + AuthorizeMap: map[bool][]hasAuthSubjects{ + true: {owner, orgMemberMe, orgAdmin}, + false: {setOtherOrg, memberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgMemberMeBanWorkspace}, + }, + }, { Name: "MyWorkspaceInOrgExecution", // When creating the WithID won't be set, but it does not change the result. @@ -216,20 +228,20 @@ func TestRolePermissions(t *testing.T) { }, { Name: "Templates", - Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete, policy.ActionViewInsights}, + Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, Resource: rbac.ResourceTemplate.WithID(templateID).InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, templateAdmin, orgTemplateAdmin}, - false: {setOtherOrg, orgAuditor, orgUserAdmin, memberMe, orgMemberMe, userAdmin}, + false: {setOtherOrg, orgUserAdmin, orgAuditor, memberMe, orgMemberMe, userAdmin}, }, }, { Name: "ReadTemplates", - Actions: []policy.Action{policy.ActionRead}, + Actions: []policy.Action{policy.ActionRead, policy.ActionViewInsights}, Resource: rbac.ResourceTemplate.InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, orgAdmin, templateAdmin, orgTemplateAdmin}, - false: {setOtherOrg, orgAuditor, orgUserAdmin, memberMe, userAdmin, orgMemberMe}, + true: {owner, orgAuditor, orgAdmin, templateAdmin, orgTemplateAdmin}, + false: {setOtherOrg, orgUserAdmin, memberMe, userAdmin, orgMemberMe}, }, }, { @@ -286,14 +298,14 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceOrganization.WithID(orgID).InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, orgAdmin, orgMemberMe, templateAdmin, orgTemplateAdmin, orgAuditor, orgUserAdmin}, - false: {setOtherOrg, memberMe, userAdmin}, + true: {owner, orgAdmin, orgMemberMe, templateAdmin, orgTemplateAdmin, auditor, orgAuditor, userAdmin, orgUserAdmin}, + false: {setOtherOrg, memberMe}, }, }, { - Name: "CreateCustomRole", - Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate}, - Resource: rbac.ResourceAssignRole, + Name: "CreateUpdateDeleteCustomRole", + Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, + Resource: rbac.ResourceAssignOrgRole, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, false: {setOtherOrg, setOrgNotMe, userAdmin, orgMemberMe, memberMe, templateAdmin}, @@ -301,7 +313,7 @@ func TestRolePermissions(t *testing.T) { }, { Name: "RoleAssignment", - Actions: []policy.Action{policy.ActionAssign, policy.ActionDelete}, + Actions: []policy.Action{policy.ActionAssign, policy.ActionUnassign}, Resource: rbac.ResourceAssignRole, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, userAdmin}, @@ -319,7 +331,7 @@ func TestRolePermissions(t *testing.T) { }, { Name: "OrgRoleAssignment", - Actions: []policy.Action{policy.ActionAssign, policy.ActionDelete}, + Actions: []policy.Action{policy.ActionAssign, policy.ActionUnassign}, Resource: rbac.ResourceAssignOrgRole.InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, userAdmin, orgUserAdmin}, @@ -340,8 +352,8 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceAssignOrgRole.InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, setOrgNotMe, orgMemberMe, userAdmin}, - false: {setOtherOrg, memberMe, templateAdmin}, + true: {owner, setOrgNotMe, orgMemberMe, userAdmin, templateAdmin}, + false: {setOtherOrg, memberMe}, }, }, { @@ -376,8 +388,8 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceOrganizationMember.WithID(currentUser).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, orgAdmin, userAdmin, orgMemberMe, templateAdmin, orgUserAdmin, orgTemplateAdmin}, - false: {memberMe, setOtherOrg, orgAuditor}, + true: {owner, orgAuditor, orgAdmin, userAdmin, orgMemberMe, templateAdmin, orgUserAdmin, orgTemplateAdmin}, + false: {memberMe, setOtherOrg}, }, }, { @@ -403,7 +415,7 @@ func TestRolePermissions(t *testing.T) { }), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, userAdmin, orgUserAdmin}, - false: {setOtherOrg, memberMe, orgMemberMe, templateAdmin, orgTemplateAdmin, orgAuditor, groupMemberMe}, + false: {setOtherOrg, memberMe, orgMemberMe, templateAdmin, orgTemplateAdmin, groupMemberMe, orgAuditor}, }, }, { @@ -415,8 +427,8 @@ func TestRolePermissions(t *testing.T) { }, }), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, orgAdmin, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, groupMemberMe}, - false: {setOtherOrg, memberMe, orgMemberMe, orgAuditor}, + true: {owner, orgAdmin, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, groupMemberMe, orgAuditor}, + false: {setOtherOrg, memberMe, orgMemberMe}, }, }, { @@ -424,8 +436,8 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceGroupMember.WithID(currentUser).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, orgAdmin, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgMemberMe, groupMemberMe}, - false: {setOtherOrg, memberMe, orgAuditor}, + true: {owner, orgAuditor, orgAdmin, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgMemberMe, groupMemberMe}, + false: {setOtherOrg, memberMe}, }, }, { @@ -433,8 +445,8 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceGroupMember.WithID(adminID).InOrg(orgID).WithOwner(adminID.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, orgAdmin, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin}, - false: {setOtherOrg, memberMe, orgAuditor, orgMemberMe, groupMemberMe}, + true: {owner, orgAuditor, orgAdmin, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin}, + false: {setOtherOrg, memberMe, orgMemberMe, groupMemberMe}, }, }, { @@ -533,8 +545,8 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, Resource: rbac.ResourceProvisionerDaemon.InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, templateAdmin, orgAdmin}, - false: {setOtherOrg, orgTemplateAdmin, orgUserAdmin, memberMe, orgMemberMe, userAdmin, orgAuditor}, + true: {owner, templateAdmin, orgAdmin, orgTemplateAdmin}, + false: {setOtherOrg, orgAuditor, orgUserAdmin, memberMe, orgMemberMe, userAdmin}, }, }, { @@ -551,17 +563,8 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, Resource: rbac.ResourceProvisionerDaemon.WithOwner(currentUser.String()).InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, templateAdmin, orgMemberMe, orgAdmin}, - false: {setOtherOrg, memberMe, userAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor}, - }, - }, - { - Name: "ProvisionerKeys", - Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionDelete}, - Resource: rbac.ResourceProvisionerKeys.InOrg(orgID), - AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, orgAdmin}, - false: {setOtherOrg, memberMe, orgMemberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor}, + true: {owner, templateAdmin, orgTemplateAdmin, orgMemberMe, orgAdmin}, + false: {setOtherOrg, memberMe, userAdmin, orgUserAdmin, orgAuditor}, }, }, { @@ -777,6 +780,21 @@ func TestRolePermissions(t *testing.T) { }, }, }, + { + Name: "ResourceMonitor", + Actions: []policy.Action{policy.ActionRead, policy.ActionCreate, policy.ActionUpdate}, + Resource: rbac.ResourceWorkspaceAgentResourceMonitor, + AuthorizeMap: map[bool][]hasAuthSubjects{ + true: {owner}, + false: { + memberMe, orgMemberMe, otherOrgMember, + orgAdmin, otherOrgAdmin, + orgAuditor, otherOrgAuditor, + templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, + userAdmin, orgUserAdmin, otherOrgUserAdmin, + }, + }, + }, } // We expect every permission to be tested above. @@ -935,6 +953,7 @@ func TestListRoles(t *testing.T) { fmt.Sprintf("organization-auditor:%s", orgID.String()), fmt.Sprintf("organization-user-admin:%s", orgID.String()), fmt.Sprintf("organization-template-admin:%s", orgID.String()), + fmt.Sprintf("organization-workspace-creation-ban:%s", orgID.String()), }, orgRoleNames) } diff --git a/coderd/runtimeconfig/resolver.go b/coderd/runtimeconfig/resolver.go index d899680f034a4..5d06a156bfb41 100644 --- a/coderd/runtimeconfig/resolver.go +++ b/coderd/runtimeconfig/resolver.go @@ -12,6 +12,9 @@ import ( "github.com/coder/coder/v2/coderd/database" ) +// NoopResolver implements the Resolver interface +var _ Resolver = &NoopResolver{} + // NoopResolver is a useful test device. type NoopResolver struct{} @@ -31,6 +34,9 @@ func (NoopResolver) DeleteRuntimeConfig(context.Context, string) error { return ErrEntryNotFound } +// StoreResolver implements the Resolver interface +var _ Resolver = &StoreResolver{} + // StoreResolver uses the database as the underlying store for runtime settings. type StoreResolver struct { db Store diff --git a/coderd/searchquery/search.go b/coderd/searchquery/search.go index a4fe5d4775d6c..103dc80601ad9 100644 --- a/coderd/searchquery/search.go +++ b/coderd/searchquery/search.go @@ -19,6 +19,20 @@ import ( // AuditLogs requires the database to fetch an organization by name // to convert to organization uuid. +// +// Supported query parameters: +// +// - request_id: UUID (can be used to search for associated audits e.g. connect/disconnect or open/close) +// - resource_id: UUID +// - resource_target: string +// - username: string +// - email: string +// - date_from: string (date in format "2006-01-02") +// - date_to: string (date in format "2006-01-02") +// - organization: string (organization UUID or name) +// - resource_type: string (enum) +// - action: string (enum) +// - build_reason: string (enum) func AuditLogs(ctx context.Context, db database.Store, query string) (database.GetAuditLogsOffsetParams, []codersdk.ValidationError) { // Always lowercase for all searches. query = strings.ToLower(query) @@ -33,6 +47,7 @@ func AuditLogs(ctx context.Context, db database.Store, query string) (database.G const dateLayout = "2006-01-02" parser := httpapi.NewQueryParamParser() filter := database.GetAuditLogsOffsetParams{ + RequestID: parser.UUID(values, uuid.Nil, "request_id"), ResourceID: parser.UUID(values, uuid.Nil, "resource_id"), ResourceTarget: parser.String(values, "", "resource_target"), Username: parser.String(values, "", "username"), @@ -243,7 +258,9 @@ func parseOrganization(ctx context.Context, db database.Store, parser *httpapi.Q if err == nil { return organizationID, nil } - organization, err := db.GetOrganizationByName(ctx, v) + organization, err := db.GetOrganizationByName(ctx, database.GetOrganizationByNameParams{ + Name: v, Deleted: false, + }) if err != nil { return uuid.Nil, xerrors.Errorf("organization %q either does not exist, or you are unauthorized to view it", v) } diff --git a/coderd/searchquery/search_test.go b/coderd/searchquery/search_test.go index 91d285afbd8ec..0a8e08e3d45fe 100644 --- a/coderd/searchquery/search_test.go +++ b/coderd/searchquery/search_test.go @@ -344,6 +344,11 @@ func TestSearchAudit(t *testing.T) { ResourceTarget: "foo", }, }, + { + Name: "RequestID", + Query: "request_id:foo", + ExpectedErrorContains: "valid uuid", + }, } for _, c := range testCases { diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index 233450c43d943..8956fed23990e 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "crypto/sha256" + "database/sql" "encoding/json" "errors" "fmt" @@ -14,6 +15,7 @@ import ( "regexp" "runtime" "slices" + "strconv" "strings" "sync" "time" @@ -41,6 +43,7 @@ const ( ) type Options struct { + Disabled bool Database database.Store Logger slog.Logger // URL is an endpoint to direct telemetry towards! @@ -115,8 +118,8 @@ type remoteReporter struct { shutdownAt *time.Time } -func (*remoteReporter) Enabled() bool { - return true +func (r *remoteReporter) Enabled() bool { + return !r.options.Disabled } func (r *remoteReporter) Report(snapshot *Snapshot) { @@ -160,10 +163,12 @@ func (r *remoteReporter) Close() { close(r.closed) now := dbtime.Now() r.shutdownAt = &now - // Report a final collection of telemetry prior to close! - // This could indicate final actions a user has taken, and - // the time the deployment was shutdown. - r.reportWithDeployment() + if r.Enabled() { + // Report a final collection of telemetry prior to close! + // This could indicate final actions a user has taken, and + // the time the deployment was shutdown. + r.reportWithDeployment() + } r.closeFunc() } @@ -176,7 +181,74 @@ func (r *remoteReporter) isClosed() bool { } } +// See the corresponding test in telemetry_test.go for a truth table. +func ShouldReportTelemetryDisabled(recordedTelemetryEnabled *bool, telemetryEnabled bool) bool { + return recordedTelemetryEnabled != nil && *recordedTelemetryEnabled && !telemetryEnabled +} + +// RecordTelemetryStatus records the telemetry status in the database. +// If the status changed from enabled to disabled, returns a snapshot to +// be sent to the telemetry server. +func RecordTelemetryStatus( //nolint:revive + ctx context.Context, + logger slog.Logger, + db database.Store, + telemetryEnabled bool, +) (*Snapshot, error) { + item, err := db.GetTelemetryItem(ctx, string(TelemetryItemKeyTelemetryEnabled)) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return nil, xerrors.Errorf("get telemetry enabled: %w", err) + } + var recordedTelemetryEnabled *bool + if !errors.Is(err, sql.ErrNoRows) { + value, err := strconv.ParseBool(item.Value) + if err != nil { + logger.Debug(ctx, "parse telemetry enabled", slog.Error(err)) + } + // If ParseBool fails, value will default to false. + // This may happen if an admin manually edits the telemetry item + // in the database. + recordedTelemetryEnabled = &value + } + + if err := db.UpsertTelemetryItem(ctx, database.UpsertTelemetryItemParams{ + Key: string(TelemetryItemKeyTelemetryEnabled), + Value: strconv.FormatBool(telemetryEnabled), + }); err != nil { + return nil, xerrors.Errorf("upsert telemetry enabled: %w", err) + } + + shouldReport := ShouldReportTelemetryDisabled(recordedTelemetryEnabled, telemetryEnabled) + if !shouldReport { + return nil, nil //nolint:nilnil + } + // If any of the following calls fail, we will never report that telemetry changed + // from enabled to disabled. This is okay. We only want to ping the telemetry server + // once, and never again. If that attempt fails, so be it. + item, err = db.GetTelemetryItem(ctx, string(TelemetryItemKeyTelemetryEnabled)) + if err != nil { + return nil, xerrors.Errorf("get telemetry enabled after upsert: %w", err) + } + return &Snapshot{ + TelemetryItems: []TelemetryItem{ + ConvertTelemetryItem(item), + }, + }, nil +} + func (r *remoteReporter) runSnapshotter() { + telemetryDisabledSnapshot, err := RecordTelemetryStatus(r.ctx, r.options.Logger, r.options.Database, r.Enabled()) + if err != nil { + r.options.Logger.Debug(r.ctx, "record and maybe report telemetry status", slog.Error(err)) + } + if telemetryDisabledSnapshot != nil { + r.reportSync(telemetryDisabledSnapshot) + } + r.options.Logger.Debug(r.ctx, "finished telemetry status check") + if !r.Enabled() { + return + } + first := true ticker := time.NewTicker(r.options.SnapshotFrequency) defer ticker.Stop() @@ -244,6 +316,11 @@ func (r *remoteReporter) deployment() error { return xerrors.Errorf("install source must be <=64 chars: %s", installSource) } + idpOrgSync, err := checkIDPOrgSync(r.ctx, r.options.Database, r.options.DeploymentConfig) + if err != nil { + r.options.Logger.Debug(r.ctx, "check IDP org sync", slog.Error(err)) + } + data, err := json.Marshal(&Deployment{ ID: r.options.DeploymentID, Architecture: sysInfo.Architecture, @@ -263,6 +340,7 @@ func (r *remoteReporter) deployment() error { MachineID: sysInfo.UniqueID, StartedAt: r.startedAt, ShutdownAt: r.shutdownAt, + IDPOrgSync: &idpOrgSync, }) if err != nil { return xerrors.Errorf("marshal deployment: %w", err) @@ -284,6 +362,45 @@ func (r *remoteReporter) deployment() error { return nil } +// idpOrgSyncConfig is a subset of +// https://github.com/coder/coder/blob/5c6578d84e2940b9cfd04798c45e7c8042c3fe0e/coderd/idpsync/organization.go#L148 +type idpOrgSyncConfig struct { + Field string `json:"field"` +} + +// checkIDPOrgSync inspects the server flags and the runtime config. It's based on +// the OrganizationSyncEnabled function from enterprise/coderd/enidpsync/organizations.go. +// It has one distinct difference: it doesn't check if the license entitles to the +// feature, it only checks if the feature is configured. +// +// The above function is not used because it's very hard to make it available in +// the telemetry package due to coder/coder package structure and initialization +// order of the coder server. +// +// We don't check license entitlements because it's also hard to do from the +// telemetry package, and the config check should be sufficient for telemetry purposes. +// +// While this approach duplicates code, it's simpler than the alternative. +// +// See https://github.com/coder/coder/pull/16323 for more details. +func checkIDPOrgSync(ctx context.Context, db database.Store, values *codersdk.DeploymentValues) (bool, error) { + // key based on https://github.com/coder/coder/blob/5c6578d84e2940b9cfd04798c45e7c8042c3fe0e/coderd/idpsync/idpsync.go#L168 + syncConfigRaw, err := db.GetRuntimeConfig(ctx, "organization-sync-settings") + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + // If the runtime config is not set, we check if the deployment config + // has the organization field set. + return values != nil && values.OIDC.OrganizationField != "", nil + } + return false, xerrors.Errorf("get runtime config: %w", err) + } + syncConfig := idpOrgSyncConfig{} + if err := json.Unmarshal([]byte(syncConfigRaw), &syncConfig); err != nil { + return false, xerrors.Errorf("unmarshal runtime config: %w", err) + } + return syncConfig.Field != "", nil +} + // createSnapshot collects a full snapshot from the database. func (r *remoteReporter) createSnapshot() (*Snapshot, error) { var ( @@ -507,6 +624,28 @@ func (r *remoteReporter) createSnapshot() (*Snapshot, error) { } return nil }) + eg.Go(func() error { + memoryMonitors, err := r.options.Database.FetchMemoryResourceMonitorsUpdatedAfter(ctx, createdAfter) + if err != nil { + return xerrors.Errorf("get memory resource monitors: %w", err) + } + snapshot.WorkspaceAgentMemoryResourceMonitors = make([]WorkspaceAgentMemoryResourceMonitor, 0, len(memoryMonitors)) + for _, monitor := range memoryMonitors { + snapshot.WorkspaceAgentMemoryResourceMonitors = append(snapshot.WorkspaceAgentMemoryResourceMonitors, ConvertWorkspaceAgentMemoryResourceMonitor(monitor)) + } + return nil + }) + eg.Go(func() error { + volumeMonitors, err := r.options.Database.FetchVolumesResourceMonitorsUpdatedAfter(ctx, createdAfter) + if err != nil { + return xerrors.Errorf("get volume resource monitors: %w", err) + } + snapshot.WorkspaceAgentVolumeResourceMonitors = make([]WorkspaceAgentVolumeResourceMonitor, 0, len(volumeMonitors)) + for _, monitor := range volumeMonitors { + snapshot.WorkspaceAgentVolumeResourceMonitors = append(snapshot.WorkspaceAgentVolumeResourceMonitors, ConvertWorkspaceAgentVolumeResourceMonitor(monitor)) + } + return nil + }) eg.Go(func() error { proxies, err := r.options.Database.GetWorkspaceProxies(ctx) if err != nil { @@ -518,6 +657,32 @@ func (r *remoteReporter) createSnapshot() (*Snapshot, error) { } return nil }) + eg.Go(func() error { + // Warning: When an organization is deleted, it's completely removed from + // the database. It will no longer be reported, and there will be no other + // indicator that it was deleted. This requires special handling when + // interpreting the telemetry data later. + orgs, err := r.options.Database.GetOrganizations(r.ctx, database.GetOrganizationsParams{}) + if err != nil { + return xerrors.Errorf("get organizations: %w", err) + } + snapshot.Organizations = make([]Organization, 0, len(orgs)) + for _, org := range orgs { + snapshot.Organizations = append(snapshot.Organizations, ConvertOrganization(org)) + } + return nil + }) + eg.Go(func() error { + items, err := r.options.Database.GetTelemetryItems(ctx) + if err != nil { + return xerrors.Errorf("get telemetry items: %w", err) + } + snapshot.TelemetryItems = make([]TelemetryItem, 0, len(items)) + for _, item := range items { + snapshot.TelemetryItems = append(snapshot.TelemetryItems, ConvertTelemetryItem(item)) + } + return nil + }) err := eg.Wait() if err != nil { @@ -622,6 +787,26 @@ func ConvertWorkspaceAgent(agent database.WorkspaceAgent) WorkspaceAgent { return snapAgent } +func ConvertWorkspaceAgentMemoryResourceMonitor(monitor database.WorkspaceAgentMemoryResourceMonitor) WorkspaceAgentMemoryResourceMonitor { + return WorkspaceAgentMemoryResourceMonitor{ + AgentID: monitor.AgentID, + Enabled: monitor.Enabled, + Threshold: monitor.Threshold, + CreatedAt: monitor.CreatedAt, + UpdatedAt: monitor.UpdatedAt, + } +} + +func ConvertWorkspaceAgentVolumeResourceMonitor(monitor database.WorkspaceAgentVolumeResourceMonitor) WorkspaceAgentVolumeResourceMonitor { + return WorkspaceAgentVolumeResourceMonitor{ + AgentID: monitor.AgentID, + Enabled: monitor.Enabled, + Threshold: monitor.Threshold, + CreatedAt: monitor.CreatedAt, + UpdatedAt: monitor.UpdatedAt, + } +} + // ConvertWorkspaceAgentStat anonymizes a workspace agent stat. func ConvertWorkspaceAgentStat(stat database.GetWorkspaceAgentStatsRow) WorkspaceAgentStat { return WorkspaceAgentStat{ @@ -804,6 +989,7 @@ func ConvertUser(dbUser database.User) User { CreatedAt: dbUser.CreatedAt, Status: dbUser.Status, GithubComUserID: dbUser.GithubComUserID.Int64, + LoginType: string(dbUser.LoginType), } } @@ -916,32 +1102,53 @@ func ConvertExternalProvisioner(id uuid.UUID, tags map[string]string, provisione } } +func ConvertOrganization(org database.Organization) Organization { + return Organization{ + ID: org.ID, + CreatedAt: org.CreatedAt, + IsDefault: org.IsDefault, + } +} + +func ConvertTelemetryItem(item database.TelemetryItem) TelemetryItem { + return TelemetryItem{ + Key: item.Key, + Value: item.Value, + CreatedAt: item.CreatedAt, + UpdatedAt: item.UpdatedAt, + } +} + // Snapshot represents a point-in-time anonymized database dump. // Data is aggregated by latest on the server-side, so partial data // can be sent without issue. type Snapshot struct { DeploymentID string `json:"deployment_id"` - APIKeys []APIKey `json:"api_keys"` - CLIInvocations []clitelemetry.Invocation `json:"cli_invocations"` - ExternalProvisioners []ExternalProvisioner `json:"external_provisioners"` - Licenses []License `json:"licenses"` - ProvisionerJobs []ProvisionerJob `json:"provisioner_jobs"` - TemplateVersions []TemplateVersion `json:"template_versions"` - Templates []Template `json:"templates"` - Users []User `json:"users"` - Groups []Group `json:"groups"` - GroupMembers []GroupMember `json:"group_members"` - WorkspaceAgentStats []WorkspaceAgentStat `json:"workspace_agent_stats"` - WorkspaceAgents []WorkspaceAgent `json:"workspace_agents"` - WorkspaceApps []WorkspaceApp `json:"workspace_apps"` - WorkspaceBuilds []WorkspaceBuild `json:"workspace_build"` - WorkspaceProxies []WorkspaceProxy `json:"workspace_proxies"` - WorkspaceResourceMetadata []WorkspaceResourceMetadata `json:"workspace_resource_metadata"` - WorkspaceResources []WorkspaceResource `json:"workspace_resources"` - WorkspaceModules []WorkspaceModule `json:"workspace_modules"` - Workspaces []Workspace `json:"workspaces"` - NetworkEvents []NetworkEvent `json:"network_events"` + APIKeys []APIKey `json:"api_keys"` + CLIInvocations []clitelemetry.Invocation `json:"cli_invocations"` + ExternalProvisioners []ExternalProvisioner `json:"external_provisioners"` + Licenses []License `json:"licenses"` + ProvisionerJobs []ProvisionerJob `json:"provisioner_jobs"` + TemplateVersions []TemplateVersion `json:"template_versions"` + Templates []Template `json:"templates"` + Users []User `json:"users"` + Groups []Group `json:"groups"` + GroupMembers []GroupMember `json:"group_members"` + WorkspaceAgentStats []WorkspaceAgentStat `json:"workspace_agent_stats"` + WorkspaceAgents []WorkspaceAgent `json:"workspace_agents"` + WorkspaceApps []WorkspaceApp `json:"workspace_apps"` + WorkspaceBuilds []WorkspaceBuild `json:"workspace_build"` + WorkspaceProxies []WorkspaceProxy `json:"workspace_proxies"` + WorkspaceResourceMetadata []WorkspaceResourceMetadata `json:"workspace_resource_metadata"` + WorkspaceResources []WorkspaceResource `json:"workspace_resources"` + WorkspaceAgentMemoryResourceMonitors []WorkspaceAgentMemoryResourceMonitor `json:"workspace_agent_memory_resource_monitors"` + WorkspaceAgentVolumeResourceMonitors []WorkspaceAgentVolumeResourceMonitor `json:"workspace_agent_volume_resource_monitors"` + WorkspaceModules []WorkspaceModule `json:"workspace_modules"` + Workspaces []Workspace `json:"workspaces"` + NetworkEvents []NetworkEvent `json:"network_events"` + Organizations []Organization `json:"organizations"` + TelemetryItems []TelemetryItem `json:"telemetry_items"` } // Deployment contains information about the host running Coder. @@ -964,6 +1171,9 @@ type Deployment struct { MachineID string `json:"machine_id"` StartedAt time.Time `json:"started_at"` ShutdownAt *time.Time `json:"shutdown_at"` + // While IDPOrgSync will always be set, it's nullable to make + // the struct backwards compatible with older coder versions. + IDPOrgSync *bool `json:"idp_org_sync"` } type APIKey struct { @@ -984,6 +1194,8 @@ type User struct { RBACRoles []string `json:"rbac_roles"` Status database.UserStatus `json:"status"` GithubComUserID int64 `json:"github_com_user_id"` + // Omitempty for backwards compatibility. + LoginType string `json:"login_type,omitempty"` } type Group struct { @@ -1064,6 +1276,22 @@ type WorkspaceAgentStat struct { SessionCountSSH int64 `json:"session_count_ssh"` } +type WorkspaceAgentMemoryResourceMonitor struct { + AgentID uuid.UUID `json:"agent_id"` + Enabled bool `json:"enabled"` + Threshold int32 `json:"threshold"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type WorkspaceAgentVolumeResourceMonitor struct { + AgentID uuid.UUID `json:"agent_id"` + Enabled bool `json:"enabled"` + Threshold int32 `json:"threshold"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + type WorkspaceApp struct { ID uuid.UUID `json:"id"` CreatedAt time.Time `json:"created_at"` @@ -1457,8 +1685,36 @@ func NetworkEventFromProto(proto *tailnetproto.TelemetryEvent) (NetworkEvent, er }, nil } +type Organization struct { + ID uuid.UUID `json:"id"` + IsDefault bool `json:"is_default"` + CreatedAt time.Time `json:"created_at"` +} + +type telemetryItemKey string + +// The comment below gets rid of the warning that the name "TelemetryItemKey" has +// the "Telemetry" prefix, and that stutters when you use it outside the package +// (telemetry.TelemetryItemKey...). "TelemetryItem" is the name of a database table, +// so it makes sense to use the "Telemetry" prefix. +// +//revive:disable:exported +const ( + TelemetryItemKeyHTMLFirstServedAt telemetryItemKey = "html_first_served_at" + TelemetryItemKeyTelemetryEnabled telemetryItemKey = "telemetry_enabled" +) + +type TelemetryItem struct { + Key string `json:"key"` + Value string `json:"value"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + type noopReporter struct{} -func (*noopReporter) Report(_ *Snapshot) {} -func (*noopReporter) Enabled() bool { return false } -func (*noopReporter) Close() {} +func (*noopReporter) Report(_ *Snapshot) {} +func (*noopReporter) Enabled() bool { return false } +func (*noopReporter) Close() {} +func (*noopReporter) RunSnapshotter() {} +func (*noopReporter) ReportDisabledIfNeeded() error { return nil } diff --git a/coderd/telemetry/telemetry_test.go b/coderd/telemetry/telemetry_test.go index e0cbfd1cfa193..6f97ce8a1270b 100644 --- a/coderd/telemetry/telemetry_test.go +++ b/coderd/telemetry/telemetry_test.go @@ -22,7 +22,10 @@ import ( "github.com/coder/coder/v2/coderd/database/dbmem" "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/coder/v2/coderd/idpsync" + "github.com/coder/coder/v2/coderd/runtimeconfig" "github.com/coder/coder/v2/coderd/telemetry" + "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/testutil" ) @@ -40,27 +43,42 @@ func TestTelemetry(t *testing.T) { db := dbmem.New() ctx := testutil.Context(t, testutil.WaitMedium) + + org, err := db.GetDefaultOrganization(ctx) + require.NoError(t, err) + _, _ = dbgen.APIKey(t, db, database.APIKey{}) _ = dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{ - Provisioner: database.ProvisionerTypeTerraform, - StorageMethod: database.ProvisionerStorageMethodFile, - Type: database.ProvisionerJobTypeTemplateVersionDryRun, + Provisioner: database.ProvisionerTypeTerraform, + StorageMethod: database.ProvisionerStorageMethodFile, + Type: database.ProvisionerJobTypeTemplateVersionDryRun, + OrganizationID: org.ID, }) _ = dbgen.Template(t, db, database.Template{ - Provisioner: database.ProvisionerTypeTerraform, + Provisioner: database.ProvisionerTypeTerraform, + OrganizationID: org.ID, }) sourceExampleID := uuid.NewString() _ = dbgen.TemplateVersion(t, db, database.TemplateVersion{ SourceExampleID: sql.NullString{String: sourceExampleID, Valid: true}, + OrganizationID: org.ID, + }) + _ = dbgen.TemplateVersion(t, db, database.TemplateVersion{ + OrganizationID: org.ID, }) - _ = dbgen.TemplateVersion(t, db, database.TemplateVersion{}) user := dbgen.User(t, db, database.User{}) - _ = dbgen.Workspace(t, db, database.WorkspaceTable{}) + _ = dbgen.Workspace(t, db, database.WorkspaceTable{ + OrganizationID: org.ID, + }) _ = dbgen.WorkspaceApp(t, db, database.WorkspaceApp{ SharingLevel: database.AppSharingLevelOwner, Health: database.WorkspaceAppHealthDisabled, OpenIn: database.WorkspaceAppOpenInSlimWindow, }) + _ = dbgen.TelemetryItem(t, db, database.TelemetryItem{ + Key: string(telemetry.TelemetryItemKeyHTMLFirstServedAt), + Value: time.Now().Format(time.RFC3339), + }) group := dbgen.Group(t, db, database.Group{}) _ = dbgen.GroupMember(t, db, database.GroupMemberTable{UserID: user.ID, GroupID: group.ID}) wsagent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{}) @@ -94,6 +112,8 @@ func TestTelemetry(t *testing.T) { _, _ = dbgen.WorkspaceProxy(t, db, database.WorkspaceProxy{}) _ = dbgen.WorkspaceModule(t, db, database.WorkspaceModule{}) + _ = dbgen.WorkspaceAgentMemoryResourceMonitor(t, db, database.WorkspaceAgentMemoryResourceMonitor{}) + _ = dbgen.WorkspaceAgentVolumeResourceMonitor(t, db, database.WorkspaceAgentVolumeResourceMonitor{}) _, snapshot := collectSnapshot(t, db, nil) require.Len(t, snapshot.ProvisionerJobs, 1) @@ -112,7 +132,11 @@ func TestTelemetry(t *testing.T) { require.Len(t, snapshot.WorkspaceAgentStats, 1) require.Len(t, snapshot.WorkspaceProxies, 1) require.Len(t, snapshot.WorkspaceModules, 1) - + require.Len(t, snapshot.Organizations, 1) + // We create one item manually above. The other is TelemetryEnabled, created by the snapshotter. + require.Len(t, snapshot.TelemetryItems, 2) + require.Len(t, snapshot.WorkspaceAgentMemoryResourceMonitors, 1) + require.Len(t, snapshot.WorkspaceAgentVolumeResourceMonitors, 1) wsa := snapshot.WorkspaceAgents[0] require.Len(t, wsa.Subsystems, 2) require.Equal(t, string(database.WorkspaceAgentSubsystemEnvbox), wsa.Subsystems[0]) @@ -128,6 +152,19 @@ func TestTelemetry(t *testing.T) { }) require.Equal(t, tvs[0].SourceExampleID, &sourceExampleID) require.Nil(t, tvs[1].SourceExampleID) + + for _, entity := range snapshot.Workspaces { + require.Equal(t, entity.OrganizationID, org.ID) + } + for _, entity := range snapshot.ProvisionerJobs { + require.Equal(t, entity.OrganizationID, org.ID) + } + for _, entity := range snapshot.TemplateVersions { + require.Equal(t, entity.OrganizationID, org.ID) + } + for _, entity := range snapshot.Templates { + require.Equal(t, entity.OrganizationID, org.ID) + } }) t.Run("HashedEmail", func(t *testing.T) { t.Parallel() @@ -243,6 +280,41 @@ func TestTelemetry(t *testing.T) { require.Equal(t, c.want, telemetry.GetModuleSourceType(c.source)) } }) + t.Run("IDPOrgSync", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) + db, _ := dbtestutil.NewDB(t) + + // 1. No org sync settings + deployment, _ := collectSnapshot(t, db, nil) + require.False(t, *deployment.IDPOrgSync) + + // 2. Org sync settings set in server flags + deployment, _ = collectSnapshot(t, db, func(opts telemetry.Options) telemetry.Options { + opts.DeploymentConfig = &codersdk.DeploymentValues{ + OIDC: codersdk.OIDCConfig{ + OrganizationField: "organizations", + }, + } + return opts + }) + require.True(t, *deployment.IDPOrgSync) + + // 3. Org sync settings set in runtime config + org, err := db.GetDefaultOrganization(ctx) + require.NoError(t, err) + sync := idpsync.NewAGPLSync(testutil.Logger(t), runtimeconfig.NewManager(), idpsync.DeploymentSyncSettings{}) + err = sync.UpdateOrganizationSyncSettings(ctx, db, idpsync.OrganizationSyncSettings{ + Field: "organizations", + Mapping: map[string][]uuid.UUID{ + "first": {org.ID}, + }, + AssignDefault: true, + }) + require.NoError(t, err) + deployment, _ = collectSnapshot(t, db, nil) + require.True(t, *deployment.IDPOrgSync) + }) } // nolint:paralleltest @@ -253,31 +325,153 @@ func TestTelemetryInstallSource(t *testing.T) { require.Equal(t, "aws_marketplace", deployment.InstallSource) } -func collectSnapshot(t *testing.T, db database.Store, addOptionsFn func(opts telemetry.Options) telemetry.Options) (*telemetry.Deployment, *telemetry.Snapshot) { +func TestTelemetryItem(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) + db, _ := dbtestutil.NewDB(t) + key := testutil.GetRandomName(t) + value := time.Now().Format(time.RFC3339) + + err := db.InsertTelemetryItemIfNotExists(ctx, database.InsertTelemetryItemIfNotExistsParams{ + Key: key, + Value: value, + }) + require.NoError(t, err) + + item, err := db.GetTelemetryItem(ctx, key) + require.NoError(t, err) + require.Equal(t, item.Key, key) + require.Equal(t, item.Value, value) + + // Inserting a new value should not update the existing value + err = db.InsertTelemetryItemIfNotExists(ctx, database.InsertTelemetryItemIfNotExistsParams{ + Key: key, + Value: "new_value", + }) + require.NoError(t, err) + + item, err = db.GetTelemetryItem(ctx, key) + require.NoError(t, err) + require.Equal(t, item.Value, value) + + // Upserting a new value should update the existing value + err = db.UpsertTelemetryItem(ctx, database.UpsertTelemetryItemParams{ + Key: key, + Value: "new_value", + }) + require.NoError(t, err) + + item, err = db.GetTelemetryItem(ctx, key) + require.NoError(t, err) + require.Equal(t, item.Value, "new_value") +} + +func TestShouldReportTelemetryDisabled(t *testing.T) { + t.Parallel() + // Description | telemetryEnabled (db) | telemetryEnabled (is) | Report Telemetry Disabled | + //----------------------------------------|-----------------------|-----------------------|---------------------------| + // New deployment | | true | No | + // New deployment with telemetry disabled | | false | No | + // Telemetry was enabled, and still is | true | true | No | + // Telemetry was enabled but now disabled | true | false | Yes | + // Telemetry was disabled, now is enabled | false | true | No | + // Telemetry was disabled, still disabled | false | false | No | + boolTrue := true + boolFalse := false + require.False(t, telemetry.ShouldReportTelemetryDisabled(nil, true)) + require.False(t, telemetry.ShouldReportTelemetryDisabled(nil, false)) + require.False(t, telemetry.ShouldReportTelemetryDisabled(&boolTrue, true)) + require.True(t, telemetry.ShouldReportTelemetryDisabled(&boolTrue, false)) + require.False(t, telemetry.ShouldReportTelemetryDisabled(&boolFalse, true)) + require.False(t, telemetry.ShouldReportTelemetryDisabled(&boolFalse, false)) +} + +func TestRecordTelemetryStatus(t *testing.T) { + t.Parallel() + for _, testCase := range []struct { + name string + recordedTelemetryEnabled string + telemetryEnabled bool + shouldReport bool + }{ + {name: "New deployment", recordedTelemetryEnabled: "nil", telemetryEnabled: true, shouldReport: false}, + {name: "Telemetry disabled", recordedTelemetryEnabled: "nil", telemetryEnabled: false, shouldReport: false}, + {name: "Telemetry was enabled and still is", recordedTelemetryEnabled: "true", telemetryEnabled: true, shouldReport: false}, + {name: "Telemetry was enabled but now disabled", recordedTelemetryEnabled: "true", telemetryEnabled: false, shouldReport: true}, + {name: "Telemetry was disabled now is enabled", recordedTelemetryEnabled: "false", telemetryEnabled: true, shouldReport: false}, + {name: "Telemetry was disabled still disabled", recordedTelemetryEnabled: "false", telemetryEnabled: false, shouldReport: false}, + {name: "Telemetry was disabled still disabled, invalid value", recordedTelemetryEnabled: "invalid", telemetryEnabled: false, shouldReport: false}, + } { + testCase := testCase + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + db, _ := dbtestutil.NewDB(t) + ctx := testutil.Context(t, testutil.WaitMedium) + logger := testutil.Logger(t) + if testCase.recordedTelemetryEnabled != "nil" { + db.UpsertTelemetryItem(ctx, database.UpsertTelemetryItemParams{ + Key: string(telemetry.TelemetryItemKeyTelemetryEnabled), + Value: testCase.recordedTelemetryEnabled, + }) + } + snapshot1, err := telemetry.RecordTelemetryStatus(ctx, logger, db, testCase.telemetryEnabled) + require.NoError(t, err) + + if testCase.shouldReport { + require.NotNil(t, snapshot1) + require.Equal(t, snapshot1.TelemetryItems[0].Key, string(telemetry.TelemetryItemKeyTelemetryEnabled)) + require.Equal(t, snapshot1.TelemetryItems[0].Value, "false") + } else { + require.Nil(t, snapshot1) + } + + for i := 0; i < 3; i++ { + // Whatever happens, subsequent calls should not report if telemetryEnabled didn't change + snapshot2, err := telemetry.RecordTelemetryStatus(ctx, logger, db, testCase.telemetryEnabled) + require.NoError(t, err) + require.Nil(t, snapshot2) + } + }) + } +} + +func mockTelemetryServer(t *testing.T) (*url.URL, chan *telemetry.Deployment, chan *telemetry.Snapshot) { t.Helper() deployment := make(chan *telemetry.Deployment, 64) snapshot := make(chan *telemetry.Snapshot, 64) r := chi.NewRouter() r.Post("/deployment", func(w http.ResponseWriter, r *http.Request) { require.Equal(t, buildinfo.Version(), r.Header.Get(telemetry.VersionHeader)) - w.WriteHeader(http.StatusAccepted) dd := &telemetry.Deployment{} err := json.NewDecoder(r.Body).Decode(dd) require.NoError(t, err) deployment <- dd + // Ensure the header is sent only after deployment is sent + w.WriteHeader(http.StatusAccepted) }) r.Post("/snapshot", func(w http.ResponseWriter, r *http.Request) { require.Equal(t, buildinfo.Version(), r.Header.Get(telemetry.VersionHeader)) - w.WriteHeader(http.StatusAccepted) ss := &telemetry.Snapshot{} err := json.NewDecoder(r.Body).Decode(ss) require.NoError(t, err) snapshot <- ss + // Ensure the header is sent only after snapshot is sent + w.WriteHeader(http.StatusAccepted) }) server := httptest.NewServer(r) t.Cleanup(server.Close) serverURL, err := url.Parse(server.URL) require.NoError(t, err) + + return serverURL, deployment, snapshot +} + +func collectSnapshot(t *testing.T, db database.Store, addOptionsFn func(opts telemetry.Options) telemetry.Options) (*telemetry.Deployment, *telemetry.Snapshot) { + t.Helper() + + serverURL, deployment, snapshot := mockTelemetryServer(t) + options := telemetry.Options{ Database: db, Logger: testutil.Logger(t), diff --git a/coderd/templateversions_test.go b/coderd/templateversions_test.go index 9fd1bf6e2d830..4e3e3d2f7f2b0 100644 --- a/coderd/templateversions_test.go +++ b/coderd/templateversions_test.go @@ -829,6 +829,7 @@ func TestTemplateVersionResources(t *testing.T) { Type: "example", Agents: []*proto.Agent{{ Id: "something", + Name: "dev", Auth: &proto.Agent_Token{}, }}, }, { @@ -875,7 +876,8 @@ func TestTemplateVersionLogs(t *testing.T) { Name: "some", Type: "example", Agents: []*proto.Agent{{ - Id: "something", + Id: "something", + Name: "dev", Auth: &proto.Agent_Token{ Token: uuid.NewString(), }, diff --git a/coderd/userauth.go b/coderd/userauth.go index c5e95e44998b2..3c1481b1f9039 100644 --- a/coderd/userauth.go +++ b/coderd/userauth.go @@ -27,6 +27,7 @@ import ( "github.com/coder/coder/v2/coderd/cryptokeys" "github.com/coder/coder/v2/coderd/idpsync" "github.com/coder/coder/v2/coderd/jwtutils" + "github.com/coder/coder/v2/coderd/telemetry" "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/coderd/apikey" @@ -46,6 +47,14 @@ import ( "github.com/coder/coder/v2/cryptorand" ) +type MergedClaimsSource string + +var ( + MergedClaimsSourceNone MergedClaimsSource = "none" + MergedClaimsSourceUserInfo MergedClaimsSource = "user_info" + MergedClaimsSourceAccessToken MergedClaimsSource = "access_token" +) + const ( userAuthLoggerName = "userauth" OAuthConvertCookieValue = "coder_oauth_convert_jwt" @@ -748,10 +757,32 @@ type GithubOAuth2Config struct { ListOrganizationMemberships func(ctx context.Context, client *http.Client) ([]*github.Membership, error) TeamMembership func(ctx context.Context, client *http.Client, org, team, username string) (*github.Membership, error) + DeviceFlowEnabled bool + ExchangeDeviceCode func(ctx context.Context, deviceCode string) (*oauth2.Token, error) + AuthorizeDevice func(ctx context.Context) (*codersdk.ExternalAuthDevice, error) + AllowSignups bool AllowEveryone bool AllowOrganizations []string AllowTeams []GithubOAuth2Team + + DefaultProviderConfigured bool +} + +func (c *GithubOAuth2Config) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) { + if !c.DeviceFlowEnabled { + return c.OAuth2Config.Exchange(ctx, code, opts...) + } + return c.ExchangeDeviceCode(ctx, code) +} + +func (c *GithubOAuth2Config) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string { + if !c.DeviceFlowEnabled { + return c.OAuth2Config.AuthCodeURL(state, opts...) + } + // This is an absolute path in the Coder app. The device flow is orchestrated + // by the Coder frontend, so we need to redirect the user to the device flow page. + return "/login/device?state=" + state } // @Summary Get authentication methods @@ -777,7 +808,10 @@ func (api *API) userAuthMethods(rw http.ResponseWriter, r *http.Request) { Password: codersdk.AuthMethod{ Enabled: !api.DeploymentValues.DisablePasswordAuth.Value(), }, - Github: codersdk.AuthMethod{Enabled: api.GithubOAuth2Config != nil}, + Github: codersdk.GithubAuthMethod{ + Enabled: api.GithubOAuth2Config != nil, + DefaultProviderConfigured: api.GithubOAuth2Config != nil && api.GithubOAuth2Config.DefaultProviderConfigured, + }, OIDC: codersdk.OIDCAuthMethod{ AuthMethod: codersdk.AuthMethod{Enabled: api.OIDCConfig != nil}, SignInText: signInText, @@ -786,6 +820,53 @@ func (api *API) userAuthMethods(rw http.ResponseWriter, r *http.Request) { }) } +// @Summary Get Github device auth. +// @ID get-github-device-auth +// @Security CoderSessionToken +// @Produce json +// @Tags Users +// @Success 200 {object} codersdk.ExternalAuthDevice +// @Router /users/oauth2/github/device [get] +func (api *API) userOAuth2GithubDevice(rw http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + auditor = api.Auditor.Load() + aReq, commitAudit = audit.InitRequest[database.APIKey](rw, &audit.RequestParams{ + Audit: *auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionLogin, + }) + ) + aReq.Old = database.APIKey{} + defer commitAudit() + + if api.GithubOAuth2Config == nil { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Github OAuth2 is not enabled.", + }) + return + } + + if !api.GithubOAuth2Config.DeviceFlowEnabled { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Device flow is not enabled for Github OAuth2.", + }) + return + } + + deviceAuth, err := api.GithubOAuth2Config.AuthorizeDevice(ctx) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Failed to authorize device.", + Detail: err.Error(), + }) + return + } + + httpapi.Write(ctx, rw, http.StatusOK, deviceAuth) +} + // @Summary OAuth 2.0 GitHub Callback // @ID oauth-20-github-callback // @Security CoderSessionToken @@ -841,7 +922,17 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) { } } if len(selectedMemberships) == 0 { - httpmw.CustomRedirectToLogin(rw, r, redirect, "You aren't a member of the authorized Github organizations!", http.StatusUnauthorized) + status := http.StatusUnauthorized + msg := "You aren't a member of the authorized Github organizations!" + if api.GithubOAuth2Config.DeviceFlowEnabled { + // In the device flow, the error is rendered client-side. + httpapi.Write(ctx, rw, status, codersdk.Response{ + Message: "Unauthorized", + Detail: msg, + }) + } else { + httpmw.CustomRedirectToLogin(rw, r, redirect, msg, status) + } return } } @@ -878,7 +969,17 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) { } } if allowedTeam == nil { - httpmw.CustomRedirectToLogin(rw, r, redirect, fmt.Sprintf("You aren't a member of an authorized team in the %v Github organization(s)!", organizationNames), http.StatusUnauthorized) + msg := fmt.Sprintf("You aren't a member of an authorized team in the %v Github organization(s)!", organizationNames) + status := http.StatusUnauthorized + if api.GithubOAuth2Config.DeviceFlowEnabled { + // In the device flow, the error is rendered client-side. + httpapi.Write(ctx, rw, status, codersdk.Response{ + Message: "Unauthorized", + Detail: msg, + }) + } else { + httpmw.CustomRedirectToLogin(rw, r, redirect, msg, status) + } return } } @@ -979,6 +1080,10 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) { defer params.CommitAuditLogs() if err != nil { if httpErr := idpsync.IsHTTPError(err); httpErr != nil { + // In the device flow, the error page is rendered client-side. + if api.GithubOAuth2Config.DeviceFlowEnabled && httpErr.RenderStaticPage { + httpErr.RenderStaticPage = false + } httpErr.Write(rw, r) return } @@ -1016,7 +1121,14 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) { } redirect = uriFromURL(redirect) - http.Redirect(rw, r, redirect, http.StatusTemporaryRedirect) + if api.GithubOAuth2Config.DeviceFlowEnabled { + // In the device flow, the redirect is handled client-side. + httpapi.Write(ctx, rw, http.StatusOK, codersdk.OAuth2DeviceFlowCallbackResponse{ + RedirectURL: redirect, + }) + } else { + http.Redirect(rw, r, redirect, http.StatusTemporaryRedirect) + } } type OIDCConfig struct { @@ -1042,11 +1154,13 @@ type OIDCConfig struct { // AuthURLParams are additional parameters to be passed to the OIDC provider // when requesting an access token. AuthURLParams map[string]string - // IgnoreUserInfo causes Coder to only use claims from the ID token to - // process OIDC logins. This is useful if the OIDC provider does not - // support the userinfo endpoint, or if the userinfo endpoint causes - // undesirable behavior. - IgnoreUserInfo bool + // SecondaryClaims indicates where to source additional claim information from. + // The standard is either 'MergedClaimsSourceNone' or 'MergedClaimsSourceUserInfo'. + // + // The OIDC compliant way is to use the userinfo endpoint. This option + // is useful when the userinfo endpoint does not exist or causes undesirable + // behavior. + SecondaryClaims MergedClaimsSource // SignInText is the text to display on the OIDC login button SignInText string // IconURL points to the URL of an icon to display on the OIDC login button @@ -1112,6 +1226,20 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) { return } + if idToken.Subject == "" { + logger.Error(ctx, "oauth2: missing 'sub' claim field in OIDC token", + slog.F("source", "id_token"), + slog.F("claim_fields", claimFields(idtokenClaims)), + slog.F("blank", blankFields(idtokenClaims)), + ) + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "OIDC token missing 'sub' claim field or 'sub' claim field is empty.", + Detail: "'sub' claim field is required to be unique for all users by a given issue, " + + "an empty field is invalid and this authentication attempt is rejected.", + }) + return + } + logger.Debug(ctx, "got oidc claims", slog.F("source", "id_token"), slog.F("claim_fields", claimFields(idtokenClaims)), @@ -1128,50 +1256,39 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) { // Some providers (e.g. ADFS) do not support custom OIDC claims in the // UserInfo endpoint, so we allow users to disable it and only rely on the // ID token. - userInfoClaims := make(map[string]interface{}) + // // If user info is skipped, the idtokenClaims are the claims. mergedClaims := idtokenClaims - if !api.OIDCConfig.IgnoreUserInfo { - userInfo, err := api.OIDCConfig.Provider.UserInfo(ctx, oauth2.StaticTokenSource(state.Token)) - if err == nil { - err = userInfo.Claims(&userInfoClaims) - if err != nil { - logger.Error(ctx, "oauth2: unable to unmarshal user info claims", slog.Error(err)) - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to unmarshal user info claims.", - Detail: err.Error(), - }) - return - } - logger.Debug(ctx, "got oidc claims", - slog.F("source", "userinfo"), - slog.F("claim_fields", claimFields(userInfoClaims)), - slog.F("blank", blankFields(userInfoClaims)), - ) - - // Merge the claims from the ID token and the UserInfo endpoint. - // Information from UserInfo takes precedence. - mergedClaims = mergeClaims(idtokenClaims, userInfoClaims) + supplementaryClaims := make(map[string]interface{}) + switch api.OIDCConfig.SecondaryClaims { + case MergedClaimsSourceUserInfo: + supplementaryClaims, ok = api.userInfoClaims(ctx, rw, state, logger) + if !ok { + return + } - // Log all of the field names after merging. - logger.Debug(ctx, "got oidc claims", - slog.F("source", "merged"), - slog.F("claim_fields", claimFields(mergedClaims)), - slog.F("blank", blankFields(mergedClaims)), - ) - } else if !strings.Contains(err.Error(), "user info endpoint is not supported by this provider") { - logger.Error(ctx, "oauth2: unable to obtain user information claims", slog.Error(err)) - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to obtain user information claims.", - Detail: "The attempt to fetch claims via the UserInfo endpoint failed: " + err.Error(), - }) + // The precedence ordering is userInfoClaims > idTokenClaims. + // Note: Unsure why exactly this is the case. idTokenClaims feels more + // important? + mergedClaims = mergeClaims(idtokenClaims, supplementaryClaims) + case MergedClaimsSourceAccessToken: + supplementaryClaims, ok = api.accessTokenClaims(ctx, rw, state, logger) + if !ok { return - } else { - // The OIDC provider does not support the UserInfo endpoint. - // This is not an error, but we should log it as it may mean - // that some claims are missing. - logger.Warn(ctx, "OIDC provider does not support the user info endpoint, ensure that all required claims are present in the id_token") } + // idTokenClaims take priority over accessTokenClaims. The order should + // not matter. It is just safer to assume idTokenClaims is the truth, + // and accessTokenClaims are supplemental. + mergedClaims = mergeClaims(supplementaryClaims, idtokenClaims) + case MergedClaimsSourceNone: + // noop, keep the userInfoClaims empty + default: + // This should never happen and is a developer error + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Invalid source for secondary user claims.", + Detail: fmt.Sprintf("invalid source: %q", api.OIDCConfig.SecondaryClaims), + }) + return // Invalid MergedClaimsSource } usernameRaw, ok := mergedClaims[api.OIDCConfig.UsernameField] @@ -1325,7 +1442,7 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) { RoleSync: roleSync, UserClaims: database.UserLinkClaims{ IDTokenClaims: idtokenClaims, - UserInfoClaims: userInfoClaims, + UserInfoClaims: supplementaryClaims, MergedClaims: mergedClaims, }, }).SetInitAuditRequest(func(params *audit.RequestParams) (*audit.Request[database.User], func()) { @@ -1359,6 +1476,68 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) { http.Redirect(rw, r, redirect, http.StatusTemporaryRedirect) } +func (api *API) accessTokenClaims(ctx context.Context, rw http.ResponseWriter, state httpmw.OAuth2State, logger slog.Logger) (accessTokenClaims map[string]interface{}, ok bool) { + // Assume the access token is a jwt, and signed by the provider. + accessToken, err := api.OIDCConfig.Verifier.Verify(ctx, state.Token.AccessToken) + if err != nil { + logger.Error(ctx, "oauth2: unable to verify access token as secondary claims source", slog.Error(err)) + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Failed to verify access token.", + Detail: fmt.Sprintf("sourcing secondary claims from access token: %s", err.Error()), + }) + return nil, false + } + + rawClaims := make(map[string]any) + err = accessToken.Claims(&rawClaims) + if err != nil { + logger.Error(ctx, "oauth2: unable to unmarshal access token claims", slog.Error(err)) + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Failed to unmarshal access token claims.", + Detail: err.Error(), + }) + return nil, false + } + + return rawClaims, true +} + +func (api *API) userInfoClaims(ctx context.Context, rw http.ResponseWriter, state httpmw.OAuth2State, logger slog.Logger) (userInfoClaims map[string]interface{}, ok bool) { + userInfoClaims = make(map[string]interface{}) + userInfo, err := api.OIDCConfig.Provider.UserInfo(ctx, oauth2.StaticTokenSource(state.Token)) + if err == nil { + err = userInfo.Claims(&userInfoClaims) + if err != nil { + logger.Error(ctx, "oauth2: unable to unmarshal user info claims", slog.Error(err)) + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Failed to unmarshal user info claims.", + Detail: err.Error(), + }) + return nil, false + } + logger.Debug(ctx, "got oidc claims", + slog.F("source", "userinfo"), + slog.F("claim_fields", claimFields(userInfoClaims)), + slog.F("blank", blankFields(userInfoClaims)), + ) + } else if !strings.Contains(err.Error(), "user info endpoint is not supported by this provider") { + logger.Error(ctx, "oauth2: unable to obtain user information claims", slog.Error(err)) + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Failed to obtain user information claims.", + Detail: "The attempt to fetch claims via the UserInfo endpoint failed: " + err.Error(), + }) + return nil, false + } else { + // The OIDC provider does not support the UserInfo endpoint. + // This is not an error, but we should log it as it may mean + // that some claims are missing. + logger.Warn(ctx, "OIDC provider does not support the user info endpoint, ensure that all required claims are present in the id_token", + slog.Error(err), + ) + } + return userInfoClaims, true +} + // claimFields returns the sorted list of fields in the claims map. func claimFields(claims map[string]interface{}) []string { fields := []string{} @@ -1485,7 +1664,17 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C isConvertLoginType = true } - if user.ID == uuid.Nil && !params.AllowSignups { + // nolint:gocritic // Getting user count is a system function. + userCount, err := tx.GetUserCount(dbauthz.AsSystemRestricted(ctx)) + if err != nil { + return xerrors.Errorf("unable to fetch user count: %w", err) + } + + // Allow the first user to sign up with OIDC, regardless of + // whether signups are enabled or not. + allowSignup := userCount == 0 || params.AllowSignups + + if user.ID == uuid.Nil && !allowSignup { signupsDisabledText := "Please contact your Coder administrator to request access." if api.OIDCConfig != nil && api.OIDCConfig.SignupsDisabledText != "" { signupsDisabledText = render.HTMLFromMarkdown(api.OIDCConfig.SignupsDisabledText) @@ -1546,6 +1735,12 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C return xerrors.Errorf("unable to fetch default organization: %w", err) } + rbacRoles := []string{} + // If this is the first user, add the owner role. + if userCount == 0 { + rbacRoles = append(rbacRoles, rbac.RoleOwner().String()) + } + //nolint:gocritic user, err = api.CreateUser(dbauthz.AsSystemRestricted(ctx), tx, CreateUserRequest{ CreateUserRequestWithOrgs: codersdk.CreateUserRequestWithOrgs{ @@ -1560,10 +1755,20 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C }, LoginType: params.LoginType, accountCreatorName: "oauth", + RBACRoles: rbacRoles, }) if err != nil { return xerrors.Errorf("create user: %w", err) } + + if userCount == 0 { + telemetryUser := telemetry.ConvertUser(user) + // The email is not anonymized for the first user. + telemetryUser.Email = &user.Email + api.Telemetry.Report(&telemetry.Snapshot{ + Users: []telemetry.User{telemetryUser}, + }) + } } // Activate dormant user on sign-in diff --git a/coderd/userauth_test.go b/coderd/userauth_test.go index f0668507e38ba..ee6ee957ba861 100644 --- a/coderd/userauth_test.go +++ b/coderd/userauth_test.go @@ -22,6 +22,8 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uber.org/atomic" + "golang.org/x/oauth2" "golang.org/x/xerrors" "cdr.dev/slog" @@ -60,7 +62,7 @@ func TestOIDCOauthLoginWithExisting(t *testing.T) { cfg := fake.OIDCConfig(t, nil, func(cfg *coderd.OIDCConfig) { cfg.AllowSignups = true - cfg.IgnoreUserInfo = true + cfg.SecondaryClaims = coderd.MergedClaimsSourceNone }) client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{ @@ -72,6 +74,7 @@ func TestOIDCOauthLoginWithExisting(t *testing.T) { "email": "alice@coder.com", "email_verified": true, "preferred_username": username, + "sub": uuid.NewString(), } helper := oidctest.NewLoginHelper(client, fake) @@ -252,11 +255,20 @@ func TestUserOAuth2Github(t *testing.T) { }) t.Run("BlockSignups", func(t *testing.T) { t.Parallel() + + db, ps := dbtestutil.NewDB(t) + + id := atomic.NewInt64(100) + login := atomic.NewString("testuser") + email := atomic.NewString("testuser@coder.com") + client := coderdtest.New(t, &coderdtest.Options{ + Database: db, + Pubsub: ps, GithubOAuth2Config: &coderd.GithubOAuth2Config{ OAuth2Config: &testutil.OAuth2Config{}, AllowOrganizations: []string{"coder"}, - ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) { + ListOrganizationMemberships: func(_ context.Context, _ *http.Client) ([]*github.Membership, error) { return []*github.Membership{{ State: &stateActive, Organization: &github.Organization{ @@ -264,16 +276,19 @@ func TestUserOAuth2Github(t *testing.T) { }, }}, nil }, - AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) { + AuthenticatedUser: func(_ context.Context, _ *http.Client) (*github.User, error) { + id := id.Load() + login := login.Load() return &github.User{ - ID: github.Int64(100), - Login: github.String("testuser"), + ID: &id, + Login: &login, Name: github.String("The Right Honorable Sir Test McUser"), }, nil }, - ListEmails: func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error) { + ListEmails: func(_ context.Context, _ *http.Client) ([]*github.UserEmail, error) { + email := email.Load() return []*github.UserEmail{{ - Email: github.String("testuser@coder.com"), + Email: &email, Verified: github.Bool(true), Primary: github.Bool(true), }}, nil @@ -281,8 +296,23 @@ func TestUserOAuth2Github(t *testing.T) { }, }) + // The first user in a deployment with signups disabled will be allowed to sign up, + // but all the other users will not. resp := oauth2Callback(t, client) + require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) + + ctx := testutil.Context(t, testutil.WaitLong) + + // nolint:gocritic // Unit test + count, err := db.GetUserCount(dbauthz.AsSystemRestricted(ctx)) + require.NoError(t, err) + require.Equal(t, int64(1), count) + id.Store(101) + email.Store("someotheruser@coder.com") + login.Store("someotheruser") + + resp = oauth2Callback(t, client) require.Equal(t, http.StatusForbidden, resp.StatusCode) }) t.Run("MultiLoginNotAllowed", func(t *testing.T) { @@ -881,6 +911,92 @@ func TestUserOAuth2Github(t *testing.T) { require.Equal(t, user.ID, userID, "user_id is different, a new user was likely created") require.Equal(t, user.Email, newEmail) }) + t.Run("DeviceFlow", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{ + GithubOAuth2Config: &coderd.GithubOAuth2Config{ + OAuth2Config: &testutil.OAuth2Config{}, + AllowOrganizations: []string{"coder"}, + AllowSignups: true, + ListOrganizationMemberships: func(_ context.Context, _ *http.Client) ([]*github.Membership, error) { + return []*github.Membership{{ + State: &stateActive, + Organization: &github.Organization{ + Login: github.String("coder"), + }, + }}, nil + }, + AuthenticatedUser: func(_ context.Context, _ *http.Client) (*github.User, error) { + return &github.User{ + ID: github.Int64(100), + Login: github.String("testuser"), + Name: github.String("The Right Honorable Sir Test McUser"), + }, nil + }, + ListEmails: func(_ context.Context, _ *http.Client) ([]*github.UserEmail, error) { + return []*github.UserEmail{{ + Email: github.String("testuser@coder.com"), + Verified: github.Bool(true), + Primary: github.Bool(true), + }}, nil + }, + DeviceFlowEnabled: true, + ExchangeDeviceCode: func(_ context.Context, _ string) (*oauth2.Token, error) { + return &oauth2.Token{ + AccessToken: "access_token", + RefreshToken: "refresh_token", + Expiry: time.Now().Add(time.Hour), + }, nil + }, + AuthorizeDevice: func(_ context.Context) (*codersdk.ExternalAuthDevice, error) { + return &codersdk.ExternalAuthDevice{ + DeviceCode: "device_code", + UserCode: "user_code", + }, nil + }, + }, + }) + client.HTTPClient.CheckRedirect = func(*http.Request, []*http.Request) error { + return http.ErrUseLastResponse + } + + // Ensure that we redirect to the device login page when the user is not logged in. + oauthURL, err := client.URL.Parse("/api/v2/users/oauth2/github/callback") + require.NoError(t, err) + + req, err := http.NewRequestWithContext(context.Background(), "GET", oauthURL.String(), nil) + + require.NoError(t, err) + res, err := client.HTTPClient.Do(req) + require.NoError(t, err) + defer res.Body.Close() + + require.Equal(t, http.StatusTemporaryRedirect, res.StatusCode) + location, err := res.Location() + require.NoError(t, err) + require.Equal(t, "/login/device", location.Path) + query := location.Query() + require.NotEmpty(t, query.Get("state")) + + // Ensure that we return a JSON response when the code is successfully exchanged. + oauthURL, err = client.URL.Parse("/api/v2/users/oauth2/github/callback?code=hey&state=somestate") + require.NoError(t, err) + + req, err = http.NewRequestWithContext(context.Background(), "GET", oauthURL.String(), nil) + req.AddCookie(&http.Cookie{ + Name: "oauth_state", + Value: "somestate", + }) + require.NoError(t, err) + res, err = client.HTTPClient.Do(req) + require.NoError(t, err) + defer res.Body.Close() + + require.Equal(t, http.StatusOK, res.StatusCode) + var resp codersdk.OAuth2DeviceFlowCallbackResponse + require.NoError(t, json.NewDecoder(res.Body).Decode(&resp)) + require.Equal(t, "/", resp.RedirectURL) + }) } // nolint:bodyclose @@ -891,6 +1007,7 @@ func TestUserOIDC(t *testing.T) { Name string IDTokenClaims jwt.MapClaims UserInfoClaims jwt.MapClaims + AccessTokenClaims jwt.MapClaims AllowSignups bool EmailDomain []string AssertUser func(t testing.TB, u codersdk.User) @@ -898,11 +1015,48 @@ func TestUserOIDC(t *testing.T) { AssertResponse func(t testing.TB, resp *http.Response) IgnoreEmailVerified bool IgnoreUserInfo bool + UseAccessToken bool + PrecreateFirstUser bool }{ + { + Name: "NoSub", + IDTokenClaims: jwt.MapClaims{ + "email": "kyle@kwc.io", + }, + AllowSignups: true, + StatusCode: http.StatusBadRequest, + }, + { + Name: "AccessTokenMerge", + IDTokenClaims: jwt.MapClaims{ + "sub": uuid.NewString(), + }, + AccessTokenClaims: jwt.MapClaims{ + "email": "kyle@kwc.io", + }, + IgnoreUserInfo: true, + AllowSignups: true, + UseAccessToken: true, + StatusCode: http.StatusOK, + AssertUser: func(t testing.TB, u codersdk.User) { + assert.Equal(t, "kyle@kwc.io", u.Email) + }, + }, + { + Name: "AccessTokenMergeNotJWT", + IDTokenClaims: jwt.MapClaims{ + "sub": uuid.NewString(), + }, + IgnoreUserInfo: true, + AllowSignups: true, + UseAccessToken: true, + StatusCode: http.StatusBadRequest, + }, { Name: "EmailOnly", IDTokenClaims: jwt.MapClaims{ "email": "kyle@kwc.io", + "sub": uuid.NewString(), }, AllowSignups: true, StatusCode: http.StatusOK, @@ -915,6 +1069,7 @@ func TestUserOIDC(t *testing.T) { IDTokenClaims: jwt.MapClaims{ "email": "kyle@kwc.io", "email_verified": false, + "sub": uuid.NewString(), }, AllowSignups: true, StatusCode: http.StatusForbidden, @@ -924,6 +1079,7 @@ func TestUserOIDC(t *testing.T) { IDTokenClaims: jwt.MapClaims{ "email": 3.14159, "email_verified": false, + "sub": uuid.NewString(), }, AllowSignups: true, StatusCode: http.StatusBadRequest, @@ -933,6 +1089,7 @@ func TestUserOIDC(t *testing.T) { IDTokenClaims: jwt.MapClaims{ "email": "kyle@kwc.io", "email_verified": false, + "sub": uuid.NewString(), }, AllowSignups: true, StatusCode: http.StatusOK, @@ -946,6 +1103,7 @@ func TestUserOIDC(t *testing.T) { IDTokenClaims: jwt.MapClaims{ "email": "kyle@kwc.io", "email_verified": true, + "sub": uuid.NewString(), }, AllowSignups: true, EmailDomain: []string{ @@ -958,6 +1116,7 @@ func TestUserOIDC(t *testing.T) { IDTokenClaims: jwt.MapClaims{ "email": "cian@coder.com", "email_verified": true, + "sub": uuid.NewString(), }, AllowSignups: true, EmailDomain: []string{ @@ -970,6 +1129,7 @@ func TestUserOIDC(t *testing.T) { IDTokenClaims: jwt.MapClaims{ "email": "kyle@kwc.io", "email_verified": true, + "sub": uuid.NewString(), }, AllowSignups: true, EmailDomain: []string{ @@ -982,6 +1142,7 @@ func TestUserOIDC(t *testing.T) { IDTokenClaims: jwt.MapClaims{ "email": "kyle@KWC.io", "email_verified": true, + "sub": uuid.NewString(), }, AllowSignups: true, AssertUser: func(t testing.TB, u codersdk.User) { @@ -997,6 +1158,7 @@ func TestUserOIDC(t *testing.T) { IDTokenClaims: jwt.MapClaims{ "email": "colin@gmail.com", "email_verified": true, + "sub": uuid.NewString(), }, AllowSignups: true, EmailDomain: []string{ @@ -1015,14 +1177,26 @@ func TestUserOIDC(t *testing.T) { IDTokenClaims: jwt.MapClaims{ "email": "kyle@kwc.io", "email_verified": true, + "sub": uuid.NewString(), }, - StatusCode: http.StatusForbidden, + StatusCode: http.StatusForbidden, + PrecreateFirstUser: true, + }, + { + Name: "FirstSignup", + IDTokenClaims: jwt.MapClaims{ + "email": "kyle@kwc.io", + "email_verified": true, + "sub": uuid.NewString(), + }, + StatusCode: http.StatusOK, }, { Name: "UsernameFromEmail", IDTokenClaims: jwt.MapClaims{ "email": "kyle@kwc.io", "email_verified": true, + "sub": uuid.NewString(), }, AssertUser: func(t testing.TB, u codersdk.User) { assert.Equal(t, "kyle", u.Username) @@ -1036,6 +1210,7 @@ func TestUserOIDC(t *testing.T) { "email": "kyle@kwc.io", "email_verified": true, "preferred_username": "hotdog", + "sub": uuid.NewString(), }, AssertUser: func(t testing.TB, u codersdk.User) { assert.Equal(t, "hotdog", u.Username) @@ -1049,6 +1224,7 @@ func TestUserOIDC(t *testing.T) { "email": "kyle@kwc.io", "email_verified": true, "name": "Hot Dog", + "sub": uuid.NewString(), }, AssertUser: func(t testing.TB, u codersdk.User) { assert.Equal(t, "Hot Dog", u.Name) @@ -1065,6 +1241,7 @@ func TestUserOIDC(t *testing.T) { // However, we should not fail to log someone in if their name is too long. // Just truncate it. "name": strings.Repeat("a", 129), + "sub": uuid.NewString(), }, AllowSignups: true, StatusCode: http.StatusOK, @@ -1080,6 +1257,7 @@ func TestUserOIDC(t *testing.T) { // Full names must not have leading or trailing whitespace, but this is a // daft reason to fail a login. "name": " Bobby Whitespace ", + "sub": uuid.NewString(), }, AllowSignups: true, StatusCode: http.StatusOK, @@ -1096,6 +1274,7 @@ func TestUserOIDC(t *testing.T) { "email_verified": true, "name": "Kylium Carbonate", "preferred_username": "kyle@kwc.io", + "sub": uuid.NewString(), }, AssertUser: func(t testing.TB, u codersdk.User) { assert.Equal(t, "kyle", u.Username) @@ -1108,6 +1287,7 @@ func TestUserOIDC(t *testing.T) { Name: "UsernameIsEmail", IDTokenClaims: jwt.MapClaims{ "preferred_username": "kyle@kwc.io", + "sub": uuid.NewString(), }, AssertUser: func(t testing.TB, u codersdk.User) { assert.Equal(t, "kyle", u.Username) @@ -1123,6 +1303,7 @@ func TestUserOIDC(t *testing.T) { "email_verified": true, "preferred_username": "kyle", "picture": "/example.png", + "sub": uuid.NewString(), }, AssertUser: func(t testing.TB, u codersdk.User) { assert.Equal(t, "/example.png", u.AvatarURL) @@ -1136,6 +1317,7 @@ func TestUserOIDC(t *testing.T) { IDTokenClaims: jwt.MapClaims{ "email": "kyle@kwc.io", "email_verified": true, + "sub": uuid.NewString(), }, UserInfoClaims: jwt.MapClaims{ "preferred_username": "potato", @@ -1155,6 +1337,7 @@ func TestUserOIDC(t *testing.T) { IDTokenClaims: jwt.MapClaims{ "email": "coolin@coder.com", "groups": []string{"pingpong"}, + "sub": uuid.NewString(), }, AllowSignups: true, StatusCode: http.StatusOK, @@ -1164,6 +1347,7 @@ func TestUserOIDC(t *testing.T) { IDTokenClaims: jwt.MapClaims{ "email": "internaluser@internal.domain", "email_verified": false, + "sub": uuid.NewString(), }, UserInfoClaims: jwt.MapClaims{ "email": "externaluser@external.domain", @@ -1182,6 +1366,7 @@ func TestUserOIDC(t *testing.T) { IDTokenClaims: jwt.MapClaims{ "email": "internaluser@internal.domain", "email_verified": false, + "sub": uuid.NewString(), }, UserInfoClaims: jwt.MapClaims{ "email": 1, @@ -1197,6 +1382,7 @@ func TestUserOIDC(t *testing.T) { "email_verified": true, "name": "User McName", "preferred_username": "user", + "sub": uuid.NewString(), }, UserInfoClaims: jwt.MapClaims{ "email": "user.mcname@external.domain", @@ -1216,6 +1402,7 @@ func TestUserOIDC(t *testing.T) { IDTokenClaims: inflateClaims(t, jwt.MapClaims{ "email": "user@domain.tld", "email_verified": true, + "sub": uuid.NewString(), }, 65536), AssertUser: func(t testing.TB, u codersdk.User) { assert.Equal(t, "user", u.Username) @@ -1228,6 +1415,7 @@ func TestUserOIDC(t *testing.T) { IDTokenClaims: jwt.MapClaims{ "email": "user@domain.tld", "email_verified": true, + "sub": uuid.NewString(), }, UserInfoClaims: inflateClaims(t, jwt.MapClaims{}, 65536), AssertUser: func(t testing.TB, u codersdk.User) { @@ -1242,6 +1430,7 @@ func TestUserOIDC(t *testing.T) { "iss": "https://mismatch.com", "email": "user@domain.tld", "email_verified": true, + "sub": uuid.NewString(), }, AllowSignups: true, StatusCode: http.StatusBadRequest, @@ -1255,18 +1444,32 @@ func TestUserOIDC(t *testing.T) { tc := tc t.Run(tc.Name, func(t *testing.T) { t.Parallel() - fake := oidctest.NewFakeIDP(t, + opts := []oidctest.FakeIDPOpt{ oidctest.WithRefresh(func(_ string) error { return xerrors.New("refreshing token should never occur") }), oidctest.WithServing(), oidctest.WithStaticUserInfo(tc.UserInfoClaims), - ) + } + + if tc.AccessTokenClaims != nil && len(tc.AccessTokenClaims) > 0 { + opts = append(opts, oidctest.WithAccessTokenJWTHook(func(email string, exp time.Time) jwt.MapClaims { + return tc.AccessTokenClaims + })) + } + + fake := oidctest.NewFakeIDP(t, opts...) cfg := fake.OIDCConfig(t, nil, func(cfg *coderd.OIDCConfig) { cfg.AllowSignups = tc.AllowSignups cfg.EmailDomain = tc.EmailDomain cfg.IgnoreEmailVerified = tc.IgnoreEmailVerified - cfg.IgnoreUserInfo = tc.IgnoreUserInfo + cfg.SecondaryClaims = coderd.MergedClaimsSourceUserInfo + if tc.IgnoreUserInfo { + cfg.SecondaryClaims = coderd.MergedClaimsSourceNone + } + if tc.UseAccessToken { + cfg.SecondaryClaims = coderd.MergedClaimsSourceAccessToken + } cfg.NameField = "name" }) @@ -1279,6 +1482,15 @@ func TestUserOIDC(t *testing.T) { }) numLogs := len(auditor.AuditLogs()) + ctx := testutil.Context(t, testutil.WaitShort) + if tc.PrecreateFirstUser { + owner.CreateFirstUser(ctx, codersdk.CreateFirstUserRequest{ + Email: "precreated@coder.com", + Username: "precreated", + Password: "SomeSecurePassword!", + }) + } + client, resp := fake.AttemptLogin(t, owner, tc.IDTokenClaims) numLogs++ // add an audit log for login require.Equal(t, tc.StatusCode, resp.StatusCode) @@ -1286,8 +1498,6 @@ func TestUserOIDC(t *testing.T) { tc.AssertResponse(t, resp) } - ctx := testutil.Context(t, testutil.WaitShort) - if tc.AssertUser != nil { user, err := client.User(ctx, "me") require.NoError(t, err) @@ -1331,6 +1541,7 @@ func TestUserOIDC(t *testing.T) { client, resp := fake.AttemptLogin(t, owner, jwt.MapClaims{ "email": user.Email, + "sub": uuid.NewString(), }) require.Equal(t, http.StatusOK, resp.StatusCode) @@ -1369,6 +1580,7 @@ func TestUserOIDC(t *testing.T) { claims := jwt.MapClaims{ "email": userData.Email, + "sub": uuid.NewString(), } var err error user.HTTPClient.Jar, err = cookiejar.New(nil) @@ -1439,6 +1651,7 @@ func TestUserOIDC(t *testing.T) { claims := jwt.MapClaims{ "email": userData.Email, + "sub": uuid.NewString(), } user.HTTPClient.Jar, err = cookiejar.New(nil) require.NoError(t, err) @@ -1509,6 +1722,7 @@ func TestUserOIDC(t *testing.T) { numLogs := len(auditor.AuditLogs()) claims := jwt.MapClaims{ "email": "jon@coder.com", + "sub": uuid.NewString(), } userClient, _ := fake.Login(t, client, claims) @@ -1629,6 +1843,7 @@ func TestUserOIDC(t *testing.T) { claims := jwt.MapClaims{ "email": "user@example.com", "email_verified": true, + "sub": uuid.NewString(), } // Perform the login @@ -1794,6 +2009,7 @@ func TestOIDCSkipIssuer(t *testing.T) { userClient, _ := fake.Login(t, owner, jwt.MapClaims{ "iss": secondaryURLString, "email": "alice@coder.com", + "sub": uuid.NewString(), }) found, err := userClient.User(ctx, "me") require.NoError(t, err) diff --git a/coderd/users.go b/coderd/users.go index 56f295986859c..bf5b1db763fe9 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -118,6 +118,8 @@ func (api *API) firstUser(rw http.ResponseWriter, r *http.Request) { // @Success 201 {object} codersdk.CreateFirstUserResponse // @Router /users/first [post] func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) { + // The first user can also be created via oidc, so if making changes to the flow, + // ensure that the oidc flow is also updated. ctx := r.Context() var createUser codersdk.CreateFirstUserRequest if !httpapi.Read(ctx, rw, r, &createUser) { @@ -198,6 +200,7 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) { OrganizationIDs: []uuid.UUID{defaultOrg.ID}, }, LoginType: database.LoginTypePassword, + RBACRoles: []string{rbac.RoleOwner().String()}, accountCreatorName: "coder", }) if err != nil { @@ -225,23 +228,6 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) { Users: []telemetry.User{telemetryUser}, }) - // TODO: @emyrk this currently happens outside the database tx used to create - // the user. Maybe I add this ability to grant roles in the createUser api - // and add some rbac bypass when calling api functions this way?? - // Add the admin role to this first user. - //nolint:gocritic // needed to create first user - _, err = api.Database.UpdateUserRoles(dbauthz.AsSystemRestricted(ctx), database.UpdateUserRolesParams{ - GrantedRoles: []string{rbac.RoleOwner().String()}, - ID: user.ID, - }) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error updating user's roles.", - Detail: err.Error(), - }) - return - } - httpapi.Write(ctx, rw, http.StatusCreated, codersdk.CreateFirstUserResponse{ UserID: user.ID, OrganizationID: defaultOrg.ID, @@ -918,6 +904,7 @@ func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseW func (api *API) notifyUserStatusChanged(ctx context.Context, actingUserName string, targetUser database.User, status database.UserStatus) error { var labels map[string]string + var data map[string]any var adminTemplateID, personalTemplateID uuid.UUID switch status { case database.UserStatusSuspended: @@ -926,6 +913,9 @@ func (api *API) notifyUserStatusChanged(ctx context.Context, actingUserName stri "suspended_account_user_name": targetUser.Name, "initiator": actingUserName, } + data = map[string]any{ + "user": map[string]any{"id": targetUser.ID, "name": targetUser.Name, "email": targetUser.Email}, + } adminTemplateID = notifications.TemplateUserAccountSuspended personalTemplateID = notifications.TemplateYourAccountSuspended case database.UserStatusActive: @@ -934,6 +924,9 @@ func (api *API) notifyUserStatusChanged(ctx context.Context, actingUserName stri "activated_account_user_name": targetUser.Name, "initiator": actingUserName, } + data = map[string]any{ + "user": map[string]any{"id": targetUser.ID, "name": targetUser.Name, "email": targetUser.Email}, + } adminTemplateID = notifications.TemplateUserAccountActivated personalTemplateID = notifications.TemplateYourAccountActivated default: @@ -949,16 +942,16 @@ func (api *API) notifyUserStatusChanged(ctx context.Context, actingUserName stri // Send notifications to user admins and affected user for _, u := range userAdmins { // nolint:gocritic // Need notifier actor to enqueue notifications - if _, err := api.NotificationsEnqueuer.Enqueue(dbauthz.AsNotifier(ctx), u.ID, adminTemplateID, - labels, "api-put-user-status", + if _, err := api.NotificationsEnqueuer.EnqueueWithData(dbauthz.AsNotifier(ctx), u.ID, adminTemplateID, + labels, data, "api-put-user-status", targetUser.ID, ); err != nil { api.Logger.Warn(ctx, "unable to notify about changed user's status", slog.F("affected_user", targetUser.Username), slog.Error(err)) } } // nolint:gocritic // Need notifier actor to enqueue notifications - if _, err := api.NotificationsEnqueuer.Enqueue(dbauthz.AsNotifier(ctx), targetUser.ID, personalTemplateID, - labels, "api-put-user-status", + if _, err := api.NotificationsEnqueuer.EnqueueWithData(dbauthz.AsNotifier(ctx), targetUser.ID, personalTemplateID, + labels, data, "api-put-user-status", targetUser.ID, ); err != nil { api.Logger.Warn(ctx, "unable to notify user about status change of their account", slog.F("affected_user", targetUser.Username), slog.Error(err)) @@ -1279,7 +1272,10 @@ func (api *API) organizationsByUser(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() user := httpmw.UserParam(r) - organizations, err := api.Database.GetOrganizationsByUserID(ctx, user.ID) + organizations, err := api.Database.GetOrganizationsByUserID(ctx, database.GetOrganizationsByUserIDParams{ + UserID: user.ID, + Deleted: false, + }) if errors.Is(err, sql.ErrNoRows) { err = nil organizations = []database.Organization{} @@ -1317,7 +1313,10 @@ func (api *API) organizationsByUser(rw http.ResponseWriter, r *http.Request) { func (api *API) organizationByUserAndName(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() organizationName := chi.URLParam(r, "organizationname") - organization, err := api.Database.GetOrganizationByName(ctx, organizationName) + organization, err := api.Database.GetOrganizationByName(ctx, database.GetOrganizationByNameParams{ + Name: organizationName, + Deleted: false, + }) if httpapi.Is404Error(err) { httpapi.ResourceNotFound(rw) return @@ -1338,6 +1337,7 @@ type CreateUserRequest struct { LoginType database.LoginType SkipNotifications bool accountCreatorName string + RBACRoles []string } func (api *API) CreateUser(ctx context.Context, store database.Store, req CreateUserRequest) (database.User, error) { @@ -1347,6 +1347,13 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create return database.User{}, xerrors.Errorf("invalid username %q: %w", req.Username, usernameValid) } + // If the caller didn't specify rbac roles, default to + // a member of the site. + rbacRoles := []string{} + if req.RBACRoles != nil { + rbacRoles = req.RBACRoles + } + var user database.User err := store.InTx(func(tx database.Store) error { orgRoles := make([]string, 0) @@ -1363,10 +1370,9 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create CreatedAt: dbtime.Now(), UpdatedAt: dbtime.Now(), HashedPassword: []byte{}, - // All new users are defaulted to members of the site. - RBACRoles: []string{}, - LoginType: req.LoginType, - Status: status, + RBACRoles: rbacRoles, + LoginType: req.LoginType, + Status: status, } // If a user signs up with OAuth, they can have no password! if req.Password != "" { @@ -1424,13 +1430,24 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create } for _, u := range userAdmins { - // nolint:gocritic // Need notifier actor to enqueue notifications - if _, err := api.NotificationsEnqueuer.Enqueue(dbauthz.AsNotifier(ctx), u.ID, notifications.TemplateUserAccountCreated, + if u.ID == user.ID { + // If the new user is an admin, don't notify them about themselves. + continue + } + if _, err := api.NotificationsEnqueuer.EnqueueWithData( + // nolint:gocritic // Need notifier actor to enqueue notifications + dbauthz.AsNotifier(ctx), + u.ID, + notifications.TemplateUserAccountCreated, map[string]string{ "created_account_name": user.Username, "created_account_user_name": user.Name, "initiator": req.accountCreatorName, - }, "api-users-create", + }, + map[string]any{ + "user": map[string]any{"id": user.ID, "name": user.Name, "email": user.Email}, + }, + "api-users-create", user.ID, ); err != nil { api.Logger.Warn(ctx, "unable to notify about created user", slog.F("created_user", user.Username), slog.Error(err)) diff --git a/coderd/users_test.go b/coderd/users_test.go index 1386d76f3e0bf..74c27da7ef6f5 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -392,12 +392,19 @@ func TestNotifyUserStatusChanged(t *testing.T) { // Validate that each expected notification is present in notifyEnq.Sent() for _, expected := range expectedNotifications { found := false - for _, sent := range notifyEnq.Sent() { + for _, sent := range notifyEnq.Sent(notificationstest.WithTemplateID(expected.TemplateID)) { if sent.TemplateID == expected.TemplateID && sent.UserID == expected.UserID && slices.Contains(sent.Targets, member.ID) && sent.Labels[label] == member.Username { found = true + + require.IsType(t, map[string]any{}, sent.Data["user"]) + userData := sent.Data["user"].(map[string]any) + require.Equal(t, member.ID, userData["id"]) + require.Equal(t, member.Name, userData["name"]) + require.Equal(t, member.Email, userData["email"]) + break } } @@ -824,6 +831,7 @@ func TestPostUsers(t *testing.T) { // Try to log in with OIDC. userClient, _ := fake.Login(t, client, jwt.MapClaims{ "email": email, + "sub": uuid.NewString(), }) found, err := userClient.User(ctx, "me") @@ -858,11 +866,18 @@ func TestNotifyCreatedUser(t *testing.T) { require.NoError(t, err) // then - require.Len(t, notifyEnq.Sent(), 1) - require.Equal(t, notifications.TemplateUserAccountCreated, notifyEnq.Sent()[0].TemplateID) - require.Equal(t, firstUser.UserID, notifyEnq.Sent()[0].UserID) - require.Contains(t, notifyEnq.Sent()[0].Targets, user.ID) - require.Equal(t, user.Username, notifyEnq.Sent()[0].Labels["created_account_name"]) + sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateUserAccountCreated)) + require.Len(t, sent, 1) + require.Equal(t, notifications.TemplateUserAccountCreated, sent[0].TemplateID) + require.Equal(t, firstUser.UserID, sent[0].UserID) + require.Contains(t, sent[0].Targets, user.ID) + require.Equal(t, user.Username, sent[0].Labels["created_account_name"]) + + require.IsType(t, map[string]any{}, sent[0].Data["user"]) + userData := sent[0].Data["user"].(map[string]any) + require.Equal(t, user.ID, userData["id"]) + require.Equal(t, user.Name, userData["name"]) + require.Equal(t, user.Email, userData["email"]) }) t.Run("UserAdminNotified", func(t *testing.T) { diff --git a/coderd/util/maps/maps.go b/coderd/util/maps/maps.go new file mode 100644 index 0000000000000..8aaa6669cb8af --- /dev/null +++ b/coderd/util/maps/maps.go @@ -0,0 +1,32 @@ +package maps + +import ( + "sort" + + "golang.org/x/exp/constraints" +) + +// Subset returns true if all the keys of a are present +// in b and have the same values. +// If the corresponding value of a[k] is the zero value in +// b, Subset will skip comparing that value. +// This allows checking for the presence of map keys. +func Subset[T, U comparable](a, b map[T]U) bool { + var uz U + for ka, va := range a { + ignoreZeroValue := va == uz + if vb, ok := b[ka]; !ok || (!ignoreZeroValue && va != vb) { + return false + } + } + return true +} + +// SortedKeys returns the keys of m in sorted order. +func SortedKeys[T constraints.Ordered](m map[T]any) (keys []T) { + for k := range m { + keys = append(keys, k) + } + sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] }) + return keys +} diff --git a/coderd/util/maps/maps_test.go b/coderd/util/maps/maps_test.go new file mode 100644 index 0000000000000..543c100c210a5 --- /dev/null +++ b/coderd/util/maps/maps_test.go @@ -0,0 +1,83 @@ +package maps_test + +import ( + "strconv" + "testing" + + "github.com/coder/coder/v2/coderd/util/maps" +) + +func TestSubset(t *testing.T) { + t.Parallel() + + for idx, tc := range []struct { + a map[string]string + b map[string]string + // expected value from Subset + expected bool + }{ + { + a: nil, + b: nil, + expected: true, + }, + { + a: map[string]string{}, + b: map[string]string{}, + expected: true, + }, + { + a: map[string]string{"a": "1", "b": "2"}, + b: map[string]string{"a": "1", "b": "2"}, + expected: true, + }, + { + a: map[string]string{"a": "1", "b": "2"}, + b: map[string]string{"a": "1"}, + expected: false, + }, + { + a: map[string]string{"a": "1"}, + b: map[string]string{"a": "1", "b": "2"}, + expected: true, + }, + { + a: map[string]string{"a": "1", "b": "2"}, + b: map[string]string{}, + expected: false, + }, + { + a: map[string]string{"a": "1", "b": "2"}, + b: map[string]string{"a": "1", "b": "3"}, + expected: false, + }, + // Zero value + { + a: map[string]string{"a": "1", "b": ""}, + b: map[string]string{"a": "1", "b": "3"}, + expected: true, + }, + // Zero value, but the other way round + { + a: map[string]string{"a": "1", "b": "3"}, + b: map[string]string{"a": "1", "b": ""}, + expected: false, + }, + // Both zero values + { + a: map[string]string{"a": "1", "b": ""}, + b: map[string]string{"a": "1", "b": ""}, + expected: true, + }, + } { + tc := tc + t.Run("#"+strconv.Itoa(idx), func(t *testing.T) { + t.Parallel() + + actual := maps.Subset(tc.a, tc.b) + if actual != tc.expected { + t.Errorf("expected %v, got %v", tc.expected, actual) + } + }) + } +} diff --git a/coderd/util/slice/slice.go b/coderd/util/slice/slice.go index 2a62e23592d84..508827dfaae81 100644 --- a/coderd/util/slice/slice.go +++ b/coderd/util/slice/slice.go @@ -177,3 +177,19 @@ func DifferenceFunc[T any](a []T, b []T, equal func(a, b T) bool) []T { } return tmp } + +func CountConsecutive[T comparable](needle T, haystack ...T) int { + maxLength := 0 + curLength := 0 + + for _, v := range haystack { + if v == needle { + curLength++ + } else { + maxLength = max(maxLength, curLength) + curLength = 0 + } + } + + return max(maxLength, curLength) +} diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 026c3581ff14d..ddfb21a751671 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -34,6 +34,7 @@ import ( "github.com/coder/coder/v2/coderd/jwtutils" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" + maputil "github.com/coder/coder/v2/coderd/util/maps" "github.com/coder/coder/v2/coderd/wspubsub" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" @@ -678,6 +679,99 @@ func (api *API) workspaceAgentListeningPorts(rw http.ResponseWriter, r *http.Req httpapi.Write(ctx, rw, http.StatusOK, portsResponse) } +// @Summary Get running containers for workspace agent +// @ID get-running-containers-for-workspace-agent +// @Security CoderSessionToken +// @Produce json +// @Tags Agents +// @Param workspaceagent path string true "Workspace agent ID" format(uuid) +// @Param label query string true "Labels" format(key=value) +// @Success 200 {object} codersdk.WorkspaceAgentListContainersResponse +// @Router /workspaceagents/{workspaceagent}/containers [get] +func (api *API) workspaceAgentListContainers(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + workspaceAgent := httpmw.WorkspaceAgentParam(r) + + labelParam, ok := r.URL.Query()["label"] + if !ok { + labelParam = []string{} + } + labels := make(map[string]string, len(labelParam)/2) + for _, label := range labelParam { + kvs := strings.Split(label, "=") + if len(kvs) != 2 { + httpapi.Write(r.Context(), rw, http.StatusBadRequest, codersdk.Response{ + Message: "Invalid label format", + Detail: "Labels must be in the format key=value", + }) + return + } + labels[kvs[0]] = kvs[1] + } + + // If the agent is unreachable, the request will hang. Assume that if we + // don't get a response after 30s that the agent is unreachable. + ctx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + apiAgent, err := db2sdk.WorkspaceAgent( + api.DERPMap(), + *api.TailnetCoordinator.Load(), + workspaceAgent, + nil, + nil, + nil, + api.AgentInactiveDisconnectTimeout, + api.DeploymentValues.AgentFallbackTroubleshootingURL.String(), + ) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error reading workspace agent.", + Detail: err.Error(), + }) + return + } + if apiAgent.Status != codersdk.WorkspaceAgentConnected { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: fmt.Sprintf("Agent state is %q, it must be in the %q state.", apiAgent.Status, codersdk.WorkspaceAgentConnected), + }) + return + } + + agentConn, release, err := api.agentProvider.AgentConn(ctx, workspaceAgent.ID) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error dialing workspace agent.", + Detail: err.Error(), + }) + return + } + defer release() + + // Get a list of containers that the agent is able to detect + cts, err := agentConn.ListContainers(ctx) + if err != nil { + if errors.Is(err, context.Canceled) { + httpapi.Write(ctx, rw, http.StatusRequestTimeout, codersdk.Response{ + Message: "Failed to fetch containers from agent.", + Detail: "Request timed out.", + }) + return + } + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching containers.", + Detail: err.Error(), + }) + return + } + + // Filter in-place by labels + cts.Containers = slices.DeleteFunc(cts.Containers, func(ct codersdk.WorkspaceAgentDevcontainer) bool { + return !maputil.Subset(labels, ct.Labels) + }) + + httpapi.Write(ctx, rw, http.StatusOK, cts) +} + // @Summary Get connection info for workspace agent // @ID get-connection-info-for-workspace-agent // @Security CoderSessionToken @@ -812,6 +906,7 @@ func (api *API) workspaceAgentClientCoordinate(rw http.ResponseWriter, r *http.R } // This is used by Enterprise code to control the functionality of this route. + // Namely, disabling the route using `CODER_BROWSER_ONLY`. override := api.WorkspaceClientCoordinateOverride.Load() if override != nil { overrideFunc := *override @@ -1482,6 +1577,16 @@ func (api *API) workspaceAgentsExternalAuthListen(ctx context.Context, rw http.R func (api *API) tailnetRPCConn(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() + // This is used by Enterprise code to control the functionality of this route. + // Namely, disabling the route using `CODER_BROWSER_ONLY`. + override := api.WorkspaceClientCoordinateOverride.Load() + if override != nil { + overrideFunc := *override + if overrideFunc != nil && overrideFunc(rw) { + return + } + } + version := "2.0" qv := r.URL.Query().Get("version") if qv != "" { diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index c75b3f3ed53fc..69bba9d8baabd 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -7,6 +7,7 @@ import ( "maps" "net" "net/http" + "os" "runtime" "strconv" "strings" @@ -15,9 +16,13 @@ import ( "time" "github.com/go-jose/go-jose/v4/jwt" + "github.com/google/go-cmp/cmp" "github.com/google/uuid" + "github.com/ory/dockertest/v3" + "github.com/ory/dockertest/v3/docker" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" "golang.org/x/xerrors" "google.golang.org/protobuf/types/known/timestamppb" "tailscale.com/tailcfg" @@ -25,6 +30,9 @@ import ( "cdr.dev/slog" "cdr.dev/slog/sloggers/slogtest" "github.com/coder/coder/v2/agent" + "github.com/coder/coder/v2/agent/agentcontainers" + "github.com/coder/coder/v2/agent/agentcontainers/acmock" + "github.com/coder/coder/v2/agent/agentexec" "github.com/coder/coder/v2/agent/agenttest" agentproto "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/coderd/coderdtest" @@ -385,7 +393,8 @@ func TestWorkspaceAgentConnectRPC(t *testing.T) { Name: "example", Type: "aws_instance", Agents: []*proto.Agent{{ - Id: uuid.NewString(), + Id: uuid.NewString(), + Name: "dev", Auth: &proto.Agent_Token{ Token: uuid.NewString(), }, @@ -1053,6 +1062,191 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) { }) } +func TestWorkspaceAgentContainers(t *testing.T) { + t.Parallel() + + // This test will not normally run in CI, but is kept here as a semi-manual + // test for local development. Run it as follows: + // CODER_TEST_USE_DOCKER=1 go test -run TestWorkspaceAgentContainers/Docker ./coderd + t.Run("Docker", func(t *testing.T) { + t.Parallel() + if ctud, ok := os.LookupEnv("CODER_TEST_USE_DOCKER"); !ok || ctud != "1" { + t.Skip("Set CODER_TEST_USE_DOCKER=1 to run this test") + } + + pool, err := dockertest.NewPool("") + require.NoError(t, err, "Could not connect to docker") + testLabels := map[string]string{ + "com.coder.test": uuid.New().String(), + "com.coder.empty": "", + } + ct, err := pool.RunWithOptions(&dockertest.RunOptions{ + Repository: "busybox", + Tag: "latest", + Cmd: []string{"sleep", "infinity"}, + Labels: testLabels, + }, func(config *docker.HostConfig) { + config.AutoRemove = true + config.RestartPolicy = docker.RestartPolicy{Name: "no"} + }) + require.NoError(t, err, "Could not start test docker container") + t.Cleanup(func() { + assert.NoError(t, pool.Purge(ct), "Could not purge resource %q", ct.Container.Name) + }) + + // Start another container which we will expect to ignore. + ct2, err := pool.RunWithOptions(&dockertest.RunOptions{ + Repository: "busybox", + Tag: "latest", + Cmd: []string{"sleep", "infinity"}, + Labels: map[string]string{ + "com.coder.test": "ignoreme", + "com.coder.empty": "", + }, + }, func(config *docker.HostConfig) { + config.AutoRemove = true + config.RestartPolicy = docker.RestartPolicy{Name: "no"} + }) + require.NoError(t, err, "Could not start second test docker container") + t.Cleanup(func() { + assert.NoError(t, pool.Purge(ct2), "Could not purge resource %q", ct2.Container.Name) + }) + + client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{}) + + user := coderdtest.CreateFirstUser(t, client) + r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OrganizationID: user.OrganizationID, + OwnerID: user.UserID, + }).WithAgent(func(agents []*proto.Agent) []*proto.Agent { + return agents + }).Do() + _ = agenttest.New(t, client.URL, r.AgentToken, func(opts *agent.Options) { + opts.ContainerLister = agentcontainers.NewDocker(agentexec.DefaultExecer) + }) + resources := coderdtest.NewWorkspaceAgentWaiter(t, client, r.Workspace.ID).Wait() + require.Len(t, resources, 1, "expected one resource") + require.Len(t, resources[0].Agents, 1, "expected one agent") + agentID := resources[0].Agents[0].ID + + ctx := testutil.Context(t, testutil.WaitLong) + + // If we filter by testLabels, we should only get one container back. + res, err := client.WorkspaceAgentListContainers(ctx, agentID, testLabels) + require.NoError(t, err, "failed to list containers filtered by test label") + require.Len(t, res.Containers, 1, "expected exactly one container") + assert.Equal(t, ct.Container.ID, res.Containers[0].ID, "expected container ID to match") + assert.Equal(t, "busybox:latest", res.Containers[0].Image, "expected container image to match") + assert.Equal(t, ct.Container.Config.Labels, res.Containers[0].Labels, "expected container labels to match") + assert.Equal(t, strings.TrimPrefix(ct.Container.Name, "/"), res.Containers[0].FriendlyName, "expected container name to match") + assert.True(t, res.Containers[0].Running, "expected container to be running") + assert.Equal(t, "running", res.Containers[0].Status, "expected container status to be running") + + // List all containers and ensure we get at least both (there may be more). + res, err = client.WorkspaceAgentListContainers(ctx, agentID, nil) + require.NoError(t, err, "failed to list all containers") + require.NotEmpty(t, res.Containers, "expected to find containers") + var found []string + for _, c := range res.Containers { + found = append(found, c.ID) + } + require.Contains(t, found, ct.Container.ID, "expected to find first container without label filter") + require.Contains(t, found, ct2.Container.ID, "expected to find first container without label filter") + }) + + // This test will normally run in CI. It uses a mock implementation of + // agentcontainers.Lister instead of introducing a hard dependency on Docker. + t.Run("Mock", func(t *testing.T) { + t.Parallel() + + // begin test fixtures + testLabels := map[string]string{ + "com.coder.test": uuid.New().String(), + } + testResponse := codersdk.WorkspaceAgentListContainersResponse{ + Containers: []codersdk.WorkspaceAgentDevcontainer{ + { + ID: uuid.NewString(), + CreatedAt: dbtime.Now(), + FriendlyName: testutil.GetRandomName(t), + Image: "busybox:latest", + Labels: testLabels, + Running: true, + Status: "running", + Ports: []codersdk.WorkspaceAgentListeningPort{ + { + Network: "tcp", + Port: 80, + }, + }, + Volumes: map[string]string{ + "/host": "/container", + }, + }, + }, + } + // end test fixtures + + for _, tc := range []struct { + name string + setupMock func(*acmock.MockLister) (codersdk.WorkspaceAgentListContainersResponse, error) + }{ + { + name: "test response", + setupMock: func(mcl *acmock.MockLister) (codersdk.WorkspaceAgentListContainersResponse, error) { + mcl.EXPECT().List(gomock.Any()).Return(testResponse, nil).Times(1) + return testResponse, nil + }, + }, + { + name: "error response", + setupMock: func(mcl *acmock.MockLister) (codersdk.WorkspaceAgentListContainersResponse, error) { + mcl.EXPECT().List(gomock.Any()).Return(codersdk.WorkspaceAgentListContainersResponse{}, assert.AnError).Times(1) + return codersdk.WorkspaceAgentListContainersResponse{}, assert.AnError + }, + }, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mcl := acmock.NewMockLister(ctrl) + expected, expectedErr := tc.setupMock(mcl) + client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{}) + user := coderdtest.CreateFirstUser(t, client) + r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OrganizationID: user.OrganizationID, + OwnerID: user.UserID, + }).WithAgent(func(agents []*proto.Agent) []*proto.Agent { + return agents + }).Do() + _ = agenttest.New(t, client.URL, r.AgentToken, func(opts *agent.Options) { + opts.ContainerLister = mcl + }) + resources := coderdtest.NewWorkspaceAgentWaiter(t, client, r.Workspace.ID).Wait() + require.Len(t, resources, 1, "expected one resource") + require.Len(t, resources[0].Agents, 1, "expected one agent") + agentID := resources[0].Agents[0].ID + + ctx := testutil.Context(t, testutil.WaitLong) + + // List containers and ensure we get the expected mocked response. + res, err := client.WorkspaceAgentListContainers(ctx, agentID, nil) + if expectedErr != nil { + require.Contains(t, err.Error(), expectedErr.Error(), "unexpected error") + require.Empty(t, res, "expected empty response") + } else { + require.NoError(t, err, "failed to list all containers") + if diff := cmp.Diff(expected, res); diff != "" { + t.Fatalf("unexpected response (-want +got):\n%s", diff) + } + } + }) + } + }) +} + func TestWorkspaceAgentAppHealth(t *testing.T) { t.Parallel() client, db := coderdtest.NewWithDatabase(t, nil) @@ -2041,7 +2235,7 @@ func requireGetManifest(ctx context.Context, t testing.TB, aAPI agentproto.DRPCA } func postStartup(ctx context.Context, t testing.TB, client agent.Client, startup *agentproto.Startup) error { - aAPI, _, err := client.ConnectRPC23(ctx) + aAPI, _, err := client.ConnectRPC24(ctx) require.NoError(t, err) defer func() { cErr := aAPI.DRPCConn().Close() diff --git a/coderd/workspaceagentsrpc.go b/coderd/workspaceagentsrpc.go index cbb3a1bc44b8a..43da35410f632 100644 --- a/coderd/workspaceagentsrpc.go +++ b/coderd/workspaceagentsrpc.go @@ -143,8 +143,11 @@ func (api *API) workspaceAgentRPC(rw http.ResponseWriter, r *http.Request) { Ctx: api.ctx, Log: logger, + Clock: api.Clock, Database: api.Database, + NotificationsEnqueuer: api.NotificationsEnqueuer, Pubsub: api.Pubsub, + Auditor: &api.Auditor, DerpMapFn: api.DERPMap, TailnetCoordinator: &api.TailnetCoordinator, AppearanceFetcher: &api.AppearanceFetcher, diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 375eaab5cd33b..76166bfcb6164 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -527,7 +527,7 @@ func (api *API) notifyWorkspaceUpdated( "workspace": map[string]any{"id": workspace.ID, "name": workspace.Name}, "template": map[string]any{"id": template.ID, "name": template.Name}, "template_version": map[string]any{"id": version.ID, "name": version.Name}, - "owner": map[string]any{"id": owner.ID, "name": owner.Name}, + "owner": map[string]any{"id": owner.ID, "name": owner.Name, "email": owner.Email}, "parameters": buildParameters, }, "api-workspaces-updated", diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index da4c09329cc39..f6bfcfd2ead28 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -648,7 +648,7 @@ func TestWorkspaceBuildWithUpdatedTemplateVersionSendsNotification(t *testing.T) client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, NotificationsEnqueuer: notify}) first := coderdtest.CreateFirstUser(t, client) templateAdminClient, templateAdmin := coderdtest.CreateAnotherUser(t, client, first.OrganizationID, rbac.RoleTemplateAdmin()) - userClient, _ := coderdtest.CreateAnotherUser(t, client, first.OrganizationID) + userClient, user := coderdtest.CreateAnotherUser(t, client, first.OrganizationID) // Create a template with an initial version version := coderdtest.CreateTemplateVersion(t, templateAdminClient, first.OrganizationID, nil) @@ -684,6 +684,12 @@ func TestWorkspaceBuildWithUpdatedTemplateVersionSendsNotification(t *testing.T) require.Contains(t, sent[0].Targets, workspace.ID) require.Contains(t, sent[0].Targets, workspace.OrganizationID) require.Contains(t, sent[0].Targets, workspace.OwnerID) + + owner, ok := sent[0].Data["owner"].(map[string]any) + require.True(t, ok, "notification data should have owner") + require.Equal(t, user.ID, owner["id"]) + require.Equal(t, user.Name, owner["name"]) + require.Equal(t, user.Email, owner["email"]) }) } @@ -714,6 +720,7 @@ func TestWorkspaceBuildLogs(t *testing.T) { Type: "example", Agents: []*proto.Agent{{ Id: "something", + Name: "dev", Auth: &proto.Agent_Token{}, }}, }, { diff --git a/coderd/workspaceresourceauth_test.go b/coderd/workspaceresourceauth_test.go index d653231ab90d6..8c1b64feaf59a 100644 --- a/coderd/workspaceresourceauth_test.go +++ b/coderd/workspaceresourceauth_test.go @@ -33,6 +33,7 @@ func TestPostWorkspaceAuthAzureInstanceIdentity(t *testing.T) { Name: "somename", Type: "someinstance", Agents: []*proto.Agent{{ + Name: "dev", Auth: &proto.Agent_InstanceId{ InstanceId: instanceID, }, @@ -78,6 +79,7 @@ func TestPostWorkspaceAuthAWSInstanceIdentity(t *testing.T) { Name: "somename", Type: "someinstance", Agents: []*proto.Agent{{ + Name: "dev", Auth: &proto.Agent_InstanceId{ InstanceId: instanceID, }, @@ -164,6 +166,7 @@ func TestPostWorkspaceAuthGoogleInstanceIdentity(t *testing.T) { Name: "somename", Type: "someinstance", Agents: []*proto.Agent{{ + Name: "dev", Auth: &proto.Agent_InstanceId{ InstanceId: instanceID, }, diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 158f27132b427..7a64648033c79 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -809,7 +809,7 @@ func (api *API) notifyWorkspaceCreated( "workspace": map[string]any{"id": workspace.ID, "name": workspace.Name}, "template": map[string]any{"id": template.ID, "name": template.Name}, "template_version": map[string]any{"id": version.ID, "name": version.Name}, - "owner": map[string]any{"id": owner.ID, "name": owner.Name}, + "owner": map[string]any{"id": owner.ID, "name": owner.Name, "email": owner.Email}, "parameters": buildParameters, }, "api-workspaces-create", diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index e74d5174123a1..8ee23dcd5100d 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -219,6 +219,7 @@ func TestWorkspace(t *testing.T) { Type: "example", Agents: []*proto.Agent{{ Id: uuid.NewString(), + Name: "dev", Auth: &proto.Agent_Token{}, }}, }}, @@ -259,6 +260,7 @@ func TestWorkspace(t *testing.T) { Type: "example", Agents: []*proto.Agent{{ Id: uuid.NewString(), + Name: "dev", Auth: &proto.Agent_Token{}, ConnectionTimeoutSeconds: 1, }}, @@ -373,6 +375,54 @@ func TestWorkspace(t *testing.T) { require.Error(t, err, "create workspace with archived version") require.ErrorContains(t, err, "Archived template versions cannot") }) + + t.Run("WorkspaceBan", func(t *testing.T) { + t.Parallel() + owner, _, _ := coderdtest.NewWithAPI(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + first := coderdtest.CreateFirstUser(t, owner) + + version := coderdtest.CreateTemplateVersion(t, owner, first.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJobCompleted(t, owner, version.ID) + template := coderdtest.CreateTemplate(t, owner, first.OrganizationID, version.ID) + + goodClient, _ := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID) + + // When a user with workspace-creation-ban + client, user := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.ScopedRoleOrgWorkspaceCreationBan(first.OrganizationID)) + + // Ensure a similar user can create a workspace + coderdtest.CreateWorkspace(t, goodClient, template.ID) + + ctx := testutil.Context(t, testutil.WaitLong) + // Then: Cannot create a workspace + _, err := client.CreateUserWorkspace(ctx, codersdk.Me, codersdk.CreateWorkspaceRequest{ + TemplateID: template.ID, + TemplateVersionID: uuid.UUID{}, + Name: "random", + }) + require.Error(t, err) + var apiError *codersdk.Error + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusForbidden, apiError.StatusCode()) + + // When: workspace-ban use has a workspace + wrk, err := owner.CreateUserWorkspace(ctx, user.ID.String(), codersdk.CreateWorkspaceRequest{ + TemplateID: template.ID, + TemplateVersionID: uuid.UUID{}, + Name: "random", + }) + require.NoError(t, err) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, wrk.LatestBuild.ID) + + // Then: They cannot delete said workspace + _, err = client.CreateWorkspaceBuild(ctx, wrk.ID, codersdk.CreateWorkspaceBuildRequest{ + Transition: codersdk.WorkspaceTransitionDelete, + ProvisionerState: []byte{}, + }) + require.Error(t, err) + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusForbidden, apiError.StatusCode()) + }) } func TestResolveAutostart(t *testing.T) { @@ -639,6 +689,12 @@ func TestPostWorkspacesByOrganization(t *testing.T) { require.Contains(t, sent[0].Targets, workspace.ID) require.Contains(t, sent[0].Targets, workspace.OrganizationID) require.Contains(t, sent[0].Targets, workspace.OwnerID) + + owner, ok := sent[0].Data["owner"].(map[string]any) + require.True(t, ok, "notification data should have owner") + require.Equal(t, memberUser.ID, owner["id"]) + require.Equal(t, memberUser.Name, owner["name"]) + require.Equal(t, memberUser.Email, owner["email"]) }) t.Run("CreateWithAuditLogs", func(t *testing.T) { @@ -1716,7 +1772,8 @@ func TestWorkspaceFilterManual(t *testing.T) { Name: "example", Type: "aws_instance", Agents: []*proto.Agent{{ - Id: uuid.NewString(), + Id: uuid.NewString(), + Name: "dev", Auth: &proto.Agent_Token{ Token: authToken, }, @@ -2723,7 +2780,8 @@ func TestWorkspaceWatcher(t *testing.T) { Name: "example", Type: "aws_instance", Agents: []*proto.Agent{{ - Id: uuid.NewString(), + Id: uuid.NewString(), + Name: "dev", Auth: &proto.Agent_Token{ Token: authToken, }, @@ -2945,6 +3003,7 @@ func TestWorkspaceResource(t *testing.T) { Type: "example", Agents: []*proto.Agent{{ Id: "something", + Name: "dev", Auth: &proto.Agent_Token{}, Apps: apps, }}, @@ -3019,6 +3078,7 @@ func TestWorkspaceResource(t *testing.T) { Type: "example", Agents: []*proto.Agent{{ Id: "something", + Name: "dev", Auth: &proto.Agent_Token{}, Apps: apps, }}, @@ -3062,6 +3122,7 @@ func TestWorkspaceResource(t *testing.T) { Type: "example", Agents: []*proto.Agent{{ Id: "something", + Name: "dev", Auth: &proto.Agent_Token{}, }}, Metadata: []*proto.Resource_Metadata{{ diff --git a/coderd/workspacestats/batcher_internal_test.go b/coderd/workspacestats/batcher_internal_test.go index 874acd7667dce..59efb33bfafed 100644 --- a/coderd/workspacestats/batcher_internal_test.go +++ b/coderd/workspacestats/batcher_internal_test.go @@ -53,7 +53,7 @@ func TestBatchStats(t *testing.T) { tick <- t1 f := <-flushed require.Equal(t, 0, f, "expected no data to be flushed") - t.Logf("flush 1 completed") + t.Log("flush 1 completed") // Then: it should report no stats. stats, err := store.GetWorkspaceAgentStats(ctx, t1) @@ -62,7 +62,7 @@ func TestBatchStats(t *testing.T) { // Given: a single data point is added for workspace t2 := t1.Add(time.Second) - t.Logf("inserting 1 stat") + t.Log("inserting 1 stat") b.Add(t2.Add(time.Millisecond), deps1.Agent.ID, deps1.User.ID, deps1.Template.ID, deps1.Workspace.ID, randStats(t), false) // When: it becomes time to report stats @@ -70,7 +70,7 @@ func TestBatchStats(t *testing.T) { tick <- t2 f = <-flushed // Wait for a flush to complete. require.Equal(t, 1, f, "expected one stat to be flushed") - t.Logf("flush 2 completed") + t.Log("flush 2 completed") // Then: it should report a single stat. stats, err = store.GetWorkspaceAgentStats(ctx, t2) @@ -97,7 +97,7 @@ func TestBatchStats(t *testing.T) { // When: the buffer comes close to capacity // Then: The buffer will force-flush once. f = <-flushed - t.Logf("flush 3 completed") + t.Log("flush 3 completed") require.Greater(t, f, 819, "expected at least 819 stats to be flushed (>=80% of buffer)") // And we should finish inserting the stats <-done @@ -110,7 +110,7 @@ func TestBatchStats(t *testing.T) { t4 := t3.Add(time.Second) tick <- t4 f2 := <-flushed - t.Logf("flush 4 completed") + t.Log("flush 4 completed") expectedCount := defaultBufferSize - f require.Equal(t, expectedCount, f2, "did not flush expected remaining rows") @@ -119,7 +119,7 @@ func TestBatchStats(t *testing.T) { tick <- t5 f = <-flushed require.Zero(t, f, "expected zero stats to have been flushed") - t.Logf("flush 5 completed") + t.Log("flush 5 completed") stats, err = store.GetWorkspaceAgentStats(ctx, t5) require.NoError(t, err, "should not error getting stats") diff --git a/coderd/wsbuilder/wsbuilder.go b/coderd/wsbuilder/wsbuilder.go index 3d757f4c5590b..f6d6d7381a24f 100644 --- a/coderd/wsbuilder/wsbuilder.go +++ b/coderd/wsbuilder/wsbuilder.go @@ -363,19 +363,20 @@ func (b *Builder) buildTx(authFunc func(action policy.Action, object rbac.Object var workspaceBuild database.WorkspaceBuild err = b.store.InTx(func(store database.Store) error { err = store.InsertWorkspaceBuild(b.ctx, database.InsertWorkspaceBuildParams{ - ID: workspaceBuildID, - CreatedAt: now, - UpdatedAt: now, - WorkspaceID: b.workspace.ID, - TemplateVersionID: templateVersionID, - BuildNumber: buildNum, - ProvisionerState: state, - InitiatorID: b.initiator, - Transition: b.trans, - JobID: provisionerJob.ID, - Reason: b.reason, - Deadline: time.Time{}, // set by provisioner upon completion - MaxDeadline: time.Time{}, // set by provisioner upon completion + ID: workspaceBuildID, + CreatedAt: now, + UpdatedAt: now, + WorkspaceID: b.workspace.ID, + TemplateVersionID: templateVersionID, + BuildNumber: buildNum, + ProvisionerState: state, + InitiatorID: b.initiator, + Transition: b.trans, + JobID: provisionerJob.ID, + Reason: b.reason, + Deadline: time.Time{}, // set by provisioner upon completion + MaxDeadline: time.Time{}, // set by provisioner upon completion + TemplateVersionPresetID: uuid.NullUUID{}, // TODO (sasswart): add this in from the caller }) if err != nil { code := http.StatusInternalServerError @@ -789,6 +790,15 @@ func (b *Builder) authorize(authFunc func(action policy.Action, object rbac.Obje return BuildError{http.StatusBadRequest, msg, xerrors.New(msg)} } if !authFunc(action, b.workspace) { + if authFunc(policy.ActionRead, b.workspace) { + // If the user can read the workspace, but not delete/create/update. Show + // a more helpful error. They are allowed to know the workspace exists. + return BuildError{ + Status: http.StatusForbidden, + Message: fmt.Sprintf("You do not have permission to %s this workspace.", action), + Wrapped: xerrors.New(httpapi.ResourceForbiddenResponse.Detail), + } + } // We use the same wording as the httpapi to avoid leaking the existence of the workspace return BuildError{http.StatusNotFound, httpapi.ResourceNotFoundResponse.Message, xerrors.New(httpapi.ResourceNotFoundResponse.Message)} } diff --git a/codersdk/agentsdk/agentsdk.go b/codersdk/agentsdk/agentsdk.go index 9e6362eb7dd54..0be6ee6f8a415 100644 --- a/codersdk/agentsdk/agentsdk.go +++ b/codersdk/agentsdk/agentsdk.go @@ -34,6 +34,18 @@ import ( // log-source. This should be removed in the future. var ExternalLogSourceID = uuid.MustParse("3b579bf4-1ed8-4b99-87a8-e9a1e3410410") +// ConnectionType is the type of connection that the agent is receiving. +type ConnectionType string + +// Connection type enums. +const ( + ConnectionTypeUnspecified ConnectionType = "Unspecified" + ConnectionTypeSSH ConnectionType = "SSH" + ConnectionTypeVSCode ConnectionType = "VS Code" + ConnectionTypeJetBrains ConnectionType = "JetBrains" + ConnectionTypeReconnectingPTY ConnectionType = "Web Terminal" +) + // New returns a client that is used to interact with the // Coder API from a workspace agent. func New(serverURL *url.URL) *Client { @@ -229,6 +241,18 @@ func (c *Client) ConnectRPC23(ctx context.Context) ( return proto.NewDRPCAgentClient(conn), tailnetproto.NewDRPCTailnetClient(conn), nil } +// ConnectRPC24 returns a dRPC client to the Agent API v2.4. It is useful when you want to be +// maximally compatible with Coderd Release Versions from 2.xx+ // TODO @vincent: define version +func (c *Client) ConnectRPC24(ctx context.Context) ( + proto.DRPCAgentClient24, tailnetproto.DRPCTailnetClient24, error, +) { + conn, err := c.connectRPCVersion(ctx, apiversion.New(2, 4)) + if err != nil { + return nil, nil, err + } + return proto.NewDRPCAgentClient(conn), tailnetproto.NewDRPCTailnetClient(conn), nil +} + // ConnectRPC connects to the workspace agent API and tailnet API func (c *Client) ConnectRPC(ctx context.Context) (drpc.Conn, error) { return c.connectRPCVersion(ctx, proto.CurrentVersion) diff --git a/codersdk/agentsdk/convert.go b/codersdk/agentsdk/convert.go index 002d96a50a017..7e8ea08c7499d 100644 --- a/codersdk/agentsdk/convert.go +++ b/codersdk/agentsdk/convert.go @@ -390,3 +390,37 @@ func ProtoFromLifecycleState(s codersdk.WorkspaceAgentLifecycle) (proto.Lifecycl } return proto.Lifecycle_State(caps), nil } + +func ConnectionTypeFromProto(typ proto.Connection_Type) (ConnectionType, error) { + switch typ { + case proto.Connection_TYPE_UNSPECIFIED: + return ConnectionTypeUnspecified, nil + case proto.Connection_SSH: + return ConnectionTypeSSH, nil + case proto.Connection_VSCODE: + return ConnectionTypeVSCode, nil + case proto.Connection_JETBRAINS: + return ConnectionTypeJetBrains, nil + case proto.Connection_RECONNECTING_PTY: + return ConnectionTypeReconnectingPTY, nil + default: + return "", xerrors.Errorf("unknown connection type %q", typ) + } +} + +func ProtoFromConnectionType(typ ConnectionType) (proto.Connection_Type, error) { + switch typ { + case ConnectionTypeUnspecified: + return proto.Connection_TYPE_UNSPECIFIED, nil + case ConnectionTypeSSH: + return proto.Connection_SSH, nil + case ConnectionTypeVSCode: + return proto.Connection_VSCODE, nil + case ConnectionTypeJetBrains: + return proto.Connection_JETBRAINS, nil + case ConnectionTypeReconnectingPTY: + return proto.Connection_RECONNECTING_PTY, nil + default: + return 0, xerrors.Errorf("unknown connection type %q", typ) + } +} diff --git a/codersdk/audit.go b/codersdk/audit.go index 307eeb275b61c..1df5bd2d10e2c 100644 --- a/codersdk/audit.go +++ b/codersdk/audit.go @@ -37,6 +37,8 @@ const ( ResourceTypeIdpSyncSettingsOrganization ResourceType = "idp_sync_settings_organization" ResourceTypeIdpSyncSettingsGroup ResourceType = "idp_sync_settings_group" ResourceTypeIdpSyncSettingsRole ResourceType = "idp_sync_settings_role" + ResourceTypeWorkspaceAgent ResourceType = "workspace_agent" + ResourceTypeWorkspaceApp ResourceType = "workspace_app" ) func (r ResourceType) FriendlyString() string { @@ -87,6 +89,10 @@ func (r ResourceType) FriendlyString() string { return "settings" case ResourceTypeIdpSyncSettingsRole: return "settings" + case ResourceTypeWorkspaceAgent: + return "workspace agent" + case ResourceTypeWorkspaceApp: + return "workspace app" default: return "unknown" } @@ -104,6 +110,10 @@ const ( AuditActionLogout AuditAction = "logout" AuditActionRegister AuditAction = "register" AuditActionRequestPasswordReset AuditAction = "request_password_reset" + AuditActionConnect AuditAction = "connect" + AuditActionDisconnect AuditAction = "disconnect" + AuditActionOpen AuditAction = "open" + AuditActionClose AuditAction = "close" ) func (a AuditAction) Friendly() string { @@ -126,6 +136,14 @@ func (a AuditAction) Friendly() string { return "registered" case AuditActionRequestPasswordReset: return "password reset requested" + case AuditActionConnect: + return "connected" + case AuditActionDisconnect: + return "disconnected" + case AuditActionOpen: + return "opened" + case AuditActionClose: + return "closed" default: return "unknown" } @@ -184,6 +202,7 @@ type CreateTestAuditLogRequest struct { Time time.Time `json:"time,omitempty" format:"date-time"` BuildReason BuildReason `json:"build_reason,omitempty" enums:"autostart,autostop,initiator"` OrganizationID uuid.UUID `json:"organization_id,omitempty" format:"uuid"` + RequestID uuid.UUID `json:"request_id,omitempty" format:"uuid"` } // AuditLogs retrieves audit logs from the given page. diff --git a/codersdk/deployment.go b/codersdk/deployment.go index e1c0b977c00d2..428ebac4944f5 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -503,13 +503,15 @@ type OAuth2Config struct { } type OAuth2GithubConfig struct { - ClientID serpent.String `json:"client_id" typescript:",notnull"` - ClientSecret serpent.String `json:"client_secret" typescript:",notnull"` - AllowedOrgs serpent.StringArray `json:"allowed_orgs" typescript:",notnull"` - AllowedTeams serpent.StringArray `json:"allowed_teams" typescript:",notnull"` - AllowSignups serpent.Bool `json:"allow_signups" typescript:",notnull"` - AllowEveryone serpent.Bool `json:"allow_everyone" typescript:",notnull"` - EnterpriseBaseURL serpent.String `json:"enterprise_base_url" typescript:",notnull"` + ClientID serpent.String `json:"client_id" typescript:",notnull"` + ClientSecret serpent.String `json:"client_secret" typescript:",notnull"` + DeviceFlow serpent.Bool `json:"device_flow" typescript:",notnull"` + DefaultProviderEnable serpent.Bool `json:"default_provider_enable" typescript:",notnull"` + AllowedOrgs serpent.StringArray `json:"allowed_orgs" typescript:",notnull"` + AllowedTeams serpent.StringArray `json:"allowed_teams" typescript:",notnull"` + AllowSignups serpent.Bool `json:"allow_signups" typescript:",notnull"` + AllowEveryone serpent.Bool `json:"allow_everyone" typescript:",notnull"` + EnterpriseBaseURL serpent.String `json:"enterprise_base_url" typescript:",notnull"` } type OIDCConfig struct { @@ -517,17 +519,27 @@ type OIDCConfig struct { ClientID serpent.String `json:"client_id" typescript:",notnull"` ClientSecret serpent.String `json:"client_secret" typescript:",notnull"` // ClientKeyFile & ClientCertFile are used in place of ClientSecret for PKI auth. - ClientKeyFile serpent.String `json:"client_key_file" typescript:",notnull"` - ClientCertFile serpent.String `json:"client_cert_file" typescript:",notnull"` - EmailDomain serpent.StringArray `json:"email_domain" typescript:",notnull"` - IssuerURL serpent.String `json:"issuer_url" typescript:",notnull"` - Scopes serpent.StringArray `json:"scopes" typescript:",notnull"` - IgnoreEmailVerified serpent.Bool `json:"ignore_email_verified" typescript:",notnull"` - UsernameField serpent.String `json:"username_field" typescript:",notnull"` - NameField serpent.String `json:"name_field" typescript:",notnull"` - EmailField serpent.String `json:"email_field" typescript:",notnull"` - AuthURLParams serpent.Struct[map[string]string] `json:"auth_url_params" typescript:",notnull"` - IgnoreUserInfo serpent.Bool `json:"ignore_user_info" typescript:",notnull"` + ClientKeyFile serpent.String `json:"client_key_file" typescript:",notnull"` + ClientCertFile serpent.String `json:"client_cert_file" typescript:",notnull"` + EmailDomain serpent.StringArray `json:"email_domain" typescript:",notnull"` + IssuerURL serpent.String `json:"issuer_url" typescript:",notnull"` + Scopes serpent.StringArray `json:"scopes" typescript:",notnull"` + IgnoreEmailVerified serpent.Bool `json:"ignore_email_verified" typescript:",notnull"` + UsernameField serpent.String `json:"username_field" typescript:",notnull"` + NameField serpent.String `json:"name_field" typescript:",notnull"` + EmailField serpent.String `json:"email_field" typescript:",notnull"` + AuthURLParams serpent.Struct[map[string]string] `json:"auth_url_params" typescript:",notnull"` + // IgnoreUserInfo & UserInfoFromAccessToken are mutually exclusive. Only 1 + // can be set to true. Ideally this would be an enum with 3 states, ['none', + // 'userinfo', 'access_token']. However, for backward compatibility, + // `ignore_user_info` must remain. And `access_token` is a niche, non-spec + // compliant edge case. So it's use is rare, and should not be advised. + IgnoreUserInfo serpent.Bool `json:"ignore_user_info" typescript:",notnull"` + // UserInfoFromAccessToken as mentioned above is an edge case. This allows + // sourcing the user_info from the access token itself instead of a user_info + // endpoint. This assumes the access token is a valid JWT with a set of claims to + // be merged with the id_token. + UserInfoFromAccessToken serpent.Bool `json:"source_user_info_from_access_token" typescript:",notnull"` OrganizationField serpent.String `json:"organization_field" typescript:",notnull"` OrganizationMapping serpent.Struct[map[string][]uuid.UUID] `json:"organization_mapping" typescript:",notnull"` OrganizationAssignDefault serpent.Bool `json:"organization_assign_default" typescript:",notnull"` @@ -1572,6 +1584,26 @@ func (c *DeploymentValues) Options() serpent.OptionSet { Annotations: serpent.Annotations{}.Mark(annotationSecretKey, "true"), Group: &deploymentGroupOAuth2GitHub, }, + { + Name: "OAuth2 GitHub Device Flow", + Description: "Enable device flow for Login with GitHub.", + Flag: "oauth2-github-device-flow", + Env: "CODER_OAUTH2_GITHUB_DEVICE_FLOW", + Value: &c.OAuth2.Github.DeviceFlow, + Group: &deploymentGroupOAuth2GitHub, + YAML: "deviceFlow", + Default: "false", + }, + { + Name: "OAuth2 GitHub Default Provider Enable", + Description: "Enable the default GitHub OAuth2 provider managed by Coder.", + Flag: "oauth2-github-default-provider-enable", + Env: "CODER_OAUTH2_GITHUB_DEFAULT_PROVIDER_ENABLE", + Value: &c.OAuth2.Github.DefaultProviderEnable, + Group: &deploymentGroupOAuth2GitHub, + YAML: "defaultProviderEnable", + Default: "true", + }, { Name: "OAuth2 GitHub Allowed Orgs", Description: "Organizations the user must be a member of to Login with GitHub.", @@ -1753,6 +1785,23 @@ func (c *DeploymentValues) Options() serpent.OptionSet { Group: &deploymentGroupOIDC, YAML: "ignoreUserInfo", }, + { + Name: "OIDC Access Token Claims", + // This is a niche edge case that should not be advertised. Alternatives should + // be investigated before turning this on. A properly configured IdP should + // always have a userinfo endpoint which is preferred. + Hidden: true, + Description: "Source supplemental user claims from the 'access_token'. This assumes the " + + "token is a jwt signed by the same issuer as the id_token. Using this requires setting " + + "'oidc-ignore-userinfo' to true. This setting is not compliant with the OIDC specification " + + "and is not recommended. Use at your own risk.", + Flag: "oidc-access-token-claims", + Env: "CODER_OIDC_ACCESS_TOKEN_CLAIMS", + Default: "false", + Value: &c.OIDC.UserInfoFromAccessToken, + Group: &deploymentGroupOIDC, + YAML: "accessTokenClaims", + }, { Name: "OIDC Organization Field", Description: "This field must be set if using the organization sync feature." + diff --git a/codersdk/deployment_test.go b/codersdk/deployment_test.go index 7a84fcbbd831b..1d2af676596d3 100644 --- a/codersdk/deployment_test.go +++ b/codersdk/deployment_test.go @@ -309,8 +309,8 @@ func TestDeploymentValues_DurationFormatNanoseconds(t *testing.T) { continue } t.Logf("Option %q is a duration but does not have the format_duration annotation.", s.Name) - t.Logf("To fix this, add the following to the option declaration:") - t.Logf(`Annotations: serpent.Annotations{}.Mark(annotationFormatDurationNS, "true"),`) + t.Log("To fix this, add the following to the option declaration:") + t.Log(`Annotations: serpent.Annotations{}.Mark(annotationFormatDurationNS, "true"),`) t.FailNow() } } diff --git a/codersdk/gitsshkey.go b/codersdk/gitsshkey.go index 7b56e01427f85..d1b65774610f3 100644 --- a/codersdk/gitsshkey.go +++ b/codersdk/gitsshkey.go @@ -15,7 +15,10 @@ type GitSSHKey struct { UserID uuid.UUID `json:"user_id" format:"uuid"` CreatedAt time.Time `json:"created_at" format:"date-time"` UpdatedAt time.Time `json:"updated_at" format:"date-time"` - PublicKey string `json:"public_key"` + // PublicKey is the SSH public key in OpenSSH format. + // Example: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID3OmYJvT7q1cF1azbybYy0OZ9yrXfA+M6Lr4vzX5zlp\n" + // Note: The key includes a trailing newline (\n). + PublicKey string `json:"public_key"` } // GitSSHKey returns the user's git SSH public key. diff --git a/codersdk/idpsync.go b/codersdk/idpsync.go index 2cc1f51ee3011..8f92cea680e25 100644 --- a/codersdk/idpsync.go +++ b/codersdk/idpsync.go @@ -12,6 +12,13 @@ import ( "golang.org/x/xerrors" ) +type IDPSyncMapping[ResourceIdType uuid.UUID | string] struct { + // The IdP claim the user has + Given string + // The ID of the Coder resource the user should be added to + Gets ResourceIdType +} + type GroupSyncSettings struct { // Field is the name of the claim field that specifies what groups a user // should be in. If empty, no groups will be synced. @@ -61,6 +68,46 @@ func (c *Client) PatchGroupIDPSyncSettings(ctx context.Context, orgID string, re return resp, json.NewDecoder(res.Body).Decode(&resp) } +type PatchGroupIDPSyncConfigRequest struct { + Field string `json:"field"` + RegexFilter *regexp.Regexp `json:"regex_filter"` + AutoCreateMissing bool `json:"auto_create_missing_groups"` +} + +func (c *Client) PatchGroupIDPSyncConfig(ctx context.Context, orgID string, req PatchGroupIDPSyncConfigRequest) (GroupSyncSettings, error) { + res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/organizations/%s/settings/idpsync/groups/config", orgID), req) + if err != nil { + return GroupSyncSettings{}, xerrors.Errorf("make request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return GroupSyncSettings{}, ReadBodyAsError(res) + } + var resp GroupSyncSettings + return resp, json.NewDecoder(res.Body).Decode(&resp) +} + +// If the same mapping is present in both Add and Remove, Remove will take presidence. +type PatchGroupIDPSyncMappingRequest struct { + Add []IDPSyncMapping[uuid.UUID] + Remove []IDPSyncMapping[uuid.UUID] +} + +func (c *Client) PatchGroupIDPSyncMapping(ctx context.Context, orgID string, req PatchGroupIDPSyncMappingRequest) (GroupSyncSettings, error) { + res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/organizations/%s/settings/idpsync/groups/mapping", orgID), req) + if err != nil { + return GroupSyncSettings{}, xerrors.Errorf("make request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return GroupSyncSettings{}, ReadBodyAsError(res) + } + var resp GroupSyncSettings + return resp, json.NewDecoder(res.Body).Decode(&resp) +} + type RoleSyncSettings struct { // Field is the name of the claim field that specifies what organization roles // a user should be given. If empty, no roles will be synced. @@ -97,6 +144,44 @@ func (c *Client) PatchRoleIDPSyncSettings(ctx context.Context, orgID string, req return resp, json.NewDecoder(res.Body).Decode(&resp) } +type PatchRoleIDPSyncConfigRequest struct { + Field string `json:"field"` +} + +func (c *Client) PatchRoleIDPSyncConfig(ctx context.Context, orgID string, req PatchRoleIDPSyncConfigRequest) (RoleSyncSettings, error) { + res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/organizations/%s/settings/idpsync/roles/config", orgID), req) + if err != nil { + return RoleSyncSettings{}, xerrors.Errorf("make request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return RoleSyncSettings{}, ReadBodyAsError(res) + } + var resp RoleSyncSettings + return resp, json.NewDecoder(res.Body).Decode(&resp) +} + +// If the same mapping is present in both Add and Remove, Remove will take presidence. +type PatchRoleIDPSyncMappingRequest struct { + Add []IDPSyncMapping[string] + Remove []IDPSyncMapping[string] +} + +func (c *Client) PatchRoleIDPSyncMapping(ctx context.Context, orgID string, req PatchRoleIDPSyncMappingRequest) (RoleSyncSettings, error) { + res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/organizations/%s/settings/idpsync/roles/mapping", orgID), req) + if err != nil { + return RoleSyncSettings{}, xerrors.Errorf("make request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return RoleSyncSettings{}, ReadBodyAsError(res) + } + var resp RoleSyncSettings + return resp, json.NewDecoder(res.Body).Decode(&resp) +} + type OrganizationSyncSettings struct { // Field selects the claim field to be used as the created user's // organizations. If the field is the empty string, then no organization @@ -137,6 +222,45 @@ func (c *Client) PatchOrganizationIDPSyncSettings(ctx context.Context, req Organ return resp, json.NewDecoder(res.Body).Decode(&resp) } +type PatchOrganizationIDPSyncConfigRequest struct { + Field string `json:"field"` + AssignDefault bool `json:"assign_default"` +} + +func (c *Client) PatchOrganizationIDPSyncConfig(ctx context.Context, req PatchOrganizationIDPSyncConfigRequest) (OrganizationSyncSettings, error) { + res, err := c.Request(ctx, http.MethodPatch, "/api/v2/settings/idpsync/organization/config", req) + if err != nil { + return OrganizationSyncSettings{}, xerrors.Errorf("make request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return OrganizationSyncSettings{}, ReadBodyAsError(res) + } + var resp OrganizationSyncSettings + return resp, json.NewDecoder(res.Body).Decode(&resp) +} + +// If the same mapping is present in both Add and Remove, Remove will take presidence. +type PatchOrganizationIDPSyncMappingRequest struct { + Add []IDPSyncMapping[uuid.UUID] + Remove []IDPSyncMapping[uuid.UUID] +} + +func (c *Client) PatchOrganizationIDPSyncMapping(ctx context.Context, req PatchOrganizationIDPSyncMappingRequest) (OrganizationSyncSettings, error) { + res, err := c.Request(ctx, http.MethodPatch, "/api/v2/settings/idpsync/organization/mapping", req) + if err != nil { + return OrganizationSyncSettings{}, xerrors.Errorf("make request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return OrganizationSyncSettings{}, ReadBodyAsError(res) + } + var resp OrganizationSyncSettings + return resp, json.NewDecoder(res.Body).Decode(&resp) +} + func (c *Client) GetAvailableIDPSyncFields(ctx context.Context) ([]string, error) { res, err := c.Request(ctx, http.MethodGet, "/api/v2/settings/idpsync/available-fields", nil) if err != nil { diff --git a/codersdk/licenses.go b/codersdk/licenses.go index d7634c72bf4ff..4863aad60c6ff 100644 --- a/codersdk/licenses.go +++ b/codersdk/licenses.go @@ -12,7 +12,8 @@ import ( ) const ( - LicenseExpiryClaim = "license_expires" + LicenseExpiryClaim = "license_expires" + LicenseTelemetryRequiredErrorText = "License requires telemetry but telemetry is disabled" ) type AddLicenseRequest struct { diff --git a/codersdk/notifications.go b/codersdk/notifications.go index c1602c19f4260..ac5fe8e60bce1 100644 --- a/codersdk/notifications.go +++ b/codersdk/notifications.go @@ -193,6 +193,19 @@ func (c *Client) GetNotificationDispatchMethods(ctx context.Context) (Notificati return resp, nil } +func (c *Client) PostTestNotification(ctx context.Context) error { + res, err := c.Request(ctx, http.MethodPost, "/api/v2/notifications/test", nil) + if err != nil { + return err + } + defer res.Body.Close() + + if res.StatusCode != http.StatusNoContent { + return ReadBodyAsError(res) + } + return nil +} + type UpdateNotificationTemplateMethod struct { Method string `json:"method,omitempty" example:"webhook"` } diff --git a/codersdk/oauth2.go b/codersdk/oauth2.go index 726a50907e3fd..bb198d04a6108 100644 --- a/codersdk/oauth2.go +++ b/codersdk/oauth2.go @@ -227,3 +227,7 @@ func (c *Client) RevokeOAuth2ProviderApp(ctx context.Context, appID uuid.UUID) e } return nil } + +type OAuth2DeviceFlowCallbackResponse struct { + RedirectURL string `json:"redirect_url"` +} diff --git a/codersdk/organizations.go b/codersdk/organizations.go index a6bacd2798043..781baaaa5d5d6 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -316,21 +316,34 @@ func (c *Client) ProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, e return daemons, json.NewDecoder(res.Body).Decode(&daemons) } -func (c *Client) OrganizationProvisionerDaemons(ctx context.Context, organizationID uuid.UUID, tags map[string]string) ([]ProvisionerDaemon, error) { - baseURL := fmt.Sprintf("/api/v2/organizations/%s/provisionerdaemons", organizationID.String()) - - queryParams := url.Values{} - tagsJSON, err := json.Marshal(tags) - if err != nil { - return nil, xerrors.Errorf("marshal tags: %w", err) - } +type OrganizationProvisionerDaemonsOptions struct { + Limit int + IDs []uuid.UUID + Tags map[string]string +} - queryParams.Add("tags", string(tagsJSON)) - if len(queryParams) > 0 { - baseURL = fmt.Sprintf("%s?%s", baseURL, queryParams.Encode()) +func (c *Client) OrganizationProvisionerDaemons(ctx context.Context, organizationID uuid.UUID, opts *OrganizationProvisionerDaemonsOptions) ([]ProvisionerDaemon, error) { + qp := url.Values{} + if opts != nil { + if opts.Limit > 0 { + qp.Add("limit", strconv.Itoa(opts.Limit)) + } + if len(opts.IDs) > 0 { + qp.Add("ids", joinSliceStringer(opts.IDs)) + } + if len(opts.Tags) > 0 { + tagsRaw, err := json.Marshal(opts.Tags) + if err != nil { + return nil, xerrors.Errorf("marshal tags: %w", err) + } + qp.Add("tags", string(tagsRaw)) + } } - res, err := c.Request(ctx, http.MethodGet, baseURL, nil) + res, err := c.Request(ctx, http.MethodGet, + fmt.Sprintf("/api/v2/organizations/%s/provisionerdaemons?%s", organizationID.String(), qp.Encode()), + nil, + ) if err != nil { return nil, xerrors.Errorf("execute request: %w", err) } @@ -346,7 +359,9 @@ func (c *Client) OrganizationProvisionerDaemons(ctx context.Context, organizatio type OrganizationProvisionerJobsOptions struct { Limit int + IDs []uuid.UUID Status []ProvisionerJobStatus + Tags map[string]string } func (c *Client) OrganizationProvisionerJobs(ctx context.Context, organizationID uuid.UUID, opts *OrganizationProvisionerJobsOptions) ([]ProvisionerJob, error) { @@ -355,9 +370,19 @@ func (c *Client) OrganizationProvisionerJobs(ctx context.Context, organizationID if opts.Limit > 0 { qp.Add("limit", strconv.Itoa(opts.Limit)) } + if len(opts.IDs) > 0 { + qp.Add("ids", joinSliceStringer(opts.IDs)) + } if len(opts.Status) > 0 { qp.Add("status", joinSlice(opts.Status)) } + if len(opts.Tags) > 0 { + tagsRaw, err := json.Marshal(opts.Tags) + if err != nil { + return nil, xerrors.Errorf("marshal tags: %w", err) + } + qp.Add("tags", string(tagsRaw)) + } } res, err := c.Request(ctx, http.MethodGet, @@ -401,6 +426,14 @@ func joinSlice[T ~string](s []T) string { return strings.Join(ss, ",") } +func joinSliceStringer[T fmt.Stringer](s []T) string { + var ss []string + for _, v := range s { + ss = append(ss, v.String()) + } + return strings.Join(ss, ",") +} + // CreateTemplateVersion processes source-code and optionally associates the version with a template. // Executing without a template is useful for validating source-code. func (c *Client) CreateTemplateVersion(ctx context.Context, organizationID uuid.UUID, req CreateTemplateVersionRequest) (TemplateVersion, error) { diff --git a/codersdk/presets.go b/codersdk/presets.go new file mode 100644 index 0000000000000..110f6c605f026 --- /dev/null +++ b/codersdk/presets.go @@ -0,0 +1,36 @@ +package codersdk + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/google/uuid" + "golang.org/x/xerrors" +) + +type Preset struct { + ID uuid.UUID + Name string + Parameters []PresetParameter +} + +type PresetParameter struct { + Name string + Value string +} + +// TemplateVersionPresets returns the presets associated with a template version. +func (c *Client) TemplateVersionPresets(ctx context.Context, templateVersionID uuid.UUID) ([]Preset, error) { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templateversions/%s/presets", templateVersionID), nil) + if err != nil { + return nil, xerrors.Errorf("do request: %w", err) + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return nil, ReadBodyAsError(res) + } + var presets []Preset + return presets, json.NewDecoder(res.Body).Decode(&presets) +} diff --git a/codersdk/provisionerdaemons.go b/codersdk/provisionerdaemons.go index 33177c52bcf6b..f6130f3b8235d 100644 --- a/codersdk/provisionerdaemons.go +++ b/codersdk/provisionerdaemons.go @@ -69,8 +69,11 @@ type ProvisionerDaemon struct { } type ProvisionerDaemonJob struct { - ID uuid.UUID `json:"id" format:"uuid" table:"id"` - Status ProvisionerJobStatus `json:"status" enums:"pending,running,succeeded,canceling,canceled,failed" table:"status"` + ID uuid.UUID `json:"id" format:"uuid" table:"id"` + Status ProvisionerJobStatus `json:"status" enums:"pending,running,succeeded,canceling,canceled,failed" table:"status"` + TemplateName string `json:"template_name" table:"template name"` + TemplateIcon string `json:"template_icon" table:"template icon"` + TemplateDisplayName string `json:"template_display_name" table:"template display name"` } // MatchedProvisioners represents the number of provisioner daemons @@ -131,6 +134,17 @@ type ProvisionerJobInput struct { Error string `json:"error,omitempty" table:"-"` } +// ProvisionerJobMetadata contains metadata for the job. +type ProvisionerJobMetadata struct { + TemplateVersionName string `json:"template_version_name" table:"template version name"` + TemplateID uuid.UUID `json:"template_id" format:"uuid" table:"template id"` + TemplateName string `json:"template_name" table:"template name"` + TemplateDisplayName string `json:"template_display_name" table:"template display name"` + TemplateIcon string `json:"template_icon" table:"template icon"` + WorkspaceID *uuid.UUID `json:"workspace_id,omitempty" format:"uuid" table:"workspace id"` + WorkspaceName string `json:"workspace_name,omitempty" table:"workspace name"` +} + // ProvisionerJobType represents the type of job. type ProvisionerJobType string @@ -155,23 +169,24 @@ func JobIsMissingParameterErrorCode(code JobErrorCode) bool { // ProvisionerJob describes the job executed by the provisioning daemon. type ProvisionerJob struct { - ID uuid.UUID `json:"id" format:"uuid" table:"id"` - CreatedAt time.Time `json:"created_at" format:"date-time" table:"created at"` - StartedAt *time.Time `json:"started_at,omitempty" format:"date-time" table:"started at"` - CompletedAt *time.Time `json:"completed_at,omitempty" format:"date-time" table:"completed at"` - CanceledAt *time.Time `json:"canceled_at,omitempty" format:"date-time" table:"canceled at"` - Error string `json:"error,omitempty" table:"error"` - ErrorCode JobErrorCode `json:"error_code,omitempty" enums:"REQUIRED_TEMPLATE_VARIABLES" table:"error code"` - Status ProvisionerJobStatus `json:"status" enums:"pending,running,succeeded,canceling,canceled,failed" table:"status"` - WorkerID *uuid.UUID `json:"worker_id,omitempty" format:"uuid" table:"worker id"` - FileID uuid.UUID `json:"file_id" format:"uuid" table:"file id"` - Tags map[string]string `json:"tags" table:"tags"` - QueuePosition int `json:"queue_position" table:"queue position"` - QueueSize int `json:"queue_size" table:"queue size"` - OrganizationID uuid.UUID `json:"organization_id" format:"uuid" table:"organization id"` - Input ProvisionerJobInput `json:"input" table:"input,recursive_inline"` - Type ProvisionerJobType `json:"type" table:"type"` - AvailableWorkers []uuid.UUID `json:"available_workers,omitempty" format:"uuid" table:"available workers"` + ID uuid.UUID `json:"id" format:"uuid" table:"id"` + CreatedAt time.Time `json:"created_at" format:"date-time" table:"created at"` + StartedAt *time.Time `json:"started_at,omitempty" format:"date-time" table:"started at"` + CompletedAt *time.Time `json:"completed_at,omitempty" format:"date-time" table:"completed at"` + CanceledAt *time.Time `json:"canceled_at,omitempty" format:"date-time" table:"canceled at"` + Error string `json:"error,omitempty" table:"error"` + ErrorCode JobErrorCode `json:"error_code,omitempty" enums:"REQUIRED_TEMPLATE_VARIABLES" table:"error code"` + Status ProvisionerJobStatus `json:"status" enums:"pending,running,succeeded,canceling,canceled,failed" table:"status"` + WorkerID *uuid.UUID `json:"worker_id,omitempty" format:"uuid" table:"worker id"` + FileID uuid.UUID `json:"file_id" format:"uuid" table:"file id"` + Tags map[string]string `json:"tags" table:"tags"` + QueuePosition int `json:"queue_position" table:"queue position"` + QueueSize int `json:"queue_size" table:"queue size"` + OrganizationID uuid.UUID `json:"organization_id" format:"uuid" table:"organization id"` + Input ProvisionerJobInput `json:"input" table:"input,recursive_inline"` + Type ProvisionerJobType `json:"type" table:"type"` + AvailableWorkers []uuid.UUID `json:"available_workers,omitempty" format:"uuid" table:"available workers"` + Metadata ProvisionerJobMetadata `json:"metadata" table:"metadata,recursive_inline"` } // ProvisionerJobLog represents the provisioner log entry annotated with source and level. @@ -352,6 +367,12 @@ const ( ProvisionerKeyIDPSK = "00000000-0000-0000-0000-000000000003" ) +var ( + ProvisionerKeyUUIDBuiltIn = uuid.MustParse(ProvisionerKeyIDBuiltIn) + ProvisionerKeyUUIDUserAuth = uuid.MustParse(ProvisionerKeyIDUserAuth) + ProvisionerKeyUUIDPSK = uuid.MustParse(ProvisionerKeyIDPSK) +) + const ( ProvisionerKeyNameBuiltIn = "built-in" ProvisionerKeyNameUserAuth = "user-auth" diff --git a/codersdk/rbacresources_gen.go b/codersdk/rbacresources_gen.go index 8de32c107aae4..68b765db3f8a6 100644 --- a/codersdk/rbacresources_gen.go +++ b/codersdk/rbacresources_gen.go @@ -4,39 +4,39 @@ package codersdk type RBACResource string const ( - ResourceWildcard RBACResource = "*" - ResourceApiKey RBACResource = "api_key" - ResourceAssignOrgRole RBACResource = "assign_org_role" - ResourceAssignRole RBACResource = "assign_role" - ResourceAuditLog RBACResource = "audit_log" - ResourceCryptoKey RBACResource = "crypto_key" - ResourceDebugInfo RBACResource = "debug_info" - ResourceDeploymentConfig RBACResource = "deployment_config" - ResourceDeploymentStats RBACResource = "deployment_stats" - ResourceFile RBACResource = "file" - ResourceGroup RBACResource = "group" - ResourceGroupMember RBACResource = "group_member" - ResourceIdpsyncSettings RBACResource = "idpsync_settings" - ResourceLicense RBACResource = "license" - ResourceNotificationMessage RBACResource = "notification_message" - ResourceNotificationPreference RBACResource = "notification_preference" - ResourceNotificationTemplate RBACResource = "notification_template" - ResourceOauth2App RBACResource = "oauth2_app" - ResourceOauth2AppCodeToken RBACResource = "oauth2_app_code_token" - ResourceOauth2AppSecret RBACResource = "oauth2_app_secret" - ResourceOrganization RBACResource = "organization" - ResourceOrganizationMember RBACResource = "organization_member" - ResourceProvisionerDaemon RBACResource = "provisioner_daemon" - ResourceProvisionerJobs RBACResource = "provisioner_jobs" - ResourceProvisionerKeys RBACResource = "provisioner_keys" - ResourceReplicas RBACResource = "replicas" - ResourceSystem RBACResource = "system" - ResourceTailnetCoordinator RBACResource = "tailnet_coordinator" - ResourceTemplate RBACResource = "template" - ResourceUser RBACResource = "user" - ResourceWorkspace RBACResource = "workspace" - ResourceWorkspaceDormant RBACResource = "workspace_dormant" - ResourceWorkspaceProxy RBACResource = "workspace_proxy" + ResourceWildcard RBACResource = "*" + ResourceApiKey RBACResource = "api_key" + ResourceAssignOrgRole RBACResource = "assign_org_role" + ResourceAssignRole RBACResource = "assign_role" + ResourceAuditLog RBACResource = "audit_log" + ResourceCryptoKey RBACResource = "crypto_key" + ResourceDebugInfo RBACResource = "debug_info" + ResourceDeploymentConfig RBACResource = "deployment_config" + ResourceDeploymentStats RBACResource = "deployment_stats" + ResourceFile RBACResource = "file" + ResourceGroup RBACResource = "group" + ResourceGroupMember RBACResource = "group_member" + ResourceIdpsyncSettings RBACResource = "idpsync_settings" + ResourceLicense RBACResource = "license" + ResourceNotificationMessage RBACResource = "notification_message" + ResourceNotificationPreference RBACResource = "notification_preference" + ResourceNotificationTemplate RBACResource = "notification_template" + ResourceOauth2App RBACResource = "oauth2_app" + ResourceOauth2AppCodeToken RBACResource = "oauth2_app_code_token" + ResourceOauth2AppSecret RBACResource = "oauth2_app_secret" + ResourceOrganization RBACResource = "organization" + ResourceOrganizationMember RBACResource = "organization_member" + ResourceProvisionerDaemon RBACResource = "provisioner_daemon" + ResourceProvisionerJobs RBACResource = "provisioner_jobs" + ResourceReplicas RBACResource = "replicas" + ResourceSystem RBACResource = "system" + ResourceTailnetCoordinator RBACResource = "tailnet_coordinator" + ResourceTemplate RBACResource = "template" + ResourceUser RBACResource = "user" + ResourceWorkspace RBACResource = "workspace" + ResourceWorkspaceAgentResourceMonitor RBACResource = "workspace_agent_resource_monitor" + ResourceWorkspaceDormant RBACResource = "workspace_dormant" + ResourceWorkspaceProxy RBACResource = "workspace_proxy" ) type RBACAction string @@ -49,6 +49,7 @@ const ( ActionRead RBACAction = "read" ActionReadPersonal RBACAction = "read_personal" ActionSSH RBACAction = "ssh" + ActionUnassign RBACAction = "unassign" ActionUpdate RBACAction = "update" ActionUpdatePersonal RBACAction = "update_personal" ActionUse RBACAction = "use" @@ -60,37 +61,37 @@ const ( // RBACResourceActions is the mapping of resources to which actions are valid for // said resource type. var RBACResourceActions = map[RBACResource][]RBACAction{ - ResourceWildcard: {}, - ResourceApiKey: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, - ResourceAssignOrgRole: {ActionAssign, ActionCreate, ActionDelete, ActionRead, ActionUpdate}, - ResourceAssignRole: {ActionAssign, ActionCreate, ActionDelete, ActionRead, ActionUpdate}, - ResourceAuditLog: {ActionCreate, ActionRead}, - ResourceCryptoKey: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, - ResourceDebugInfo: {ActionRead}, - ResourceDeploymentConfig: {ActionRead, ActionUpdate}, - ResourceDeploymentStats: {ActionRead}, - ResourceFile: {ActionCreate, ActionRead}, - ResourceGroup: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, - ResourceGroupMember: {ActionRead}, - ResourceIdpsyncSettings: {ActionRead, ActionUpdate}, - ResourceLicense: {ActionCreate, ActionDelete, ActionRead}, - ResourceNotificationMessage: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, - ResourceNotificationPreference: {ActionRead, ActionUpdate}, - ResourceNotificationTemplate: {ActionRead, ActionUpdate}, - ResourceOauth2App: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, - ResourceOauth2AppCodeToken: {ActionCreate, ActionDelete, ActionRead}, - ResourceOauth2AppSecret: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, - ResourceOrganization: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, - ResourceOrganizationMember: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, - ResourceProvisionerDaemon: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, - ResourceProvisionerJobs: {ActionRead}, - ResourceProvisionerKeys: {ActionCreate, ActionDelete, ActionRead}, - ResourceReplicas: {ActionRead}, - ResourceSystem: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, - ResourceTailnetCoordinator: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, - ResourceTemplate: {ActionCreate, ActionDelete, ActionRead, ActionUpdate, ActionUse, ActionViewInsights}, - ResourceUser: {ActionCreate, ActionDelete, ActionRead, ActionReadPersonal, ActionUpdate, ActionUpdatePersonal}, - ResourceWorkspace: {ActionApplicationConnect, ActionCreate, ActionDelete, ActionRead, ActionSSH, ActionWorkspaceStart, ActionWorkspaceStop, ActionUpdate}, - ResourceWorkspaceDormant: {ActionApplicationConnect, ActionCreate, ActionDelete, ActionRead, ActionSSH, ActionWorkspaceStart, ActionWorkspaceStop, ActionUpdate}, - ResourceWorkspaceProxy: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, + ResourceWildcard: {}, + ResourceApiKey: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, + ResourceAssignOrgRole: {ActionAssign, ActionCreate, ActionDelete, ActionRead, ActionUnassign, ActionUpdate}, + ResourceAssignRole: {ActionAssign, ActionRead, ActionUnassign}, + ResourceAuditLog: {ActionCreate, ActionRead}, + ResourceCryptoKey: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, + ResourceDebugInfo: {ActionRead}, + ResourceDeploymentConfig: {ActionRead, ActionUpdate}, + ResourceDeploymentStats: {ActionRead}, + ResourceFile: {ActionCreate, ActionRead}, + ResourceGroup: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, + ResourceGroupMember: {ActionRead}, + ResourceIdpsyncSettings: {ActionRead, ActionUpdate}, + ResourceLicense: {ActionCreate, ActionDelete, ActionRead}, + ResourceNotificationMessage: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, + ResourceNotificationPreference: {ActionRead, ActionUpdate}, + ResourceNotificationTemplate: {ActionRead, ActionUpdate}, + ResourceOauth2App: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, + ResourceOauth2AppCodeToken: {ActionCreate, ActionDelete, ActionRead}, + ResourceOauth2AppSecret: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, + ResourceOrganization: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, + ResourceOrganizationMember: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, + ResourceProvisionerDaemon: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, + ResourceProvisionerJobs: {ActionRead}, + ResourceReplicas: {ActionRead}, + ResourceSystem: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, + ResourceTailnetCoordinator: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, + ResourceTemplate: {ActionCreate, ActionDelete, ActionRead, ActionUpdate, ActionUse, ActionViewInsights}, + ResourceUser: {ActionCreate, ActionDelete, ActionRead, ActionReadPersonal, ActionUpdate, ActionUpdatePersonal}, + ResourceWorkspace: {ActionApplicationConnect, ActionCreate, ActionDelete, ActionRead, ActionSSH, ActionWorkspaceStart, ActionWorkspaceStop, ActionUpdate}, + ResourceWorkspaceAgentResourceMonitor: {ActionCreate, ActionRead, ActionUpdate}, + ResourceWorkspaceDormant: {ActionApplicationConnect, ActionCreate, ActionDelete, ActionRead, ActionSSH, ActionWorkspaceStart, ActionWorkspaceStop, ActionUpdate}, + ResourceWorkspaceProxy: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, } diff --git a/codersdk/rbacroles.go b/codersdk/rbacroles.go index 49ed5c5b73176..7721eacbd5624 100644 --- a/codersdk/rbacroles.go +++ b/codersdk/rbacroles.go @@ -8,9 +8,10 @@ const ( RoleUserAdmin string = "user-admin" RoleAuditor string = "auditor" - RoleOrganizationAdmin string = "organization-admin" - RoleOrganizationMember string = "organization-member" - RoleOrganizationAuditor string = "organization-auditor" - RoleOrganizationTemplateAdmin string = "organization-template-admin" - RoleOrganizationUserAdmin string = "organization-user-admin" + RoleOrganizationAdmin string = "organization-admin" + RoleOrganizationMember string = "organization-member" + RoleOrganizationAuditor string = "organization-auditor" + RoleOrganizationTemplateAdmin string = "organization-template-admin" + RoleOrganizationUserAdmin string = "organization-user-admin" + RoleOrganizationWorkspaceCreationBan string = "organization-workspace-creation-ban" ) diff --git a/codersdk/richparameters.go b/codersdk/richparameters.go index a0848d3cdffec..6fd082d5faf6c 100644 --- a/codersdk/richparameters.go +++ b/codersdk/richparameters.go @@ -5,7 +5,7 @@ import ( "golang.org/x/xerrors" - "github.com/coder/terraform-provider-coder/provider" + "github.com/coder/terraform-provider-coder/v2/provider" ) func ValidateNewWorkspaceParameters(richParameters []TemplateVersionParameter, buildParameters []WorkspaceBuildParameter) error { diff --git a/codersdk/users.go b/codersdk/users.go index 4dbdc0d4e4f91..7177a1bc3e76d 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -275,10 +275,10 @@ type OAuthConversionResponse struct { // AuthMethods contains authentication method information like whether they are enabled or not or custom text, etc. type AuthMethods struct { - TermsOfServiceURL string `json:"terms_of_service_url,omitempty"` - Password AuthMethod `json:"password"` - Github AuthMethod `json:"github"` - OIDC OIDCAuthMethod `json:"oidc"` + TermsOfServiceURL string `json:"terms_of_service_url,omitempty"` + Password AuthMethod `json:"password"` + Github GithubAuthMethod `json:"github"` + OIDC OIDCAuthMethod `json:"oidc"` } type AuthMethod struct { @@ -289,6 +289,11 @@ type UserLoginType struct { LoginType LoginType `json:"login_type"` } +type GithubAuthMethod struct { + Enabled bool `json:"enabled"` + DefaultProviderConfigured bool `json:"default_provider_configured"` +} + type OIDCAuthMethod struct { AuthMethod SignInText string `json:"signInText"` diff --git a/codersdk/workspaceagents.go b/codersdk/workspaceagents.go index 4f04b70aee83c..8e2209fa8072b 100644 --- a/codersdk/workspaceagents.go +++ b/codersdk/workspaceagents.go @@ -392,6 +392,72 @@ func (c *Client) WorkspaceAgentListeningPorts(ctx context.Context, agentID uuid. return listeningPorts, json.NewDecoder(res.Body).Decode(&listeningPorts) } +// WorkspaceAgentDevcontainer describes a devcontainer of some sort +// that is visible to the workspace agent. This struct is an abstraction +// of potentially multiple implementations, and the fields will be +// somewhat implementation-dependent. +type WorkspaceAgentDevcontainer struct { + // CreatedAt is the time the container was created. + CreatedAt time.Time `json:"created_at" format:"date-time"` + // ID is the unique identifier of the container. + ID string `json:"id"` + // FriendlyName is the human-readable name of the container. + FriendlyName string `json:"name"` + // Image is the name of the container image. + Image string `json:"image"` + // Labels is a map of key-value pairs of container labels. + Labels map[string]string `json:"labels"` + // Running is true if the container is currently running. + Running bool `json:"running"` + // Ports includes ports exposed by the container. + Ports []WorkspaceAgentListeningPort `json:"ports"` + // Status is the current status of the container. This is somewhat + // implementation-dependent, but should generally be a human-readable + // string. + Status string `json:"status"` + // Volumes is a map of "things" mounted into the container. Again, this + // is somewhat implementation-dependent. + Volumes map[string]string `json:"volumes"` +} + +// WorkspaceAgentListContainersResponse is the response to the list containers +// request. +type WorkspaceAgentListContainersResponse struct { + // Containers is a list of containers visible to the workspace agent. + Containers []WorkspaceAgentDevcontainer `json:"containers"` + // Warnings is a list of warnings that may have occurred during the + // process of listing containers. This should not include fatal errors. + Warnings []string `json:"warnings,omitempty"` +} + +func workspaceAgentContainersLabelFilter(kvs map[string]string) RequestOption { + return func(r *http.Request) { + q := r.URL.Query() + for k, v := range kvs { + kv := fmt.Sprintf("%s=%s", k, v) + q.Add("label", kv) + } + r.URL.RawQuery = q.Encode() + } +} + +// WorkspaceAgentListContainers returns a list of containers that are currently +// running on a Docker daemon accessible to the workspace agent. +func (c *Client) WorkspaceAgentListContainers(ctx context.Context, agentID uuid.UUID, labels map[string]string) (WorkspaceAgentListContainersResponse, error) { + lf := workspaceAgentContainersLabelFilter(labels) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaceagents/%s/containers", agentID), nil, lf) + if err != nil { + return WorkspaceAgentListContainersResponse{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return WorkspaceAgentListContainersResponse{}, ReadBodyAsError(res) + } + var cr WorkspaceAgentListContainersResponse + + return cr, json.NewDecoder(res.Body).Decode(&cr) +} + //nolint:revive // Follow is a control flag on the server as well. func (c *Client) WorkspaceAgentLogsAfter(ctx context.Context, agentID uuid.UUID, after int64, follow bool) (<-chan []WorkspaceAgentLog, io.Closer, error) { var queryParams []string diff --git a/codersdk/workspacesdk/agentconn.go b/codersdk/workspacesdk/agentconn.go index 4c3a9539bbf55..95a27a1cd334e 100644 --- a/codersdk/workspacesdk/agentconn.go +++ b/codersdk/workspacesdk/agentconn.go @@ -143,6 +143,12 @@ func (c *AgentConn) ReconnectingPTY(ctx context.Context, id uuid.UUID, height, w // SSH pipes the SSH protocol over the returned net.Conn. // This connects to the built-in SSH server in the workspace agent. func (c *AgentConn) SSH(ctx context.Context) (*gonet.TCPConn, error) { + return c.SSHOnPort(ctx, AgentSSHPort) +} + +// SSHOnPort pipes the SSH protocol over the returned net.Conn. +// This connects to the built-in SSH server in the workspace agent on the specified port. +func (c *AgentConn) SSHOnPort(ctx context.Context, port uint16) (*gonet.TCPConn, error) { ctx, span := tracing.StartSpan(ctx) defer span.End() @@ -150,17 +156,23 @@ func (c *AgentConn) SSH(ctx context.Context) (*gonet.TCPConn, error) { return nil, xerrors.Errorf("workspace agent not reachable in time: %v", ctx.Err()) } - c.Conn.SendConnectedTelemetry(c.agentAddress(), tailnet.TelemetryApplicationSSH) - return c.Conn.DialContextTCP(ctx, netip.AddrPortFrom(c.agentAddress(), AgentSSHPort)) + c.SendConnectedTelemetry(c.agentAddress(), tailnet.TelemetryApplicationSSH) + return c.DialContextTCP(ctx, netip.AddrPortFrom(c.agentAddress(), port)) } // SSHClient calls SSH to create a client that uses a weak cipher // to improve throughput. func (c *AgentConn) SSHClient(ctx context.Context) (*ssh.Client, error) { + return c.SSHClientOnPort(ctx, AgentSSHPort) +} + +// SSHClientOnPort calls SSH to create a client on a specific port +// that uses a weak cipher to improve throughput. +func (c *AgentConn) SSHClientOnPort(ctx context.Context, port uint16) (*ssh.Client, error) { ctx, span := tracing.StartSpan(ctx) defer span.End() - netConn, err := c.SSH(ctx) + netConn, err := c.SSHOnPort(ctx, port) if err != nil { return nil, xerrors.Errorf("ssh: %w", err) } @@ -336,6 +348,22 @@ func (c *AgentConn) PrometheusMetrics(ctx context.Context) ([]byte, error) { return bs, nil } +// ListContainers returns a response from the agent's containers endpoint +func (c *AgentConn) ListContainers(ctx context.Context) (codersdk.WorkspaceAgentListContainersResponse, error) { + ctx, span := tracing.StartSpan(ctx) + defer span.End() + res, err := c.apiRequest(ctx, http.MethodGet, "/api/v0/containers", nil) + if err != nil { + return codersdk.WorkspaceAgentListContainersResponse{}, xerrors.Errorf("do request: %w", err) + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return codersdk.WorkspaceAgentListContainersResponse{}, codersdk.ReadBodyAsError(res) + } + var resp codersdk.WorkspaceAgentListContainersResponse + return resp, json.NewDecoder(res.Body).Decode(&resp) +} + // apiRequest makes a request to the workspace agent's HTTP API server. func (c *AgentConn) apiRequest(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) { ctx, span := tracing.StartSpan(ctx) diff --git a/codersdk/workspacesdk/workspacesdk.go b/codersdk/workspacesdk/workspacesdk.go index 17b22a363d6a0..3a5419381e94f 100644 --- a/codersdk/workspacesdk/workspacesdk.go +++ b/codersdk/workspacesdk/workspacesdk.go @@ -29,6 +29,7 @@ var ErrSkipClose = xerrors.New("skip tailnet close") const ( AgentSSHPort = tailnet.WorkspaceAgentSSHPort + AgentStandardSSHPort = tailnet.WorkspaceAgentStandardSSHPort AgentReconnectingPTYPort = tailnet.WorkspaceAgentReconnectingPTYPort AgentSpeedtestPort = tailnet.WorkspaceAgentSpeedtestPort // AgentHTTPAPIServerPort serves a HTTP server with endpoints for e.g. diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 7be637cb8203c..4ec303b388d49 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -106,6 +106,15 @@ Use the following `make` commands and scripts in development: - The default user is `admin@coder.com` and the default password is `SomeSecurePassword!` +### Running Coder using docker-compose + +This mode is useful for testing HA or validating more complex setups. + +- Generate a new image from your HEAD: `make build/coder_$(./scripts/version.sh)_$(go env GOOS)_$(go env GOARCH).tag` + - This will output the name of the new image, e.g.: `ghcr.io/coder/coder:v2.19.0-devel-22fa71d15-amd64` +- Inject this image into docker-compose: `CODER_VERSION=v2.19.0-devel-22fa71d15-amd64 docker-compose up` (*note the prefix `ghcr.io/coder/coder:` was removed*) +- To use Docker, determine your host's `docker` group ID with `getent group docker | cut -d: -f3`, then update the value of `group_add` and uncomment + ### Deploying a PR > You need to be a member or collaborator of the of @@ -150,17 +159,17 @@ Database migrations are managed with To add new migrations, use the following command: ```shell -./coderd/database/migrations/create_fixture.sh my name +./coderd/database/migrations/create_migration.sh my name /home/coder/src/coder/coderd/database/migrations/000070_my_name.up.sql /home/coder/src/coder/coderd/database/migrations/000070_my_name.down.sql ``` -Run "make gen" to generate models. - Then write queries into the generated `.up.sql` and `.down.sql` files and commit them into the repository. The down script should make a best-effort to retain as much data as possible. +Run `make gen` to generate models. + #### Database fixtures (for testing migrations) There are two types of fixtures that are used to test that migrations don't diff --git a/docs/admin/index.md b/docs/admin/index.md index 7dcdbc3ce91df..8e527ba420c8a 100644 --- a/docs/admin/index.md +++ b/docs/admin/index.md @@ -1,5 +1,7 @@ # Administration +![Admin settings general page](../images/admin/admin-settings-general.png) + These guides contain information on managing the Coder control plane and [authoring templates](./templates/index.md). diff --git a/docs/admin/integrations/kubernetes-logs.md b/docs/admin/integrations/kubernetes-logs.md index 95fb5d84801f5..03c942283931f 100644 --- a/docs/admin/integrations/kubernetes-logs.md +++ b/docs/admin/integrations/kubernetes-logs.md @@ -8,29 +8,6 @@ or deployment, such as: - Causes of pod provisioning failures, or why a pod is stuck in a pending state. - Visibility into when pods are OOMKilled, or when they are evicted. -## Prerequisites - -`coder-logstream-kube` works best with the -[`kubernetes_deployment`](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/deployment) -Terraform resource, which requires the `coder` service account to have -permission to create deployments. For example, if you use -[Helm](../../install/kubernetes.md#4-install-coder-with-helm) to install Coder, -you should set `coder.serviceAccount.enableDeployments=true` in your -`values.yaml` - -```diff -coder: -serviceAccount: - workspacePerms: true -- enableDeployments: false -+ enableDeployments: true - annotations: {} - name: coder -``` - -> Note: This is only required for Coder versions < 0.28.0, as this will be the -> default value for Coder versions >= 0.28.0 - ## Installation Install the `coder-logstream-kube` helm chart on the cluster where the diff --git a/docs/admin/integrations/platformx.md b/docs/admin/integrations/platformx.md new file mode 100644 index 0000000000000..207087b23562e --- /dev/null +++ b/docs/admin/integrations/platformx.md @@ -0,0 +1,70 @@ +# DX PlatformX + +[DX](https://getdx.com) is a developer intelligence platform used by engineering +leaders and platform engineers. Coder notifications can be transformed to +[PlatformX](https://getdx.com/platformx) events, allowing platform engineers to +measure activity and send pulse surveys to subsets of Coder users to understand +their experience. + +![PlatformX Events in Coder](../../images/integrations/platformx-screenshot.png) + +## Requirements + +You'll need: + +- Coder v2.19+ +- A PlatformX subscription from [DX](https://getdx.com/) +- A platform to host the integration, such as: + - AWS Lambda + - Google Cloud Run + - Heroku + - Kubernetes + - Or any other platform that can run Python web applications + +## coder-platformx-events-middleware + +Coder sends [notifications](../monitoring/notifications/index.md) via webhooks +to coder-platformx-events-middleware, which processes and reformats the payload +into a structure compatible with [PlatformX by DX](https://help.getdx.com/en/articles/7880779-getting-started). + +For more information about coder-platformx-events-middleware and how to +integrate it with your Coder deployment and PlatformX events, refer to the +[coder-platformx-notifications](https://github.com/coder/coder-platformx-notifications) +repository. + +### Supported Notification Types + +coder-platformx-events-middleware supports the following [Coder notifications](../monitoring/notifications/index.md): + +- Workspace Created +- Workspace Manually Updated +- User Account Created +- User Account Suspended +- User Account Activated + +### Environment Variables + +The application expects the following environment variables when started. +For local development, create a `.env` file in the project root with the following variables. +A `.env.sample` file is included: + +| Variable | Description | Example | +|------------------|--------------------------------------------|----------------------------------------------| +| `LOG_LEVEL` | Logging level (`DEBUG`, `INFO`, `WARNING`) | `INFO` | +| `GETDX_API_KEY` | API key for PlatformX | `your-api-key` | +| `EVENTS_TRACKED` | Comma-separated list of tracked events | `"Workspace Created,User Account Suspended"` | + +### Logging + +Logs are printed to the console and can be adjusted using the `LOG_LEVEL` variable. The available levels are: + +| Level | Description | +|-----------|---------------------------------------| +| `DEBUG` | Most verbose, useful for debugging | +| `INFO` | Standard logging for normal operation | +| `WARNING` | Logs only warnings and errors | + +### API Endpoints + +- `GET /` - Health check endpoint +- `POST /` - Webhook receiver diff --git a/docs/admin/licensing/index.md b/docs/admin/licensing/index.md index 5fb7f345bb26a..6d2abda948125 100644 --- a/docs/admin/licensing/index.md +++ b/docs/admin/licensing/index.md @@ -12,6 +12,8 @@ features, you can [request a trial](https://coder.com/trial) or +![Licenses screen shows license information and seat consumption](../../images/admin/licenses/licenses-screen.png) + ## Adding your license key There are two ways to add a license to a Coder deployment: @@ -20,33 +22,48 @@ There are two ways to add a license to a Coder deployment: ### Coder UI -First, ensure you have a license key -([request a trial](https://coder.com/trial)). +1. With an `Owner` account, go to **Admin settings** > **Deployment**. + +1. Select **Licenses** from the sidebar, then **Add a license**: + + ![Add a license from the licenses screen](../../images/admin/licenses/licenses-nolicense.png) -With an `Owner` account, navigate to `Deployment -> Licenses`, `Add a license` -then drag or select the license file with the `jwt` extension. +1. On the **Add a license** screen, drag your `.jwt` license file into the + **Upload Your License** section, or paste your license in the + **Paste Your License** text box, then select **Upload License**: -![Add License UI](../../images/add-license-ui.png) + ![Add a license screen](../../images/admin/licenses/add-license-ui.png) ### Coder CLI -First, ensure you have a license key -([request a trial](https://coder.com/trial)) and the -[Coder CLI](../../install/cli.md) installed. +1. Ensure you have the [Coder CLI](../../install/cli.md) installed. +1. Save your license key to disk and make note of the path. +1. Open a terminal. +1. Log in to your Coder deployment: + + ```shell + coder login + ``` + +1. Run `coder licenses add`: -1. Save your license key to disk and make note of the path -2. Open a terminal -3. Ensure you are logged into your Coder deployment + - For a `.jwt` license file: - `coder login ` + ```shell + coder licenses add -f + ``` -4. Run + - For a text string: - `coder licenses add -f ` + ```sh + coder licenses add -l 1f5...765 + ``` -## Find your deployment ID +## FAQ + +### Find your deployment ID You'll need your deployment ID to request a trial or license key. @@ -54,3 +71,10 @@ From your Coder dashboard, select your user avatar, then select the **Copy to clipboard** icon at the bottom: ![Copy the deployment ID from the bottom of the user avatar dropdown](../../images/admin/deployment-id-copy-clipboard.png) + +### How we calculate license seat consumption + +Licenses are consumed based on the status of user accounts. +Only users who have been active in the last 90 days consume license seats. + +Consult the [user status documentation](../users/index.md#user-status) for more information about active, dormant, and suspended user statuses. diff --git a/docs/admin/monitoring/notifications/index.md b/docs/admin/monitoring/notifications/index.md index a7eeab44d4b79..eb077e13b38ed 100644 --- a/docs/admin/monitoring/notifications/index.md +++ b/docs/admin/monitoring/notifications/index.md @@ -3,43 +3,54 @@ Notifications are sent by Coder in response to specific internal events, such as a workspace being deleted or a user being created. +Available events may differ between versions. +For a list of all events, visit your Coder deployment's +`https://coder.example.com/deployment/notifications`. + ## Event Types Notifications are sent in response to internal events, to alert the affected -user(s) of this event. Currently we support the following list of events: +user(s) of the event. + +Coder supports the following list of events: ### Workspace Events -_These notifications are sent to the workspace owner._ +These notifications are sent to the workspace owner: -- Workspace Deleted -- Workspace Manual Build Failure -- Workspace Automatic Build Failure -- Workspace Automatically Updated -- Workspace Dormant -- Workspace Marked For Deletion +- Workspace created +- Workspace deleted +- Workspace manual build failure +- Workspace automatic build failure +- Workspace manually updated +- Workspace automatically updated +- Workspace marked as dormant +- Workspace marked for deletion ### User Events -_These notifications are sent to users with **owner** and **user admin** roles._ +These notifications sent to users with **owner** and **user admin** roles: -- User Account Created -- User Account Deleted -- User Account Suspended -- User Account Activated -- _(coming soon) User Password Reset_ -- _(coming soon) User Email Verification_ +- User account created +- User account deleted +- User account suspended +- User account activated -_These notifications are sent to the user themselves._ +These notifications sent to users themselves: -- User Account Suspended -- User Account Activated +- User account suspended +- User account activated +- User password reset (One-time passcode) ### Template Events -_These notifications are sent to users with **template admin** roles._ +These notifications are sent to users with **template admin** roles: -- Template Deleted +- Template deleted +- Template deprecated +- Report: Workspace builds failed for template + - This notification is delivered as part of a weekly cron job and summarizes + the failed builds for a given template. ## Configuration diff --git a/docs/admin/monitoring/notifications/slack.md b/docs/admin/monitoring/notifications/slack.md index e7cad847faad4..4b9810d9fbe86 100644 --- a/docs/admin/monitoring/notifications/slack.md +++ b/docs/admin/monitoring/notifications/slack.md @@ -89,11 +89,11 @@ To build the server to receive webhooks and interact with Slack: return res.status(400).send("Error: request body is missing"); } - const { title, body } = req.body; - if (!title || !body) { - return res - .status(400) - .send('Error: missing fields: "title", or "body"'); + const { title_markdown, body_markdown } = req.body; + if (!title_markdown || !body_markdown) { + return res + .status(400) + .send('Error: missing fields: "title_markdown", or "body_markdown"'); } const payload = req.body.payload; @@ -119,11 +119,11 @@ To build the server to receive webhooks and interact with Slack: blocks: [ { type: "header", - text: { type: "plain_text", text: title }, + text: { type: "mrkdwn", text: title_markdown }, }, { type: "section", - text: { type: "mrkdwn", text: body }, + text: { type: "mrkdwn", text: body_markdown }, }, ], }; diff --git a/docs/admin/monitoring/notifications/teams.md b/docs/admin/monitoring/notifications/teams.md index 0b874a997c54a..477ebcb714603 100644 --- a/docs/admin/monitoring/notifications/teams.md +++ b/docs/admin/monitoring/notifications/teams.md @@ -67,10 +67,10 @@ The process of setting up a Teams workflow consists of three key steps: } } }, - "title": { + "title_markdown": { "type": "string" }, - "body": { + "body_markdown": { "type": "string" } } @@ -108,11 +108,11 @@ The process of setting up a Teams workflow consists of three key steps: }, { "type": "TextBlock", - "text": "**@{replace(body('Parse_JSON')?['title'], '"', '\"')}**" + "text": "**@{replace(body('Parse_JSON')?['title_markdown'], '"', '\"')}**" }, { "type": "TextBlock", - "text": "@{replace(body('Parse_JSON')?['body'], '"', '\"')}", + "text": "@{replace(body('Parse_JSON')?['body_markdown'], '"', '\"')}", "wrap": true }, { diff --git a/docs/admin/networking/index.md b/docs/admin/networking/index.md index 34e1ef875a7b4..9858a8bfe4316 100644 --- a/docs/admin/networking/index.md +++ b/docs/admin/networking/index.md @@ -31,14 +31,6 @@ In order for clients and workspaces to be able to connect: - Any reverse proxy or ingress between the Coder control plane and clients/agents must support WebSockets. -> **Note:** We strongly recommend that clients connect to Coder and their -> workspaces over a good quality, broadband network connection. The following -> are minimum requirements: -> -> - better than 400ms round-trip latency to the Coder server and to their -> workspace -> - better than 0.5% random packet loss - In order for clients to be able to establish direct connections: > **Note:** Direct connections via the web browser are not supported. To improve diff --git a/docs/admin/security/audit-logs.md b/docs/admin/security/audit-logs.md index 85e3a17e34665..4817ea03f4bc5 100644 --- a/docs/admin/security/audit-logs.md +++ b/docs/admin/security/audit-logs.md @@ -23,13 +23,15 @@ We track the following resources: | NotificationsSettings
| |
FieldTracked
idfalse
notifier_pausedtrue
| | OAuth2ProviderApp
| |
FieldTracked
callback_urltrue
created_atfalse
icontrue
idfalse
nametrue
updated_atfalse
| | OAuth2ProviderAppSecret
| |
FieldTracked
app_idfalse
created_atfalse
display_secretfalse
hashed_secretfalse
idfalse
last_used_atfalse
secret_prefixfalse
| -| Organization
| |
FieldTracked
created_atfalse
descriptiontrue
display_nametrue
icontrue
idfalse
is_defaulttrue
nametrue
updated_attrue
| +| Organization
| |
FieldTracked
created_atfalse
deletedtrue
descriptiontrue
display_nametrue
icontrue
idfalse
is_defaulttrue
nametrue
updated_attrue
| | OrganizationSyncSettings
| |
FieldTracked
assign_defaulttrue
fieldtrue
mappingtrue
| | RoleSyncSettings
| |
FieldTracked
fieldtrue
mappingtrue
| | Template
write, delete | |
FieldTracked
active_version_idtrue
activity_bumptrue
allow_user_autostarttrue
allow_user_autostoptrue
allow_user_cancel_workspace_jobstrue
autostart_block_days_of_weektrue
autostop_requirement_days_of_weektrue
autostop_requirement_weekstrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
default_ttltrue
deletedfalse
deprecatedtrue
descriptiontrue
display_nametrue
failure_ttltrue
group_acltrue
icontrue
idtrue
max_port_sharing_leveltrue
nametrue
organization_display_namefalse
organization_iconfalse
organization_idfalse
organization_namefalse
provisionertrue
require_active_versiontrue
time_til_dormanttrue
time_til_dormant_autodeletetrue
updated_atfalse
user_acltrue
| | TemplateVersion
create, write | |
FieldTracked
archivedtrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
external_auth_providersfalse
idtrue
job_idfalse
messagefalse
nametrue
organization_idfalse
readmetrue
source_example_idfalse
template_idtrue
updated_atfalse
| | User
create, write, delete | |
FieldTracked
avatar_urlfalse
created_atfalse
deletedtrue
emailtrue
github_com_user_idfalse
hashed_one_time_passcodefalse
hashed_passwordtrue
idtrue
last_seen_atfalse
login_typetrue
nametrue
one_time_passcode_expires_attrue
quiet_hours_scheduletrue
rbac_rolestrue
statustrue
theme_preferencefalse
updated_atfalse
usernametrue
| -| WorkspaceBuild
start, stop | |
FieldTracked
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
idfalse
initiator_by_avatar_urlfalse
initiator_by_usernamefalse
initiator_idfalse
job_idfalse
max_deadlinefalse
provisioner_statefalse
reasonfalse
template_version_idtrue
transitionfalse
updated_atfalse
workspace_idfalse
| +| WorkspaceAgent
connect, disconnect | |
FieldTracked
api_versionfalse
architecturefalse
auth_instance_idfalse
auth_tokenfalse
connection_timeout_secondsfalse
created_atfalse
directoryfalse
disconnected_atfalse
display_appsfalse
display_orderfalse
environment_variablesfalse
expanded_directoryfalse
first_connected_atfalse
idfalse
instance_metadatafalse
last_connected_atfalse
last_connected_replica_idfalse
lifecycle_statefalse
logs_lengthfalse
logs_overflowedfalse
motd_filefalse
namefalse
operating_systemfalse
ready_atfalse
resource_idfalse
resource_metadatafalse
started_atfalse
subsystemsfalse
troubleshooting_urlfalse
updated_atfalse
versionfalse
| +| WorkspaceApp
open, close | |
FieldTracked
agent_idfalse
commandfalse
created_atfalse
display_namefalse
display_orderfalse
externalfalse
healthfalse
healthcheck_intervalfalse
healthcheck_thresholdfalse
healthcheck_urlfalse
hiddenfalse
iconfalse
idfalse
open_infalse
sharing_levelfalse
slugfalse
subdomainfalse
urlfalse
| +| WorkspaceBuild
start, stop | |
FieldTracked
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
idfalse
initiator_by_avatar_urlfalse
initiator_by_usernamefalse
initiator_idfalse
job_idfalse
max_deadlinefalse
provisioner_statefalse
reasonfalse
template_version_idtrue
template_version_preset_idfalse
transitionfalse
updated_atfalse
workspace_idfalse
| | WorkspaceProxy
| |
FieldTracked
created_attrue
deletedfalse
derp_enabledtrue
derp_onlytrue
display_nametrue
icontrue
idtrue
nametrue
region_idtrue
token_hashed_secrettrue
updated_atfalse
urltrue
versiontrue
wildcard_hostnametrue
| | WorkspaceTable
| |
FieldTracked
automatic_updatestrue
autostart_scheduletrue
created_atfalse
deletedfalse
deleting_attrue
dormant_attrue
favoritetrue
idtrue
last_used_atfalse
nametrue
next_start_attrue
organization_idfalse
owner_idtrue
template_idtrue
ttltrue
updated_atfalse
| diff --git a/docs/admin/templates/extending-templates/workspace-tags.md b/docs/admin/templates/extending-templates/workspace-tags.md index 96691fe162540..04bf64ad511c5 100644 --- a/docs/admin/templates/extending-templates/workspace-tags.md +++ b/docs/admin/templates/extending-templates/workspace-tags.md @@ -71,7 +71,7 @@ added that can handle its combination of tags. Before releasing the template version with configurable workspace tags, ensure that every tag set is associated with at least one healthy provisioner. -> [!NOTE] It may be useful to run at least one provisioner with no additional +> **Note:** It may be useful to run at least one provisioner with no additional > tag restrictions that is able to take on any job. ### Parameters types diff --git a/docs/admin/templates/managing-templates/schedule.md b/docs/admin/templates/managing-templates/schedule.md index 89185f7fa7df7..584bd025d5aa2 100644 --- a/docs/admin/templates/managing-templates/schedule.md +++ b/docs/admin/templates/managing-templates/schedule.md @@ -122,7 +122,7 @@ stopped due to the policy at the start of the user's quiet hours. ![User schedule settings](../../../images/admin/templates/schedule/user-quiet-hours.png) Admins can define the default quiet hours for all users with the -`--default-quiet-hours-schedule` flag or `CODER_DEFAULT_QUIET_HOURS_SCHEDULE` +[CODER_QUIET_HOURS_DEFAULT_SCHEDULE](../../../reference/cli/server.md#--default-quiet-hours-schedule) environment variable. The value should be a cron expression such as `CRON_TZ=America/Chicago 30 2 * * *` which would set the default quiet hours to 2:30 AM in the America/Chicago timezone. The cron schedule can only have a diff --git a/docs/admin/templates/troubleshooting.md b/docs/admin/templates/troubleshooting.md index e08a422938e2f..992811175f804 100644 --- a/docs/admin/templates/troubleshooting.md +++ b/docs/admin/templates/troubleshooting.md @@ -2,7 +2,7 @@ Occasionally, you may run into scenarios where a workspace is created, but the agent is either not connected or the -[startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script) +[startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script-1) has failed or timed out. ## Agent connection issues @@ -36,18 +36,18 @@ practices: ## Startup script issues Depending on the contents of the -[startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script), +[startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script-1), and whether or not the -[startup script behavior](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script_behavior) +[startup script behavior](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script_behavior-1) is set to blocking or non-blocking, you may notice issues related to the startup script. In this section we will cover common scenarios and how to resolve them. ### Unable to access workspace, startup script is still running If you're trying to access your workspace and are unable to because the -[startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script) +[startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script-1) is still running, it means the -[startup script behavior](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script_behavior) +[startup script behavior](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script_behavior-1) option is set to blocking or you have enabled the `--wait=yes` option (for e.g. `coder ssh` or `coder config-ssh`). In such an event, you can always access the workspace by using the web terminal, or via SSH using the `--wait=no` option. If @@ -58,13 +58,13 @@ terminating processes started by it or terminating the startup script itself (on Linux, `ps` and `kill` are useful tools). For tips on how to write a startup script that doesn't run forever, see the -[`startup_script`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script) +[`startup_script`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script-1) section. For more ways to override the startup script behavior, see the -[`startup_script_behavior`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script_behavior) +[`startup_script_behavior`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script_behavior-1) section. Template authors can also set the -[startup script behavior](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script_behavior) +[startup script behavior](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script_behavior-1) option to non-blocking, which will allow users to access the workspace while the startup script is still running. Note that the workspace must be updated after changing this option. @@ -74,7 +74,7 @@ changing this option. If you see a warning that your workspace may be incomplete, it means you should be aware that programs, files, or settings may be missing from your workspace. This can happen if the -[startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script) +[startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script-1) is still running or has exited with a non-zero status (see [startup script error](#startup-script-exited-with-an-error)). No action is necessary, but you may want to @@ -86,7 +86,7 @@ issues. ### Session was started before the startup script finished The web terminal may show this message if it was started before the -[startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script) +[startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script-1) finished, but the startup script has since finished. This message can safely be dismissed, however, be aware that your preferred shell or dotfiles may not yet be activated for this shell session. You can either start a new session or @@ -102,7 +102,7 @@ Examples for activating your preferred shell or sourcing your dotfiles: ### Startup script exited with an error When the -[startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script) +[startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script-1) exits with an error, it means the last command run by the script failed. When `set -e` is used, this means that any failing command will immediately exit the script and the remaining commands will not be executed. This also means that @@ -120,7 +120,7 @@ Common causes for startup script errors: ### Debugging the startup script The simplest way to debug the -[startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script) +[startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script-1) is to open the workspace in the Coder dashboard and click "Show startup log" (if not already visible). This will show all the output from the script. Another option is to view the log file inside the workspace (usually diff --git a/docs/admin/users/github-auth.md b/docs/admin/users/github-auth.md index 97e700e262ff8..1bacc36462326 100644 --- a/docs/admin/users/github-auth.md +++ b/docs/admin/users/github-auth.md @@ -1,5 +1,28 @@ # GitHub +## Default Configuration + +By default, new Coder deployments use a Coder-managed GitHub app to authenticate +users. We provide it for convenience, allowing you to experiment with Coder +without setting up your own GitHub OAuth app. Once you authenticate with it, you +grant Coder server read access to: + +- Your GitHub user email +- Your GitHub organization membership +- Other metadata listed during the authentication flow + +This access is necessary for the Coder server to complete the authentication +process. To the best of our knowledge, Coder, the company, does not gain access +to this data by administering the GitHub app. + +For production deployments, we recommend configuring your own GitHub OAuth app +as outlined below. The default is automatically disabled if you configure your +own app or set: + +```env +CODER_OAUTH2_GITHUB_DEFAULT_PROVIDER_ENABLE=false +``` + ## Step 1: Configure the OAuth application in GitHub First, @@ -82,3 +105,16 @@ helm upgrade coder-v2/coder -n -f values.yaml > We recommend requiring and auditing MFA usage for all users in your GitHub > organizations. This can be enforced from the organization settings page in the > "Authentication security" sidebar tab. + +## Device Flow + +Coder supports +[device flow](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow) +for GitHub OAuth. To enable it, set: + +```env +CODER_OAUTH2_GITHUB_DEVICE_FLOW=true +``` + +This is optional. We recommend using the standard OAuth flow instead, as it is +more convenient for end users. diff --git a/docs/admin/users/groups-roles.md b/docs/admin/users/groups-roles.md index 21dc22988b76b..d0b9ee0231bf6 100644 --- a/docs/admin/users/groups-roles.md +++ b/docs/admin/users/groups-roles.md @@ -35,7 +35,7 @@ may use personal workspaces.
-Custom roles are an Enterprise and Premium feature. +Custom roles are a Premium feature. [Learn more](https://coder.com/pricing#compare-plans).
@@ -44,9 +44,6 @@ Starting in v2.16.0, Premium Coder deployments can configure custom roles on the [Organization](./organizations.md) level. You can create and assign custom roles in the dashboard under **Organizations** -> **My Organization** -> **Roles**. -> Note: This requires a Premium license. -> [Contact your account team](https://coder.com/contact) for more details. - ![Custom roles](../../images/admin/users/roles/custom-roles.PNG) ### Example roles diff --git a/docs/admin/users/idp-sync.md b/docs/admin/users/idp-sync.md index 8e9ea79a9a80b..ee2dc83be387c 100644 --- a/docs/admin/users/idp-sync.md +++ b/docs/admin/users/idp-sync.md @@ -1,13 +1,39 @@ -# IDP Sync +# IdP Sync
-IDP sync is an Enterprise and Premium feature. +IdP sync is an Enterprise and Premium feature. [Learn more](https://coder.com/pricing#compare-plans).
+IdP (Identity provider) sync allows you to use OpenID Connect (OIDC) to +synchronize Coder groups, roles, and organizations based on claims from your IdP. + +## Prerequisites + +### Confirm that OIDC provider sends claims + +To confirm that your OIDC provider is sending claims, log in with OIDC and visit +the following URL with an `Owner` account: + +```text +https://[coder.example.com]/api/v2/debug/[your-username]/debug-link +``` + +You should see a field in either `id_token_claims`, `user_info_claims` or +both followed by a list of the user's OIDC groups in the response. + +This is the [claim](https://openid.net/specs/openid-connect-core-1_0.html#Claims) +sent by the OIDC provider. + +Depending on the OIDC provider, this claim might be called something else. +Common names include `groups`, `memberOf`, and `roles`. + +See the [troubleshooting section](#troubleshooting-grouproleorganization-sync) +for help troubleshooting common issues. + ## Group Sync If your OpenID Connect provider supports group claims, you can configure Coder @@ -21,115 +47,36 @@ If group sync is enabled, the user's groups will be controlled by the OIDC provider. This means manual group additions/removals will be overwritten on the next user login. -There are two ways you can configure group sync: +For deployments with multiple [organizations](./organizations.md), configure +group sync for each organization.
-## Server Flags +### Dashboard -1. Confirm that your OIDC provider is sending claims. - - Log in with OIDC and visit the following URL with an `Owner` account: +1. Fetch the corresponding group IDs using the following endpoint: ```text - https://[coder.example.com]/api/v2/debug/[your-username]/debug-link + https://[coder.example.com]/api/v2/groups ``` - You should see a field in either `id_token_claims`, `user_info_claims` or - both followed by a list of the user's OIDC groups in the response. This is - the [claim](https://openid.net/specs/openid-connect-core-1_0.html#Claims) - sent by the OIDC provider. - - See [Troubleshooting](#troubleshooting-grouproleorganization-sync) to debug - this. +1. As an Owner or Organization Admin, go to **Admin settings**, select + **Organizations**, then **IdP Sync**: - Depending on the OIDC provider, this claim may be called something else. - Common names include `groups`, `memberOf`, and `roles`. + ![IdP Sync - Group sync settings](../../images/admin/users/organizations/group-sync-empty.png) -1. Configure the Coder server to read groups from the claim name with the - [OIDC group field](../../reference/cli/server.md#--oidc-group-field) server - flag: +1. Enter the **Group sync field** and an optional **Regex filter**, then select + **Save**. - - Environment variable: +1. Select **Auto create missing groups** to automatically create groups + returned by the OIDC provider if they do not exist in Coder. - ```sh - CODER_OIDC_GROUP_FIELD=groups - ``` +1. Enter the **IdP group name** and **Coder group**, then **Add IdP group**. - - As a flag: - - ```sh - --oidc-group-field groups - ``` - -On login, users will automatically be assigned to groups that have matching -names in Coder and removed from groups that the user no longer belongs to. - -For cases when an OIDC provider only returns group IDs or you want to have -different group names in Coder than in your OIDC provider, you can configure -mapping between the two with the -[OIDC group mapping](../../reference/cli/server.md#--oidc-group-mapping) server -flag: - -- Environment variable: - - ```sh - CODER_OIDC_GROUP_MAPPING='{"myOIDCGroupID": "myCoderGroupName"}' - ``` - -- As a flag: - - ```sh - --oidc-group-mapping '{"myOIDCGroupID": "myCoderGroupName"}' - ``` - -Below is an example mapping in the Coder Helm chart: - -```yaml -coder: - env: - - name: CODER_OIDC_GROUP_MAPPING - value: > - {"myOIDCGroupID": "myCoderGroupName"} -``` - -From the example above, users that belong to the `myOIDCGroupID` group in your -OIDC provider will be added to the `myCoderGroupName` group in Coder. - -## Runtime (Organizations) - -
- -You must have a Premium license with Organizations enabled to use this. -[Contact your account team](https://coder.com/contact) for more details. - -
- -For deployments with multiple [organizations](./organizations.md), you must -configure group sync at the organization level. In future Coder versions, you -will be able to configure this in the UI. For now, you must use CLI commands. +### CLI 1. Confirm you have the [Coder CLI](../../install/index.md) installed and are - logged in with a user who is an Owner or Organization Admin role. - -1. Confirm that your OIDC provider is sending a groups claim. - - Log in with OIDC and visit the following URL: - - ```text - https://[coder.example.com]/api/v2/debug/[your-username]/debug-link - ``` - - You should see a field in either `id_token_claims`, `user_info_claims` or - both followed by a list of the user's OIDC groups in the response. This is - the [claim](https://openid.net/specs/openid-connect-core-1_0.html#Claims) - sent by the OIDC provider. - - See [Troubleshooting](#troubleshooting-grouproleorganization-sync) to debug - this. - - Depending on the OIDC provider, this claim may be called something else. - Common names include `groups`, `memberOf`, and `roles`. + logged in with a user who is an Owner or has an Organization Admin role. 1. To fetch the current group sync settings for an organization, run the following: @@ -165,7 +112,7 @@ Below is an example that uses the `groups` claim and maps all groups prefixed by
-You much specify Coder group IDs instead of group names. The fastest way to find +You must specify Coder group IDs instead of group names. The fastest way to find the ID for a corresponding group is by visiting `https://coder.example.com/api/v2/groups`. @@ -200,102 +147,110 @@ coder organizations settings set group-sync \ Visit the Coder UI to confirm these changes: -![IDP Sync](../../images/admin/users/organizations/group-sync.png) +![IdP Sync](../../images/admin/users/organizations/group-sync.png) -
+### Server Flags -### Group allowlist +
-You can limit which groups from your identity provider can log in to Coder with -[CODER_OIDC_ALLOWED_GROUPS](https://coder.com/docs/cli/server#--oidc-allowed-groups). -Users who are not in a matching group will see the following error: +Use server flags only with Coder deployments with a single organization. -Unauthorized group error +You can use the dashboard to configure group sync instead. -## Role Sync +
-
+1. Configure the Coder server to read groups from the claim name with the + [OIDC group field](../../reference/cli/server.md#--oidc-group-field) server + flag: -Role sync is an Enterprise and Premium feature. -[Learn more](https://coder.com/pricing#compare-plans). + - Environment variable: -
+ ```sh + CODER_OIDC_GROUP_FIELD=groups + ``` -If your OpenID Connect provider supports roles claims, you can configure Coder -to synchronize roles in your auth provider to roles within Coder. + - As a flag: -There are 2 ways to do role sync. Server Flags assign site wide roles, and -runtime org role sync assigns organization roles + ```sh + --oidc-group-field groups + ``` -
+1. On login, users will automatically be assigned to groups that have matching + names in Coder and removed from groups that the user no longer belongs to. -You must have a Premium license with Organizations enabled to use this. -[Contact your account team](https://coder.com/contact) for more details. +1. For cases when an OIDC provider only returns group IDs or you want to have + different group names in Coder than in your OIDC provider, you can configure + mapping between the two with the + [OIDC group mapping](../../reference/cli/server.md#--oidc-group-mapping) server + flag: -
+ - Environment variable: -
+ ```sh + CODER_OIDC_GROUP_MAPPING='{"myOIDCGroupID": "myCoderGroupName"}' + ``` + + - As a flag: -## Server Flags + ```sh + --oidc-group-mapping '{"myOIDCGroupID": "myCoderGroupName"}' + ``` -1. Confirm that your OIDC provider is sending a roles claim by logging in with - OIDC and visiting the following URL with an `Owner` account: + Below is an example mapping in the Coder Helm chart: - ```text - https://[coder.example.com]/api/v2/debug/[your-username]/debug-link + ```yaml + coder: + env: + - name: CODER_OIDC_GROUP_MAPPING + value: > + {"myOIDCGroupID": "myCoderGroupName"} ``` - You should see a field in either `id_token_claims`, `user_info_claims` or - both followed by a list of the user's OIDC roles in the response. This is the - [claim](https://openid.net/specs/openid-connect-core-1_0.html#Claims) sent by - the OIDC provider. + From this example, users that belong to the `myOIDCGroupID` group in your + OIDC provider will be added to the `myCoderGroupName` group in Coder. - See [Troubleshooting](#troubleshooting-grouproleorganization-sync) to debug - this. +
- Depending on the OIDC provider, this claim may be called something else. +### Group allowlist -1. Configure the Coder server to read groups from the claim name with the - [OIDC role field](../../reference/cli/server.md#--oidc-user-role-field) - server flag: +You can limit which groups from your identity provider can log in to Coder with +[CODER_OIDC_ALLOWED_GROUPS](https://coder.com/docs/cli/server#--oidc-allowed-groups). +Users who are not in a matching group will see the following error: -1. Set the following in your Coder server [configuration](../setup/index.md). +Unauthorized group error - ```env - # Depending on your identity provider configuration, you may need to explicitly request a "roles" scope - CODER_OIDC_SCOPES=openid,profile,email,roles +## Role Sync - # The following fields are required for role sync: - CODER_OIDC_USER_ROLE_FIELD=roles - CODER_OIDC_USER_ROLE_MAPPING='{"TemplateAuthor":["template-admin","user-admin"]}' - ``` +If your OpenID Connect provider supports roles claims, you can configure Coder +to synchronize roles in your auth provider to roles within Coder. -One role from your identity provider can be mapped to many roles in Coder. The -example above maps to two roles in Coder. +For deployments with multiple [organizations](./organizations.md), configure +role sync at the organization level. -## Runtime (Organizations) +
-For deployments with multiple [organizations](./organizations.md), you can -configure role sync at the organization level. In future Coder versions, you -will be able to configure this in the UI. For now, you must use CLI commands. +### Dashboard -1. Confirm that your OIDC provider is sending a roles claim. +1. As an Owner or Organization Admin, go to **Admin settings**, select + **Organizations**, then **IdP Sync**. - Log in with OIDC and visit the following URL with an `Owner` account: +1. Select the **Role sync settings** tab: - ```text - https://[coder.example.com]/api/v2/debug/[your-username]/debug-link - ``` + ![IdP Sync - Role sync settings](../../images/admin/users/organizations/role-sync-empty.png) + +1. Enter the **Role sync field**, then select **Save**. - You should see a field in either `id_token_claims`, `user_info_claims` or - both followed by a list of the user's OIDC roles in the response. This is the - [claim](https://openid.net/specs/openid-connect-core-1_0.html#Claims) sent by - the OIDC provider. +1. Enter the **IdP role name** and **Coder role**, then **Add IdP role**. - See [Troubleshooting](#troubleshooting-grouproleorganization-sync) to debug - this. + To add a new custom role, select **Roles** from the sidebar, then + **Create custom role**. - Depending on the OIDC provider, this claim may be called something else. + Visit the [groups and roles documentation](./groups-roles.md) for more information. + +### CLI + +1. Confirm you have the [Coder CLI](../../install/index.md) installed and are + logged in with a user who is an Owner or has an Organization Admin role. 1. To fetch the current group sync settings for an organization, run the following: @@ -316,7 +271,7 @@ will be able to configure this in the UI. For now, you must use CLI commands. ``` Below is an example that uses the `roles` claim and maps `coder-admins` from the -IDP as an `Organization Admin` and also maps to a custom `provisioner-admin` +IdP as an `Organization Admin` and also maps to a custom `provisioner-admin` role: ```json @@ -332,7 +287,7 @@ role:
Be sure to use the `name` field for each role, not the display name. Use -`coder organization roles show --org=` to see roles for your +`coder organization roles show --org=` to see roles for your organization.
@@ -347,19 +302,40 @@ coder organizations settings set role-sync \ Visit the Coder UI to confirm these changes: -![IDP Sync](../../images/admin/users/organizations/role-sync.png) +![IdP Sync](../../images/admin/users/organizations/role-sync.png) -
+### Server Flags -## Organization Sync +
-
+Use server flags only with Coder deployments with a single organization. -Organization sync is an Enterprise and Premium feature. -[Learn more](https://coder.com/pricing#compare-plans). +You can use the dashboard to configure role sync instead.
+1. Configure the Coder server to read groups from the claim name with the + [OIDC role field](../../reference/cli/server.md#--oidc-user-role-field) + server flag: + +1. Set the following in your Coder server [configuration](../setup/index.md). + + ```env + # Depending on your identity provider configuration, you may need to explicitly request a "roles" scope + CODER_OIDC_SCOPES=openid,profile,email,roles + + # The following fields are required for role sync: + CODER_OIDC_USER_ROLE_FIELD=roles + CODER_OIDC_USER_ROLE_MAPPING='{"TemplateAuthor":["template-admin","user-admin"]}' + ``` + +One role from your identity provider can be mapped to many roles in Coder. The +example above maps to two roles in Coder. + + + +## Organization Sync + If your OpenID Connect provider supports groups/role claims, you can configure Coder to synchronize claims in your auth provider to organizations within Coder. @@ -370,28 +346,11 @@ Organization sync works across all organizations. On user login, the sync will add and remove the user from organizations based on their IdP claims. After the sync, the user's state should match that of the IdP. -You can initiate an organization sync through the CLI or through the Coder -dashboard: +You can initiate an organization sync through the Coder dashboard or CLI:
-## Dashboard - -1. Confirm that your OIDC provider is sending claims. Log in with OIDC and visit - the following URL with an `Owner` account: - - ```text - https://[coder.example.com]/api/v2/debug/[your-username]/debug-link - ``` - - You should see a field in either `id_token_claims`, `user_info_claims` or - both followed by a list of the user's OIDC groups in the response. This is - the [claim](https://openid.net/specs/openid-connect-core-1_0.html#Claims) - sent by the OIDC provider. See - [Troubleshooting](#troubleshooting-grouproleorganization-sync) to debug this. - - Depending on the OIDC provider, this claim may be called something else. - Common names include `groups`, `memberOf`, and `roles`. +### Dashboard 1. Fetch the corresponding organization IDs using the following endpoint: @@ -400,7 +359,7 @@ dashboard: ``` 1. As a Coder organization user admin or site-wide user admin, go to - **Settings** > **IdP organization sync**. + **Admin settings** > **Deployment** and select **IdP organization sync**. 1. In the **Organization sync field** text box, enter the organization claim, then select **Save**. @@ -415,7 +374,7 @@ dashboard: ![IdP organization sync](../../images/admin/users/organizations/idp-org-sync.png) -## CLI +### CLI Use the Coder CLI to show and adjust the settings. @@ -467,11 +426,11 @@ settings, a user's memberships will update when they log out and log back in. ## Troubleshooting group/role/organization sync -Some common issues when enabling group/role sync. +Some common issues when enabling group, role, or organization sync. ### General guidelines -If you are running into issues with group/role sync: +If you are running into issues with a sync: 1. View your Coder server logs and enable [verbose mode](../../reference/cli/index.md#-v---verbose). @@ -487,7 +446,7 @@ If you are running into issues with group/role sync: 1. Attempt to log in, preferably with a user who has the `Owner` role. -The logs for a successful group sync look like this (human-readable): +The logs for a successful sync look like this (human-readable): ```sh [debu] coderd.userauth: got oidc claims request_id=49e86507-6842-4b0b-94d4-f245e62e49f3 source=id_token claim_fields="[aio aud email exp groups iat idp iss name nbf oid preferred_username rh sub tid uti ver]" blank=[] @@ -552,7 +511,7 @@ The application '' asked for scope 'groups' that doesn't exist This can happen because the identity provider has a different name for the scope. For example, Azure AD uses `GroupMember.Read.All` instead of `groups`. -You can find the correct scope name in the IDP's documentation. Some IDP's allow +You can find the correct scope name in the IdP's documentation. Some IdPs allow configuring the name of this scope. The solution is to update the value of `CODER_OIDC_SCOPES` to the correct value @@ -562,15 +521,15 @@ for the identity provider. Steps to troubleshoot. -1. Ensure the user is a part of a group in the IDP. If the user has 0 groups, no +1. Ensure the user is a part of a group in the IdP. If the user has 0 groups, no `groups` claim will be sent. 2. Check if another claim appears to be the correct claim with a different name. A common name is `memberOf` instead of `groups`. If this is present, update `CODER_OIDC_GROUP_FIELD=memberOf`. -3. Make sure the number of groups being sent is under the limit of the IDP. Some - IDPs will return an error, while others will just omit the `groups` claim. A +3. Make sure the number of groups being sent is under the limit of the IdP. Some + IdPs will return an error, while others will just omit the `groups` claim. A common solution is to create a filter on the identity provider that returns - less than the limit for your IDP. + less than the limit for your IdP. - [Azure AD limit is 200, and omits groups if exceeded.](https://learn.microsoft.com/en-us/azure/active-directory/hybrid/connect/how-to-connect-fed-group-claims#options-for-applications-to-consume-group-information) - [Okta limit is 100, and returns an error if exceeded.](https://developer.okta.com/docs/reference/api/oidc/#scope-dependent-claims-not-always-returned) @@ -582,32 +541,37 @@ Below are some details specific to individual OIDC providers. > **Note:** Tested on ADFS 4.0, Windows Server 2019 -1. In your Federation Server, create a new application group for Coder. Follow - the steps as described - [here.](https://learn.microsoft.com/en-us/windows-server/identity/ad-fs/development/msal/adfs-msal-web-app-web-api#app-registration-in-ad-fs) +1. In your Federation Server, create a new application group for Coder. + Follow the steps as described in the [Windows Server documentation] + (https://learn.microsoft.com/en-us/windows-server/identity/ad-fs/development/msal/adfs-msal-web-app-web-api#app-registration-in-ad-fs). + - **Server Application**: Note the Client ID. - **Configure Application Credentials**: Note the Client Secret. - **Configure Web API**: Set the Client ID as the relying party identifier. - **Application Permissions**: Allow access to the claims `openid`, `email`, `profile`, and `allatclaims`. + 1. Visit your ADFS server's `/.well-known/openid-configuration` URL and note the value for `issuer`. - > **Note:** This is usually of the form - > `https://adfs.corp/adfs/.well-known/openid-configuration` + + This will look something like + `https://adfs.corp/adfs/.well-known/openid-configuration`. + 1. In Coder's configuration file (or Helm values as appropriate), set the following environment variables or their corresponding CLI arguments: - - `CODER_OIDC_ISSUER_URL`: the `issuer` value from the previous step. - - `CODER_OIDC_CLIENT_ID`: the Client ID from step 1. - - `CODER_OIDC_CLIENT_SECRET`: the Client Secret from step 1. + - `CODER_OIDC_ISSUER_URL`: `issuer` value from the previous step. + - `CODER_OIDC_CLIENT_ID`: Client ID from step 1. + - `CODER_OIDC_CLIENT_SECRET`: Client Secret from step 1. - `CODER_OIDC_AUTH_URL_PARAMS`: set to - ```console + ```json {"resource":"$CLIENT_ID"} ``` - where `$CLIENT_ID` is the Client ID from step 1 - ([see here](https://learn.microsoft.com/en-us/windows-server/identity/ad-fs/overview/ad-fs-openid-connect-oauth-flows-scenarios#:~:text=scope%E2%80%AFopenid.-,resource,-optional)). + Where `$CLIENT_ID` is the Client ID from step 1. + Consult the Microsoft [AD FS OpenID Connect/OAuth flows and Application Scenarios documentation](https://learn.microsoft.com/en-us/windows-server/identity/ad-fs/overview/ad-fs-openid-connect-oauth-flows-scenarios#:~:text=scope%E2%80%AFopenid.-,resource,-optional) for more information. + This is required for the upstream OIDC provider to return the requested claims. @@ -615,34 +579,35 @@ Below are some details specific to individual OIDC providers. 1. Configure [Issuance Transform Rules](https://learn.microsoft.com/en-us/windows-server/identity/ad-fs/operations/create-a-rule-to-send-ldap-attributes-as-claims) - on your federation server to send the following claims: + on your Federation Server to send the following claims: - `preferred_username`: You can use e.g. "Display Name" as required. - `email`: You can use e.g. the LDAP attribute "E-Mail-Addresses" as required. - `email_verified`: Create a custom claim rule: - ```console + ```json => issue(Type = "email_verified", Value = "true") ``` - (Optional) If using Group Sync, send the required groups in the configured - groups claim field. See [here](https://stackoverflow.com/a/55570286) for an - example. + groups claim field. + Use [this answer from Stack Overflow](https://stackoverflow.com/a/55570286) for an example. ### Keycloak -The access_type parameter has two possible values: "online" and "offline." By -default, the value is set to "offline". This means that when a user -authenticates using OIDC, the application requests offline access to the user's -resources, including the ability to refresh access tokens without requiring the -user to reauthenticate. +The `access_type` parameter has two possible values: `online` and `offline`. +By default, the value is set to `offline`. + +This means that when a user authenticates using OIDC, the application requests +offline access to the user's resources, including the ability to refresh access +tokens without requiring the user to reauthenticate. -To enable the `offline_access` scope, which allows for the refresh token +To enable the `offline_access` scope which allows for the refresh token functionality, you need to add it to the list of requested scopes during the -authentication flow. Including the `offline_access` scope in the requested -scopes ensures that the user is granted the necessary permissions to obtain -refresh tokens. +authentication flow. +Including the `offline_access` scope in the requested scopes ensures that the +user is granted the necessary permissions to obtain refresh tokens. By combining the `{"access_type":"offline"}` parameter in the OIDC Auth URL with the `offline_access` scope, you can achieve the desired behavior of obtaining diff --git a/docs/admin/users/oidc-auth.md b/docs/admin/users/oidc-auth.md index fc4b0fd6559d7..5c46c5781670c 100644 --- a/docs/admin/users/oidc-auth.md +++ b/docs/admin/users/oidc-auth.md @@ -11,16 +11,7 @@ Your OIDC provider will ask you for the following parameter: ## Step 2: Configure Coder with the OpenID Connect credentials -Navigate to your Coder host and run the following command to start up the Coder -server: - -```shell -coder server --oidc-issuer-url="https://issuer.corp.com" --oidc-email-domain="your-domain-1,your-domain-2" --oidc-client-id="533...des" --oidc-client-secret="G0CSP...7qSM" -``` - -If you are running Coder as a system service, you can achieve the same result as -the command above by adding the following environment variables to the -`/etc/coder.d/coder.env` file: +Set the following environment variables on your Coder deployment and restart Coder: ```env CODER_OIDC_ISSUER_URL="https://issuer.corp.com" @@ -29,30 +20,6 @@ CODER_OIDC_CLIENT_ID="533...des" CODER_OIDC_CLIENT_SECRET="G0CSP...7qSM" ``` -Once complete, run `sudo service coder restart` to reboot Coder. - -If deploying Coder via Helm, you can set the above environment variables in the -`values.yaml` file as such: - -```yaml -coder: - env: - - name: CODER_OIDC_ISSUER_URL - value: "https://issuer.corp.com" - - name: CODER_OIDC_EMAIL_DOMAIN - value: "your-domain-1,your-domain-2" - - name: CODER_OIDC_CLIENT_ID - value: "533...des" - - name: CODER_OIDC_CLIENT_SECRET - value: "G0CSP...7qSM" -``` - -To upgrade Coder, run: - -```shell -helm upgrade coder-v2/coder -n -f values.yaml -``` - ## OIDC Claims When a user logs in for the first time via OIDC, Coder will merge both the diff --git a/docs/admin/users/organizations.md b/docs/admin/users/organizations.md index 9c832132f7a3a..5a4b805f7c954 100644 --- a/docs/admin/users/organizations.md +++ b/docs/admin/users/organizations.md @@ -23,14 +23,14 @@ guide. All Coder deployments start with one organization called `coder`. All new users are added to this organization by default. -To edit the organization details, select **Deployment** from the top bar, then +To edit the organization details, select **Admin settings** from the top bar, then **Organizations**: -![Organizations Menu](../../images/admin/users/organizations/deployment-organizations.png) +Organizations Menu From there, you can manage the name, icon, description, users, and groups: -![Organization Settings](../../images/admin/users/organizations/default-organization.png) +![Organization Settings](../../images/admin/users/organizations/default-organization-settings.png) ## Additional organizations @@ -52,10 +52,19 @@ identity provider to avoid manually assigning users to organizations. ### 1. Create the organization -In the sidebar, select **New organization** to create an organization. In this -example, we'll create the `data-platform` org. +To create a new organization: -![New Organization](../../images/admin/users/organizations/new-organization.png) +1. Select **Admin settings** from the top bar, then **Organizations**. + +1. Select the current organization to expand the organizations dropdown, then select **Create Organization**: + + Organizations dropdown and Create Organization + +1. Enter the details and select **Save** to continue: + + New Organization + +In this example, we'll create the `data-platform` org. Next deploy a provisioner and template for this organization. @@ -97,11 +106,11 @@ Once you've started a provisioner, you can create a template. You'll notice the ### 4. Add members -From **Administration > Settings**, select **Organizations** to add members to -your organization. Once added, they will be able to see the +From **Admin settings**, select **Organizations**, then **Members** to add members to +your organization. Once added, members will be able to see the organization-specific templates. -![Add members](../../images/admin/users/organizations/organization-members.png) +Add members ### 5. Create a workspace @@ -110,12 +119,6 @@ their organization. Users can be in multiple organizations. ![Workspace List](../../images/admin/users/organizations/workspace-list.png) -## Beta - -Organizations is in beta. If you encounter any issues, please -[file an issue](https://github.com/coder/internal/issues/new?title=request%28orgs%29%3A+request+title+here&labels=["customer-feedback"]&body=please+enter+your+issue+or+request+here) -or contact your account team. - ## Next steps - [Organizations - best practices](../../tutorials/best-practices/organizations.md) diff --git a/docs/admin/users/sessions-tokens.md b/docs/admin/users/sessions-tokens.md index dbbcfb82dfd47..6332b8182fc17 100644 --- a/docs/admin/users/sessions-tokens.md +++ b/docs/admin/users/sessions-tokens.md @@ -51,10 +51,28 @@ See the help docs for ### Generate a long-lived API token on behalf of another user -Today, you must use the REST API to generate a token on behalf of another user. -You must have the `Owner` role to do this. Use our API reference for more -information: -[Create token API key](https://coder.com/docs/reference/api/users#create-token-api-key) +You must have the `Owner` role to generate a token for another user. + +As of Coder v2.17+, you can use the CLI or API to create long-lived tokens on +behalf of other users. Use the API for earlier versions of Coder. + +
+ +#### CLI + +```sh +coder tokens create my-token --user +``` + +See the full CLI reference for +[`coder tokens create`](../../reference/cli/tokens_create.md) + +#### API + +Use our API reference for more information on how to +[create token API key](../../reference/api/users.md#create-token-api-key) + +
### Set max token length diff --git a/docs/images/add-license-ui.png b/docs/images/add-license-ui.png deleted file mode 100644 index 837698908e8f1..0000000000000 Binary files a/docs/images/add-license-ui.png and /dev/null differ diff --git a/docs/images/admin/admin-settings-general.png b/docs/images/admin/admin-settings-general.png new file mode 100644 index 0000000000000..d3447ac45d2c0 Binary files /dev/null and b/docs/images/admin/admin-settings-general.png differ diff --git a/docs/images/admin/licenses/add-license-ui.png b/docs/images/admin/licenses/add-license-ui.png new file mode 100644 index 0000000000000..bfb91395595f8 Binary files /dev/null and b/docs/images/admin/licenses/add-license-ui.png differ diff --git a/docs/images/admin/licenses/licenses-nolicense.png b/docs/images/admin/licenses/licenses-nolicense.png new file mode 100644 index 0000000000000..69bb5dc25b820 Binary files /dev/null and b/docs/images/admin/licenses/licenses-nolicense.png differ diff --git a/docs/images/admin/licenses/licenses-screen.png b/docs/images/admin/licenses/licenses-screen.png new file mode 100644 index 0000000000000..45fbd5d6c5cf8 Binary files /dev/null and b/docs/images/admin/licenses/licenses-screen.png differ diff --git a/docs/images/admin/users/organizations/admin-settings-orgs.png b/docs/images/admin/users/organizations/admin-settings-orgs.png new file mode 100644 index 0000000000000..c33ef423e2552 Binary files /dev/null and b/docs/images/admin/users/organizations/admin-settings-orgs.png differ diff --git a/docs/images/admin/users/organizations/default-organization-settings.png b/docs/images/admin/users/organizations/default-organization-settings.png new file mode 100644 index 0000000000000..58d8113f337b9 Binary files /dev/null and b/docs/images/admin/users/organizations/default-organization-settings.png differ diff --git a/docs/images/admin/users/organizations/default-organization.png b/docs/images/admin/users/organizations/default-organization.png deleted file mode 100644 index 183d622beafad..0000000000000 Binary files a/docs/images/admin/users/organizations/default-organization.png and /dev/null differ diff --git a/docs/images/admin/users/organizations/deployment-organizations.png b/docs/images/admin/users/organizations/deployment-organizations.png deleted file mode 100644 index ab3340f337f82..0000000000000 Binary files a/docs/images/admin/users/organizations/deployment-organizations.png and /dev/null differ diff --git a/docs/images/admin/users/organizations/group-sync-empty.png b/docs/images/admin/users/organizations/group-sync-empty.png new file mode 100644 index 0000000000000..4114ec7cacd8f Binary files /dev/null and b/docs/images/admin/users/organizations/group-sync-empty.png differ diff --git a/docs/images/admin/users/organizations/group-sync.png b/docs/images/admin/users/organizations/group-sync.png index a4013f2f15559..f617dd02eeef0 100644 Binary files a/docs/images/admin/users/organizations/group-sync.png and b/docs/images/admin/users/organizations/group-sync.png differ diff --git a/docs/images/admin/users/organizations/new-organization.png b/docs/images/admin/users/organizations/new-organization.png index 26fda5222af55..503fda8cf5ee5 100644 Binary files a/docs/images/admin/users/organizations/new-organization.png and b/docs/images/admin/users/organizations/new-organization.png differ diff --git a/docs/images/admin/users/organizations/org-dropdown-create.png b/docs/images/admin/users/organizations/org-dropdown-create.png new file mode 100644 index 0000000000000..d0d61921cb10c Binary files /dev/null and b/docs/images/admin/users/organizations/org-dropdown-create.png differ diff --git a/docs/images/admin/users/organizations/organization-members.png b/docs/images/admin/users/organizations/organization-members.png index d3d29b3bd113f..fa799eabfd5b1 100644 Binary files a/docs/images/admin/users/organizations/organization-members.png and b/docs/images/admin/users/organizations/organization-members.png differ diff --git a/docs/images/admin/users/organizations/role-sync-empty.png b/docs/images/admin/users/organizations/role-sync-empty.png new file mode 100644 index 0000000000000..91e36fff5bf02 Binary files /dev/null and b/docs/images/admin/users/organizations/role-sync-empty.png differ diff --git a/docs/images/admin/users/organizations/role-sync.png b/docs/images/admin/users/organizations/role-sync.png index 1b0fafb39fae1..9360c9e1337aa 100644 Binary files a/docs/images/admin/users/organizations/role-sync.png and b/docs/images/admin/users/organizations/role-sync.png differ diff --git a/docs/images/admin/users/organizations/template-org-picker.png b/docs/images/admin/users/organizations/template-org-picker.png index 73c37ed517aec..cf5d80761902c 100644 Binary files a/docs/images/admin/users/organizations/template-org-picker.png and b/docs/images/admin/users/organizations/template-org-picker.png differ diff --git a/docs/images/admin/users/organizations/workspace-list.png b/docs/images/admin/users/organizations/workspace-list.png index bbe6cca9eb909..e007cdaf8734a 100644 Binary files a/docs/images/admin/users/organizations/workspace-list.png and b/docs/images/admin/users/organizations/workspace-list.png differ diff --git a/docs/images/guides/using-organizations/default-organization.png b/docs/images/guides/using-organizations/default-organization.png deleted file mode 100644 index 183d622beafad..0000000000000 Binary files a/docs/images/guides/using-organizations/default-organization.png and /dev/null differ diff --git a/docs/images/integrations/platformx-screenshot.png b/docs/images/integrations/platformx-screenshot.png new file mode 100644 index 0000000000000..20bffb215a931 Binary files /dev/null and b/docs/images/integrations/platformx-screenshot.png differ diff --git a/docs/images/screenshots/welcome-create-admin-user.png b/docs/images/screenshots/welcome-create-admin-user.png index 2d4c0b9bb7835..de78b48c7ea26 100644 Binary files a/docs/images/screenshots/welcome-create-admin-user.png and b/docs/images/screenshots/welcome-create-admin-user.png differ diff --git a/docs/install/docker.md b/docs/install/docker.md index 61da25d99e296..d1b2c2c109905 100644 --- a/docs/install/docker.md +++ b/docs/install/docker.md @@ -56,27 +56,43 @@ which includes an PostgreSQL container and volume. 1. Make sure you have [Docker Compose](https://docs.docker.com/compose/install/) installed. -2. Download the +1. Download the [`docker-compose.yaml`](https://github.com/coder/coder/blob/main/docker-compose.yaml) file. -3. Update `group_add:` in `docker-compose.yaml` with the `gid` of `docker` +1. Update `group_add:` in `docker-compose.yaml` with the `gid` of `docker` group. You can get the `docker` group `gid` by running the below command: ```shell getent group docker | cut -d: -f3 ``` -4. Start Coder with `docker compose up` +1. Start Coder with `docker compose up` -5. Visit the web UI via the configured url. +1. Visit the web UI via the configured url. -6. Follow the on-screen instructions log in and create your first template and +1. Follow the on-screen instructions log in and create your first template and workspace Coder configuration is defined via environment variables. Learn more about Coder's [configuration options](../admin/setup/index.md). +## Install the preview release + +
+ +We do not recommend using preview releases in production environments. + +
+ +You can install and test a +[preview release of Coder](https://github.com/coder/coder/pkgs/container/coder-preview) +by using the `coder-preview:latest` image tag. +This image is automatically updated with the latest changes from the `main` branch. + +Replace `ghcr.io/coder/coder:latest` in the `docker run` command in the +[steps above](#install-coder-via-docker-run) with `ghcr.io/coder/coder-preview:latest`. + ## Troubleshooting ### Docker-based workspace is stuck in "Connecting..." diff --git a/docs/install/kubernetes.md b/docs/install/kubernetes.md index 7ca8670767b35..9c53eb3dc29ae 100644 --- a/docs/install/kubernetes.md +++ b/docs/install/kubernetes.md @@ -101,11 +101,15 @@ coder: # postgres://coder:password@postgres:5432/coder?sslmode=disable name: coder-db-url key: url + # For production deployments, we recommend configuring your own GitHub + # OAuth2 provider and disabling the default one. + - name: CODER_OAUTH2_GITHUB_DEFAULT_PROVIDER_ENABLE + value: "false" # (Optional) For production deployments the access URL should be set. # If you're just trying Coder, access the dashboard via the service IP. - - name: CODER_ACCESS_URL - value: "https://coder.example.com" + # - name: CODER_ACCESS_URL + # value: "https://coder.example.com" #tls: # secretNames: @@ -129,7 +133,7 @@ We support two release channels: mainline and stable - read the helm install coder coder-v2/coder \ --namespace coder \ --values values.yaml \ - --version 2.18.0 + --version 2.19.0 ``` - **Stable** Coder release: @@ -140,7 +144,7 @@ We support two release channels: mainline and stable - read the helm install coder coder-v2/coder \ --namespace coder \ --values values.yaml \ - --version 2.17.2 + --version 2.18.5 ``` You can watch Coder start up by running `kubectl get pods -n coder`. Once Coder diff --git a/docs/install/offline.md b/docs/install/offline.md index 6a41bd9437894..0f83ae4077ee4 100644 --- a/docs/install/offline.md +++ b/docs/install/offline.md @@ -54,7 +54,7 @@ RUN mkdir -p /opt/terraform # The below step is optional if you wish to keep the existing version. # See https://github.com/coder/coder/blob/main/provisioner/terraform/install.go#L23-L24 # for supported Terraform versions. -ARG TERRAFORM_VERSION=1.9.8 +ARG TERRAFORM_VERSION=1.10.5 RUN apk update && \ apk del terraform && \ curl -LOs https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip \ diff --git a/docs/install/other/index.md b/docs/install/other/index.md index 3809d86812526..f727e5c34bf55 100644 --- a/docs/install/other/index.md +++ b/docs/install/other/index.md @@ -5,8 +5,6 @@ welcome! | Platform Name | Status | Documentation | |-----------------------------------------------------------------------------------|------------|----------------------------------------------------------------------------------------------| -| AWS EC2 | Official | [Guide: AWS](../cloud/ec2.md) | -| Google Compute Engine | Official | [Guide: Google Compute Engine](../cloud/compute-engine.md) | | Azure AKS | Unofficial | [GitHub: coder-aks](https://github.com/ericpaulsen/coder-aks) | | Terraform (GKE, AKS, LKE, DOKS, IBMCloud K8s, OVHCloud K8s, Scaleway K8s Kapsule) | Unofficial | [GitHub: coder-oss-terraform](https://github.com/ElliotG/coder-oss-tf) | | Fly.io | Unofficial | [Blog: Run Coder on Fly.io](https://coder.com/blog/remote-developer-environments-on-fly-io) | diff --git a/docs/install/releases.md b/docs/install/releases.md index a32f2f4fb9eec..157adf7fe8961 100644 --- a/docs/install/releases.md +++ b/docs/install/releases.md @@ -10,7 +10,7 @@ deployment. ## Release channels We support two release channels: -[mainline](https://github.com/coder/coder/releases/tag/v2.16.0) for the bleeding +[mainline](https://github.com/coder/coder/releases/tag/v2.19.0) for the bleeding edge version of Coder and [stable](https://github.com/coder/coder/releases/latest) for those with lower tolerance for fault. We field our mainline releases publicly for one month @@ -63,7 +63,7 @@ pages. | 2.16.x | October 01, 2024 | Security Support | | 2.17.x | November 05, 2024 | Security Support | | 2.18.x | December 03, 2024 | Stable | -| 2.19.x | February 04, 2024 | Not Released | +| 2.19.x | February 04, 2024 | Mainline | > **Tip**: We publish a > [`preview`](https://github.com/coder/coder/pkgs/container/coder-preview) image @@ -77,5 +77,4 @@ pages. v2.18 was promoted to stable on January 7th, 2025. -Effective starting January, 2025 we will skip the January release each year because most of our engineering team is out for the December holiday period. -We'll return to our regular release cadence on February 4th. +As of January, 2025 we skip the January release each year because most of our engineering team is out for the December holiday period. diff --git a/docs/manifest.json b/docs/manifest.json index 7cf556984ecbf..2da08f84d6419 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -284,14 +284,14 @@ "state": ["enterprise", "premium"] }, { - "title": "IDP Sync", + "title": "IdP Sync", "path": "./admin/users/idp-sync.md", "state": ["enterprise", "premium"] }, { "title": "Organizations", "path": "./admin/users/organizations.md", - "state": ["premium", "beta"] + "state": ["premium"] }, { "title": "Quotas", @@ -504,6 +504,11 @@ "description": "Integrate Coder with Island's Secure Browser", "path": "./admin/integrations/island.md" }, + { + "title": "DX PlatformX", + "description": "Integrate Coder with DX PlatformX", + "path": "./admin/integrations/platformx.md" + }, { "title": "Hashicorp Vault", "description": "Integrate Coder with Hashicorp Vault", @@ -1033,6 +1038,11 @@ "description": "Resume notifications", "path": "reference/cli/notifications_resume.md" }, + { + "title": "notifications test", + "description": "Send a test notification", + "path": "reference/cli/notifications_test.md" + }, { "title": "open", "description": "Open a workspace", diff --git a/docs/reference/api/agents.md b/docs/reference/api/agents.md index 22ebe7f35530f..38e30c35e18cd 100644 --- a/docs/reference/api/agents.md +++ b/docs/reference/api/agents.md @@ -638,6 +638,71 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/con To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Get running containers for workspace agent + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/containers?label=string \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /workspaceagents/{workspaceagent}/containers` + +### Parameters + +| Name | In | Type | Required | Description | +|------------------|-------|-------------------|----------|--------------------| +| `workspaceagent` | path | string(uuid) | true | Workspace agent ID | +| `label` | query | string(key=value) | true | Labels | + +### Example responses + +> 200 Response + +```json +{ + "containers": [ + { + "created_at": "2019-08-24T14:15:22Z", + "id": "string", + "image": "string", + "labels": { + "property1": "string", + "property2": "string" + }, + "name": "string", + "ports": [ + { + "network": "string", + "port": 0, + "process_name": "string" + } + ], + "running": true, + "status": "string", + "volumes": { + "property1": "string", + "property2": "string" + } + } + ], + "warnings": [ + "string" + ] +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|----------------------------------------------------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.WorkspaceAgentListContainersResponse](schemas.md#codersdkworkspaceagentlistcontainersresponse) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Coordinate workspace agent ### Code samples diff --git a/docs/reference/api/builds.md b/docs/reference/api/builds.md index 8c17b95a4b7a4..26f6df4a55b73 100644 --- a/docs/reference/api/builds.md +++ b/docs/reference/api/builds.md @@ -50,6 +50,15 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" }, + "metadata": { + "template_display_name": "string", + "template_icon": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "template_name": "string", + "template_version_name": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", + "workspace_name": "string" + }, "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, @@ -255,6 +264,15 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild} \ "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" }, + "metadata": { + "template_display_name": "string", + "template_icon": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "template_name": "string", + "template_version_name": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", + "workspace_name": "string" + }, "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, @@ -902,6 +920,15 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/sta "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" }, + "metadata": { + "template_display_name": "string", + "template_icon": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "template_name": "string", + "template_version_name": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", + "workspace_name": "string" + }, "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, @@ -1180,6 +1207,15 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" }, + "metadata": { + "template_display_name": "string", + "template_icon": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "template_name": "string", + "template_version_name": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", + "workspace_name": "string" + }, "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, @@ -1363,6 +1399,14 @@ Status Code **200** | `»»» error` | string | false | | | | `»»» template_version_id` | string(uuid) | false | | | | `»»» workspace_build_id` | string(uuid) | false | | | +| `»» metadata` | [codersdk.ProvisionerJobMetadata](schemas.md#codersdkprovisionerjobmetadata) | false | | | +| `»»» template_display_name` | string | false | | | +| `»»» template_icon` | string | false | | | +| `»»» template_id` | string(uuid) | false | | | +| `»»» template_name` | string | false | | | +| `»»» template_version_name` | string | false | | | +| `»»» workspace_id` | string(uuid) | false | | | +| `»»» workspace_name` | string | false | | | | `»» organization_id` | string(uuid) | false | | | | `»» queue_position` | integer | false | | | | `»» queue_size` | integer | false | | | @@ -1605,6 +1649,15 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" }, + "metadata": { + "template_display_name": "string", + "template_icon": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "template_name": "string", + "template_version_name": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", + "workspace_name": "string" + }, "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, diff --git a/docs/reference/api/debug.md b/docs/reference/api/debug.md index 63fd1aeda8f98..93fd3e7b638c2 100644 --- a/docs/reference/api/debug.md +++ b/docs/reference/api/debug.md @@ -309,7 +309,10 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \ "created_at": "2019-08-24T14:15:22Z", "current_job": { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "status": "pending" + "status": "pending", + "template_display_name": "string", + "template_icon": "string", + "template_name": "string" }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "key_id": "1e779c8a-6786-4c89-b7c3-a6666f5fd6b5", @@ -319,7 +322,10 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \ "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "previous_job": { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "status": "pending" + "status": "pending", + "template_display_name": "string", + "template_icon": "string", + "template_name": "string" }, "provisioners": [ "string" diff --git a/docs/reference/api/enterprise.md b/docs/reference/api/enterprise.md index 6f8b061ed9025..282cf20ab252d 100644 --- a/docs/reference/api/enterprise.md +++ b/docs/reference/api/enterprise.md @@ -1629,7 +1629,10 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/provisi "created_at": "2019-08-24T14:15:22Z", "current_job": { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "status": "pending" + "status": "pending", + "template_display_name": "string", + "template_icon": "string", + "template_name": "string" }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "key_id": "1e779c8a-6786-4c89-b7c3-a6666f5fd6b5", @@ -1639,7 +1642,10 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/provisi "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "previous_job": { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "status": "pending" + "status": "pending", + "template_display_name": "string", + "template_icon": "string", + "template_name": "string" }, "provisioners": [ "string" @@ -1676,34 +1682,37 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/provisi Status Code **200** -| Name | Type | Required | Restrictions | Description | -|----------------------|--------------------------------------------------------------------------------|----------|--------------|------------------| -| `[array item]` | array | false | | | -| `» daemons` | array | false | | | -| `»» api_version` | string | false | | | -| `»» created_at` | string(date-time) | false | | | -| `»» current_job` | [codersdk.ProvisionerDaemonJob](schemas.md#codersdkprovisionerdaemonjob) | false | | | -| `»»» id` | string(uuid) | false | | | -| `»»» status` | [codersdk.ProvisionerJobStatus](schemas.md#codersdkprovisionerjobstatus) | false | | | -| `»» id` | string(uuid) | false | | | -| `»» key_id` | string(uuid) | false | | | -| `»» key_name` | string | false | | Optional fields. | -| `»» last_seen_at` | string(date-time) | false | | | -| `»» name` | string | false | | | -| `»» organization_id` | string(uuid) | false | | | -| `»» previous_job` | [codersdk.ProvisionerDaemonJob](schemas.md#codersdkprovisionerdaemonjob) | false | | | -| `»» provisioners` | array | false | | | -| `»» status` | [codersdk.ProvisionerDaemonStatus](schemas.md#codersdkprovisionerdaemonstatus) | false | | | -| `»» tags` | object | false | | | -| `»»» [any property]` | string | false | | | -| `»» version` | string | false | | | -| `» key` | [codersdk.ProvisionerKey](schemas.md#codersdkprovisionerkey) | false | | | -| `»» created_at` | string(date-time) | false | | | -| `»» id` | string(uuid) | false | | | -| `»» name` | string | false | | | -| `»» organization` | string(uuid) | false | | | -| `»» tags` | [codersdk.ProvisionerKeyTags](schemas.md#codersdkprovisionerkeytags) | false | | | -| `»»» [any property]` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|-----------------------------|--------------------------------------------------------------------------------|----------|--------------|------------------| +| `[array item]` | array | false | | | +| `» daemons` | array | false | | | +| `»» api_version` | string | false | | | +| `»» created_at` | string(date-time) | false | | | +| `»» current_job` | [codersdk.ProvisionerDaemonJob](schemas.md#codersdkprovisionerdaemonjob) | false | | | +| `»»» id` | string(uuid) | false | | | +| `»»» status` | [codersdk.ProvisionerJobStatus](schemas.md#codersdkprovisionerjobstatus) | false | | | +| `»»» template_display_name` | string | false | | | +| `»»» template_icon` | string | false | | | +| `»»» template_name` | string | false | | | +| `»» id` | string(uuid) | false | | | +| `»» key_id` | string(uuid) | false | | | +| `»» key_name` | string | false | | Optional fields. | +| `»» last_seen_at` | string(date-time) | false | | | +| `»» name` | string | false | | | +| `»» organization_id` | string(uuid) | false | | | +| `»» previous_job` | [codersdk.ProvisionerDaemonJob](schemas.md#codersdkprovisionerdaemonjob) | false | | | +| `»» provisioners` | array | false | | | +| `»» status` | [codersdk.ProvisionerDaemonStatus](schemas.md#codersdkprovisionerdaemonstatus) | false | | | +| `»» tags` | object | false | | | +| `»»» [any property]` | string | false | | | +| `»» version` | string | false | | | +| `» key` | [codersdk.ProvisionerKey](schemas.md#codersdkprovisionerkey) | false | | | +| `»» created_at` | string(date-time) | false | | | +| `»» id` | string(uuid) | false | | | +| `»» name` | string | false | | | +| `»» organization` | string(uuid) | false | | | +| `»» tags` | [codersdk.ProvisionerKeyTags](schemas.md#codersdkprovisionerkeytags) | false | | | +| `»»» [any property]` | string | false | | | #### Enumerated Values @@ -1953,6 +1962,141 @@ curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization}/setti To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Update group IdP Sync config + +### Code samples + +```shell +# Example request using curl +curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization}/settings/idpsync/groups/config \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`PATCH /organizations/{organization}/settings/idpsync/groups/config` + +> Body parameter + +```json +{ + "auto_create_missing_groups": true, + "field": "string", + "regex_filter": {} +} +``` + +### Parameters + +| Name | In | Type | Required | Description | +|----------------|------|----------------------------------------------------------------------------------------------|----------|-------------------------| +| `organization` | path | string(uuid) | true | Organization ID or name | +| `body` | body | [codersdk.PatchGroupIDPSyncConfigRequest](schemas.md#codersdkpatchgroupidpsyncconfigrequest) | true | New config values | + +### Example responses + +> 200 Response + +```json +{ + "auto_create_missing_groups": true, + "field": "string", + "legacy_group_name_mapping": { + "property1": "string", + "property2": "string" + }, + "mapping": { + "property1": [ + "string" + ], + "property2": [ + "string" + ] + }, + "regex_filter": {} +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|--------------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.GroupSyncSettings](schemas.md#codersdkgroupsyncsettings) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + +## Update group IdP Sync mapping + +### Code samples + +```shell +# Example request using curl +curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization}/settings/idpsync/groups/mapping \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`PATCH /organizations/{organization}/settings/idpsync/groups/mapping` + +> Body parameter + +```json +{ + "add": [ + { + "gets": "string", + "given": "string" + } + ], + "remove": [ + { + "gets": "string", + "given": "string" + } + ] +} +``` + +### Parameters + +| Name | In | Type | Required | Description | +|----------------|------|------------------------------------------------------------------------------------------------|----------|-----------------------------------------------| +| `organization` | path | string(uuid) | true | Organization ID or name | +| `body` | body | [codersdk.PatchGroupIDPSyncMappingRequest](schemas.md#codersdkpatchgroupidpsyncmappingrequest) | true | Description of the mappings to add and remove | + +### Example responses + +> 200 Response + +```json +{ + "auto_create_missing_groups": true, + "field": "string", + "legacy_group_name_mapping": { + "property1": "string", + "property2": "string" + }, + "mapping": { + "property1": [ + "string" + ], + "property2": [ + "string" + ] + }, + "regex_filter": {} +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|--------------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.GroupSyncSettings](schemas.md#codersdkgroupsyncsettings) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Get role IdP Sync settings by organization ### Code samples @@ -2061,6 +2205,127 @@ curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization}/setti To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Update role IdP Sync config + +### Code samples + +```shell +# Example request using curl +curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization}/settings/idpsync/roles/config \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`PATCH /organizations/{organization}/settings/idpsync/roles/config` + +> Body parameter + +```json +{ + "field": "string" +} +``` + +### Parameters + +| Name | In | Type | Required | Description | +|----------------|------|--------------------------------------------------------------------------------------------|----------|-------------------------| +| `organization` | path | string(uuid) | true | Organization ID or name | +| `body` | body | [codersdk.PatchRoleIDPSyncConfigRequest](schemas.md#codersdkpatchroleidpsyncconfigrequest) | true | New config values | + +### Example responses + +> 200 Response + +```json +{ + "field": "string", + "mapping": { + "property1": [ + "string" + ], + "property2": [ + "string" + ] + } +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|------------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.RoleSyncSettings](schemas.md#codersdkrolesyncsettings) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + +## Update role IdP Sync mapping + +### Code samples + +```shell +# Example request using curl +curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization}/settings/idpsync/roles/mapping \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`PATCH /organizations/{organization}/settings/idpsync/roles/mapping` + +> Body parameter + +```json +{ + "add": [ + { + "gets": "string", + "given": "string" + } + ], + "remove": [ + { + "gets": "string", + "given": "string" + } + ] +} +``` + +### Parameters + +| Name | In | Type | Required | Description | +|----------------|------|----------------------------------------------------------------------------------------------|----------|-----------------------------------------------| +| `organization` | path | string(uuid) | true | Organization ID or name | +| `body` | body | [codersdk.PatchRoleIDPSyncMappingRequest](schemas.md#codersdkpatchroleidpsyncmappingrequest) | true | Description of the mappings to add and remove | + +### Example responses + +> 200 Response + +```json +{ + "field": "string", + "mapping": { + "property1": [ + "string" + ], + "property2": [ + "string" + ] + } +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|------------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.RoleSyncSettings](schemas.md#codersdkrolesyncsettings) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Fetch provisioner key details ### Code samples @@ -2677,6 +2942,128 @@ curl -X PATCH http://coder-server:8080/api/v2/settings/idpsync/organization \ To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Update organization IdP Sync config + +### Code samples + +```shell +# Example request using curl +curl -X PATCH http://coder-server:8080/api/v2/settings/idpsync/organization/config \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`PATCH /settings/idpsync/organization/config` + +> Body parameter + +```json +{ + "assign_default": true, + "field": "string" +} +``` + +### Parameters + +| Name | In | Type | Required | Description | +|--------|------|------------------------------------------------------------------------------------------------------------|----------|-------------------| +| `body` | body | [codersdk.PatchOrganizationIDPSyncConfigRequest](schemas.md#codersdkpatchorganizationidpsyncconfigrequest) | true | New config values | + +### Example responses + +> 200 Response + +```json +{ + "field": "string", + "mapping": { + "property1": [ + "string" + ], + "property2": [ + "string" + ] + }, + "organization_assign_default": true +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|----------------------------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.OrganizationSyncSettings](schemas.md#codersdkorganizationsyncsettings) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + +## Update organization IdP Sync mapping + +### Code samples + +```shell +# Example request using curl +curl -X PATCH http://coder-server:8080/api/v2/settings/idpsync/organization/mapping \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`PATCH /settings/idpsync/organization/mapping` + +> Body parameter + +```json +{ + "add": [ + { + "gets": "string", + "given": "string" + } + ], + "remove": [ + { + "gets": "string", + "given": "string" + } + ] +} +``` + +### Parameters + +| Name | In | Type | Required | Description | +|--------|------|--------------------------------------------------------------------------------------------------------------|----------|-----------------------------------------------| +| `body` | body | [codersdk.PatchOrganizationIDPSyncMappingRequest](schemas.md#codersdkpatchorganizationidpsyncmappingrequest) | true | Description of the mappings to add and remove | + +### Example responses + +> 200 Response + +```json +{ + "field": "string", + "mapping": { + "property1": [ + "string" + ], + "property2": [ + "string" + ] + }, + "organization_assign_default": true +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|----------------------------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.OrganizationSyncSettings](schemas.md#codersdkorganizationsyncsettings) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Get template ACLs ### Code samples diff --git a/docs/reference/api/general.md b/docs/reference/api/general.md index 66e85f3f6978a..2b4a1e36c22cc 100644 --- a/docs/reference/api/general.md +++ b/docs/reference/api/general.md @@ -328,6 +328,8 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ ], "client_id": "string", "client_secret": "string", + "default_provider_enable": true, + "device_flow": true, "enterprise_base_url": "string" } }, @@ -375,6 +377,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ "sign_in_text": "string", "signups_disabled_text": "string", "skip_issuer_checks": true, + "source_user_info_from_access_token": true, "user_role_field": "string", "user_role_mapping": {}, "user_roles_default": [ diff --git a/docs/reference/api/members.md b/docs/reference/api/members.md index efe76a2eda58e..d29774663bc32 100644 --- a/docs/reference/api/members.md +++ b/docs/reference/api/members.md @@ -164,54 +164,55 @@ Status Code **200** #### Enumerated Values -| Property | Value | -|-----------------|---------------------------| -| `action` | `application_connect` | -| `action` | `assign` | -| `action` | `create` | -| `action` | `delete` | -| `action` | `read` | -| `action` | `read_personal` | -| `action` | `ssh` | -| `action` | `update` | -| `action` | `update_personal` | -| `action` | `use` | -| `action` | `view_insights` | -| `action` | `start` | -| `action` | `stop` | -| `resource_type` | `*` | -| `resource_type` | `api_key` | -| `resource_type` | `assign_org_role` | -| `resource_type` | `assign_role` | -| `resource_type` | `audit_log` | -| `resource_type` | `crypto_key` | -| `resource_type` | `debug_info` | -| `resource_type` | `deployment_config` | -| `resource_type` | `deployment_stats` | -| `resource_type` | `file` | -| `resource_type` | `group` | -| `resource_type` | `group_member` | -| `resource_type` | `idpsync_settings` | -| `resource_type` | `license` | -| `resource_type` | `notification_message` | -| `resource_type` | `notification_preference` | -| `resource_type` | `notification_template` | -| `resource_type` | `oauth2_app` | -| `resource_type` | `oauth2_app_code_token` | -| `resource_type` | `oauth2_app_secret` | -| `resource_type` | `organization` | -| `resource_type` | `organization_member` | -| `resource_type` | `provisioner_daemon` | -| `resource_type` | `provisioner_jobs` | -| `resource_type` | `provisioner_keys` | -| `resource_type` | `replicas` | -| `resource_type` | `system` | -| `resource_type` | `tailnet_coordinator` | -| `resource_type` | `template` | -| `resource_type` | `user` | -| `resource_type` | `workspace` | -| `resource_type` | `workspace_dormant` | -| `resource_type` | `workspace_proxy` | +| Property | Value | +|-----------------|------------------------------------| +| `action` | `application_connect` | +| `action` | `assign` | +| `action` | `create` | +| `action` | `delete` | +| `action` | `read` | +| `action` | `read_personal` | +| `action` | `ssh` | +| `action` | `unassign` | +| `action` | `update` | +| `action` | `update_personal` | +| `action` | `use` | +| `action` | `view_insights` | +| `action` | `start` | +| `action` | `stop` | +| `resource_type` | `*` | +| `resource_type` | `api_key` | +| `resource_type` | `assign_org_role` | +| `resource_type` | `assign_role` | +| `resource_type` | `audit_log` | +| `resource_type` | `crypto_key` | +| `resource_type` | `debug_info` | +| `resource_type` | `deployment_config` | +| `resource_type` | `deployment_stats` | +| `resource_type` | `file` | +| `resource_type` | `group` | +| `resource_type` | `group_member` | +| `resource_type` | `idpsync_settings` | +| `resource_type` | `license` | +| `resource_type` | `notification_message` | +| `resource_type` | `notification_preference` | +| `resource_type` | `notification_template` | +| `resource_type` | `oauth2_app` | +| `resource_type` | `oauth2_app_code_token` | +| `resource_type` | `oauth2_app_secret` | +| `resource_type` | `organization` | +| `resource_type` | `organization_member` | +| `resource_type` | `provisioner_daemon` | +| `resource_type` | `provisioner_jobs` | +| `resource_type` | `replicas` | +| `resource_type` | `system` | +| `resource_type` | `tailnet_coordinator` | +| `resource_type` | `template` | +| `resource_type` | `user` | +| `resource_type` | `workspace` | +| `resource_type` | `workspace_agent_resource_monitor` | +| `resource_type` | `workspace_dormant` | +| `resource_type` | `workspace_proxy` | To perform this operation, you must be authenticated. [Learn more](authentication.md). @@ -326,54 +327,55 @@ Status Code **200** #### Enumerated Values -| Property | Value | -|-----------------|---------------------------| -| `action` | `application_connect` | -| `action` | `assign` | -| `action` | `create` | -| `action` | `delete` | -| `action` | `read` | -| `action` | `read_personal` | -| `action` | `ssh` | -| `action` | `update` | -| `action` | `update_personal` | -| `action` | `use` | -| `action` | `view_insights` | -| `action` | `start` | -| `action` | `stop` | -| `resource_type` | `*` | -| `resource_type` | `api_key` | -| `resource_type` | `assign_org_role` | -| `resource_type` | `assign_role` | -| `resource_type` | `audit_log` | -| `resource_type` | `crypto_key` | -| `resource_type` | `debug_info` | -| `resource_type` | `deployment_config` | -| `resource_type` | `deployment_stats` | -| `resource_type` | `file` | -| `resource_type` | `group` | -| `resource_type` | `group_member` | -| `resource_type` | `idpsync_settings` | -| `resource_type` | `license` | -| `resource_type` | `notification_message` | -| `resource_type` | `notification_preference` | -| `resource_type` | `notification_template` | -| `resource_type` | `oauth2_app` | -| `resource_type` | `oauth2_app_code_token` | -| `resource_type` | `oauth2_app_secret` | -| `resource_type` | `organization` | -| `resource_type` | `organization_member` | -| `resource_type` | `provisioner_daemon` | -| `resource_type` | `provisioner_jobs` | -| `resource_type` | `provisioner_keys` | -| `resource_type` | `replicas` | -| `resource_type` | `system` | -| `resource_type` | `tailnet_coordinator` | -| `resource_type` | `template` | -| `resource_type` | `user` | -| `resource_type` | `workspace` | -| `resource_type` | `workspace_dormant` | -| `resource_type` | `workspace_proxy` | +| Property | Value | +|-----------------|------------------------------------| +| `action` | `application_connect` | +| `action` | `assign` | +| `action` | `create` | +| `action` | `delete` | +| `action` | `read` | +| `action` | `read_personal` | +| `action` | `ssh` | +| `action` | `unassign` | +| `action` | `update` | +| `action` | `update_personal` | +| `action` | `use` | +| `action` | `view_insights` | +| `action` | `start` | +| `action` | `stop` | +| `resource_type` | `*` | +| `resource_type` | `api_key` | +| `resource_type` | `assign_org_role` | +| `resource_type` | `assign_role` | +| `resource_type` | `audit_log` | +| `resource_type` | `crypto_key` | +| `resource_type` | `debug_info` | +| `resource_type` | `deployment_config` | +| `resource_type` | `deployment_stats` | +| `resource_type` | `file` | +| `resource_type` | `group` | +| `resource_type` | `group_member` | +| `resource_type` | `idpsync_settings` | +| `resource_type` | `license` | +| `resource_type` | `notification_message` | +| `resource_type` | `notification_preference` | +| `resource_type` | `notification_template` | +| `resource_type` | `oauth2_app` | +| `resource_type` | `oauth2_app_code_token` | +| `resource_type` | `oauth2_app_secret` | +| `resource_type` | `organization` | +| `resource_type` | `organization_member` | +| `resource_type` | `provisioner_daemon` | +| `resource_type` | `provisioner_jobs` | +| `resource_type` | `replicas` | +| `resource_type` | `system` | +| `resource_type` | `tailnet_coordinator` | +| `resource_type` | `template` | +| `resource_type` | `user` | +| `resource_type` | `workspace` | +| `resource_type` | `workspace_agent_resource_monitor` | +| `resource_type` | `workspace_dormant` | +| `resource_type` | `workspace_proxy` | To perform this operation, you must be authenticated. [Learn more](authentication.md). @@ -488,54 +490,55 @@ Status Code **200** #### Enumerated Values -| Property | Value | -|-----------------|---------------------------| -| `action` | `application_connect` | -| `action` | `assign` | -| `action` | `create` | -| `action` | `delete` | -| `action` | `read` | -| `action` | `read_personal` | -| `action` | `ssh` | -| `action` | `update` | -| `action` | `update_personal` | -| `action` | `use` | -| `action` | `view_insights` | -| `action` | `start` | -| `action` | `stop` | -| `resource_type` | `*` | -| `resource_type` | `api_key` | -| `resource_type` | `assign_org_role` | -| `resource_type` | `assign_role` | -| `resource_type` | `audit_log` | -| `resource_type` | `crypto_key` | -| `resource_type` | `debug_info` | -| `resource_type` | `deployment_config` | -| `resource_type` | `deployment_stats` | -| `resource_type` | `file` | -| `resource_type` | `group` | -| `resource_type` | `group_member` | -| `resource_type` | `idpsync_settings` | -| `resource_type` | `license` | -| `resource_type` | `notification_message` | -| `resource_type` | `notification_preference` | -| `resource_type` | `notification_template` | -| `resource_type` | `oauth2_app` | -| `resource_type` | `oauth2_app_code_token` | -| `resource_type` | `oauth2_app_secret` | -| `resource_type` | `organization` | -| `resource_type` | `organization_member` | -| `resource_type` | `provisioner_daemon` | -| `resource_type` | `provisioner_jobs` | -| `resource_type` | `provisioner_keys` | -| `resource_type` | `replicas` | -| `resource_type` | `system` | -| `resource_type` | `tailnet_coordinator` | -| `resource_type` | `template` | -| `resource_type` | `user` | -| `resource_type` | `workspace` | -| `resource_type` | `workspace_dormant` | -| `resource_type` | `workspace_proxy` | +| Property | Value | +|-----------------|------------------------------------| +| `action` | `application_connect` | +| `action` | `assign` | +| `action` | `create` | +| `action` | `delete` | +| `action` | `read` | +| `action` | `read_personal` | +| `action` | `ssh` | +| `action` | `unassign` | +| `action` | `update` | +| `action` | `update_personal` | +| `action` | `use` | +| `action` | `view_insights` | +| `action` | `start` | +| `action` | `stop` | +| `resource_type` | `*` | +| `resource_type` | `api_key` | +| `resource_type` | `assign_org_role` | +| `resource_type` | `assign_role` | +| `resource_type` | `audit_log` | +| `resource_type` | `crypto_key` | +| `resource_type` | `debug_info` | +| `resource_type` | `deployment_config` | +| `resource_type` | `deployment_stats` | +| `resource_type` | `file` | +| `resource_type` | `group` | +| `resource_type` | `group_member` | +| `resource_type` | `idpsync_settings` | +| `resource_type` | `license` | +| `resource_type` | `notification_message` | +| `resource_type` | `notification_preference` | +| `resource_type` | `notification_template` | +| `resource_type` | `oauth2_app` | +| `resource_type` | `oauth2_app_code_token` | +| `resource_type` | `oauth2_app_secret` | +| `resource_type` | `organization` | +| `resource_type` | `organization_member` | +| `resource_type` | `provisioner_daemon` | +| `resource_type` | `provisioner_jobs` | +| `resource_type` | `replicas` | +| `resource_type` | `system` | +| `resource_type` | `tailnet_coordinator` | +| `resource_type` | `template` | +| `resource_type` | `user` | +| `resource_type` | `workspace` | +| `resource_type` | `workspace_agent_resource_monitor` | +| `resource_type` | `workspace_dormant` | +| `resource_type` | `workspace_proxy` | To perform this operation, you must be authenticated. [Learn more](authentication.md). @@ -619,54 +622,55 @@ Status Code **200** #### Enumerated Values -| Property | Value | -|-----------------|---------------------------| -| `action` | `application_connect` | -| `action` | `assign` | -| `action` | `create` | -| `action` | `delete` | -| `action` | `read` | -| `action` | `read_personal` | -| `action` | `ssh` | -| `action` | `update` | -| `action` | `update_personal` | -| `action` | `use` | -| `action` | `view_insights` | -| `action` | `start` | -| `action` | `stop` | -| `resource_type` | `*` | -| `resource_type` | `api_key` | -| `resource_type` | `assign_org_role` | -| `resource_type` | `assign_role` | -| `resource_type` | `audit_log` | -| `resource_type` | `crypto_key` | -| `resource_type` | `debug_info` | -| `resource_type` | `deployment_config` | -| `resource_type` | `deployment_stats` | -| `resource_type` | `file` | -| `resource_type` | `group` | -| `resource_type` | `group_member` | -| `resource_type` | `idpsync_settings` | -| `resource_type` | `license` | -| `resource_type` | `notification_message` | -| `resource_type` | `notification_preference` | -| `resource_type` | `notification_template` | -| `resource_type` | `oauth2_app` | -| `resource_type` | `oauth2_app_code_token` | -| `resource_type` | `oauth2_app_secret` | -| `resource_type` | `organization` | -| `resource_type` | `organization_member` | -| `resource_type` | `provisioner_daemon` | -| `resource_type` | `provisioner_jobs` | -| `resource_type` | `provisioner_keys` | -| `resource_type` | `replicas` | -| `resource_type` | `system` | -| `resource_type` | `tailnet_coordinator` | -| `resource_type` | `template` | -| `resource_type` | `user` | -| `resource_type` | `workspace` | -| `resource_type` | `workspace_dormant` | -| `resource_type` | `workspace_proxy` | +| Property | Value | +|-----------------|------------------------------------| +| `action` | `application_connect` | +| `action` | `assign` | +| `action` | `create` | +| `action` | `delete` | +| `action` | `read` | +| `action` | `read_personal` | +| `action` | `ssh` | +| `action` | `unassign` | +| `action` | `update` | +| `action` | `update_personal` | +| `action` | `use` | +| `action` | `view_insights` | +| `action` | `start` | +| `action` | `stop` | +| `resource_type` | `*` | +| `resource_type` | `api_key` | +| `resource_type` | `assign_org_role` | +| `resource_type` | `assign_role` | +| `resource_type` | `audit_log` | +| `resource_type` | `crypto_key` | +| `resource_type` | `debug_info` | +| `resource_type` | `deployment_config` | +| `resource_type` | `deployment_stats` | +| `resource_type` | `file` | +| `resource_type` | `group` | +| `resource_type` | `group_member` | +| `resource_type` | `idpsync_settings` | +| `resource_type` | `license` | +| `resource_type` | `notification_message` | +| `resource_type` | `notification_preference` | +| `resource_type` | `notification_template` | +| `resource_type` | `oauth2_app` | +| `resource_type` | `oauth2_app_code_token` | +| `resource_type` | `oauth2_app_secret` | +| `resource_type` | `organization` | +| `resource_type` | `organization_member` | +| `resource_type` | `provisioner_daemon` | +| `resource_type` | `provisioner_jobs` | +| `resource_type` | `replicas` | +| `resource_type` | `system` | +| `resource_type` | `tailnet_coordinator` | +| `resource_type` | `template` | +| `resource_type` | `user` | +| `resource_type` | `workspace` | +| `resource_type` | `workspace_agent_resource_monitor` | +| `resource_type` | `workspace_dormant` | +| `resource_type` | `workspace_proxy` | To perform this operation, you must be authenticated. [Learn more](authentication.md). @@ -882,53 +886,54 @@ Status Code **200** #### Enumerated Values -| Property | Value | -|-----------------|---------------------------| -| `action` | `application_connect` | -| `action` | `assign` | -| `action` | `create` | -| `action` | `delete` | -| `action` | `read` | -| `action` | `read_personal` | -| `action` | `ssh` | -| `action` | `update` | -| `action` | `update_personal` | -| `action` | `use` | -| `action` | `view_insights` | -| `action` | `start` | -| `action` | `stop` | -| `resource_type` | `*` | -| `resource_type` | `api_key` | -| `resource_type` | `assign_org_role` | -| `resource_type` | `assign_role` | -| `resource_type` | `audit_log` | -| `resource_type` | `crypto_key` | -| `resource_type` | `debug_info` | -| `resource_type` | `deployment_config` | -| `resource_type` | `deployment_stats` | -| `resource_type` | `file` | -| `resource_type` | `group` | -| `resource_type` | `group_member` | -| `resource_type` | `idpsync_settings` | -| `resource_type` | `license` | -| `resource_type` | `notification_message` | -| `resource_type` | `notification_preference` | -| `resource_type` | `notification_template` | -| `resource_type` | `oauth2_app` | -| `resource_type` | `oauth2_app_code_token` | -| `resource_type` | `oauth2_app_secret` | -| `resource_type` | `organization` | -| `resource_type` | `organization_member` | -| `resource_type` | `provisioner_daemon` | -| `resource_type` | `provisioner_jobs` | -| `resource_type` | `provisioner_keys` | -| `resource_type` | `replicas` | -| `resource_type` | `system` | -| `resource_type` | `tailnet_coordinator` | -| `resource_type` | `template` | -| `resource_type` | `user` | -| `resource_type` | `workspace` | -| `resource_type` | `workspace_dormant` | -| `resource_type` | `workspace_proxy` | +| Property | Value | +|-----------------|------------------------------------| +| `action` | `application_connect` | +| `action` | `assign` | +| `action` | `create` | +| `action` | `delete` | +| `action` | `read` | +| `action` | `read_personal` | +| `action` | `ssh` | +| `action` | `unassign` | +| `action` | `update` | +| `action` | `update_personal` | +| `action` | `use` | +| `action` | `view_insights` | +| `action` | `start` | +| `action` | `stop` | +| `resource_type` | `*` | +| `resource_type` | `api_key` | +| `resource_type` | `assign_org_role` | +| `resource_type` | `assign_role` | +| `resource_type` | `audit_log` | +| `resource_type` | `crypto_key` | +| `resource_type` | `debug_info` | +| `resource_type` | `deployment_config` | +| `resource_type` | `deployment_stats` | +| `resource_type` | `file` | +| `resource_type` | `group` | +| `resource_type` | `group_member` | +| `resource_type` | `idpsync_settings` | +| `resource_type` | `license` | +| `resource_type` | `notification_message` | +| `resource_type` | `notification_preference` | +| `resource_type` | `notification_template` | +| `resource_type` | `oauth2_app` | +| `resource_type` | `oauth2_app_code_token` | +| `resource_type` | `oauth2_app_secret` | +| `resource_type` | `organization` | +| `resource_type` | `organization_member` | +| `resource_type` | `provisioner_daemon` | +| `resource_type` | `provisioner_jobs` | +| `resource_type` | `replicas` | +| `resource_type` | `system` | +| `resource_type` | `tailnet_coordinator` | +| `resource_type` | `template` | +| `resource_type` | `user` | +| `resource_type` | `workspace` | +| `resource_type` | `workspace_agent_resource_monitor` | +| `resource_type` | `workspace_dormant` | +| `resource_type` | `workspace_proxy` | To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/docs/reference/api/notifications.md b/docs/reference/api/notifications.md index 0d9b07b3ffce2..b513786bfcb1e 100644 --- a/docs/reference/api/notifications.md +++ b/docs/reference/api/notifications.md @@ -182,6 +182,26 @@ Status Code **200** To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Send a test notification + +### Code samples + +```shell +# Example request using curl +curl -X POST http://coder-server:8080/api/v2/notifications/test \ + -H 'Coder-Session-Token: API_KEY' +``` + +`POST /notifications/test` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|--------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Get user notification preferences ### Code samples diff --git a/docs/reference/api/organizations.md b/docs/reference/api/organizations.md index 32789743afc38..8c49f33e31ce3 100644 --- a/docs/reference/api/organizations.md +++ b/docs/reference/api/organizations.md @@ -359,11 +359,13 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/provisi ### Parameters -| Name | In | Type | Required | Description | -|----------------|-------|--------------|----------|--------------------------| -| `organization` | path | string(uuid) | true | Organization ID | -| `limit` | query | integer | false | Page limit | -| `status` | query | string | false | Filter results by status | +| Name | In | Type | Required | Description | +|----------------|-------|--------------|----------|------------------------------------------------------------------------------------| +| `organization` | path | string(uuid) | true | Organization ID | +| `limit` | query | integer | false | Page limit | +| `ids` | query | array(uuid) | false | Filter results by job IDs | +| `status` | query | string | false | Filter results by status | +| `tags` | query | object | false | Provisioner tags to filter by (JSON of the form {'tag1':'value1','tag2':'value2'}) | #### Enumerated Values @@ -405,6 +407,15 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/provisi "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" }, + "metadata": { + "template_display_name": "string", + "template_icon": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "template_name": "string", + "template_version_name": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", + "workspace_name": "string" + }, "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, @@ -430,30 +441,38 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/provisi Status Code **200** -| Name | Type | Required | Restrictions | Description | -|--------------------------|--------------------------------------------------------------------------|----------|--------------|-------------| -| `[array item]` | array | false | | | -| `» available_workers` | array | false | | | -| `» canceled_at` | string(date-time) | false | | | -| `» completed_at` | string(date-time) | false | | | -| `» created_at` | string(date-time) | false | | | -| `» error` | string | false | | | -| `» error_code` | [codersdk.JobErrorCode](schemas.md#codersdkjoberrorcode) | false | | | -| `» file_id` | string(uuid) | false | | | -| `» id` | string(uuid) | false | | | -| `» input` | [codersdk.ProvisionerJobInput](schemas.md#codersdkprovisionerjobinput) | false | | | -| `»» error` | string | false | | | -| `»» template_version_id` | string(uuid) | false | | | -| `»» workspace_build_id` | string(uuid) | false | | | -| `» organization_id` | string(uuid) | false | | | -| `» queue_position` | integer | false | | | -| `» queue_size` | integer | false | | | -| `» started_at` | string(date-time) | false | | | -| `» status` | [codersdk.ProvisionerJobStatus](schemas.md#codersdkprovisionerjobstatus) | false | | | -| `» tags` | object | false | | | -| `»» [any property]` | string | false | | | -| `» type` | [codersdk.ProvisionerJobType](schemas.md#codersdkprovisionerjobtype) | false | | | -| `» worker_id` | string(uuid) | false | | | +| Name | Type | Required | Restrictions | Description | +|----------------------------|------------------------------------------------------------------------------|----------|--------------|-------------| +| `[array item]` | array | false | | | +| `» available_workers` | array | false | | | +| `» canceled_at` | string(date-time) | false | | | +| `» completed_at` | string(date-time) | false | | | +| `» created_at` | string(date-time) | false | | | +| `» error` | string | false | | | +| `» error_code` | [codersdk.JobErrorCode](schemas.md#codersdkjoberrorcode) | false | | | +| `» file_id` | string(uuid) | false | | | +| `» id` | string(uuid) | false | | | +| `» input` | [codersdk.ProvisionerJobInput](schemas.md#codersdkprovisionerjobinput) | false | | | +| `»» error` | string | false | | | +| `»» template_version_id` | string(uuid) | false | | | +| `»» workspace_build_id` | string(uuid) | false | | | +| `» metadata` | [codersdk.ProvisionerJobMetadata](schemas.md#codersdkprovisionerjobmetadata) | false | | | +| `»» template_display_name` | string | false | | | +| `»» template_icon` | string | false | | | +| `»» template_id` | string(uuid) | false | | | +| `»» template_name` | string | false | | | +| `»» template_version_name` | string | false | | | +| `»» workspace_id` | string(uuid) | false | | | +| `»» workspace_name` | string | false | | | +| `» organization_id` | string(uuid) | false | | | +| `» queue_position` | integer | false | | | +| `» queue_size` | integer | false | | | +| `» started_at` | string(date-time) | false | | | +| `» status` | [codersdk.ProvisionerJobStatus](schemas.md#codersdkprovisionerjobstatus) | false | | | +| `» tags` | object | false | | | +| `»» [any property]` | string | false | | | +| `» type` | [codersdk.ProvisionerJobType](schemas.md#codersdkprovisionerjobtype) | false | | | +| `» worker_id` | string(uuid) | false | | | #### Enumerated Values @@ -513,6 +532,15 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/provisi "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" }, + "metadata": { + "template_display_name": "string", + "template_icon": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "template_name": "string", + "template_version_name": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", + "workspace_name": "string" + }, "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, diff --git a/docs/reference/api/provisioning.md b/docs/reference/api/provisioning.md index bf3c36269fafa..1d910e4bc045e 100644 --- a/docs/reference/api/provisioning.md +++ b/docs/reference/api/provisioning.md @@ -18,8 +18,29 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/provisi | Name | In | Type | Required | Description | |----------------|-------|--------------|----------|------------------------------------------------------------------------------------| | `organization` | path | string(uuid) | true | Organization ID | +| `limit` | query | integer | false | Page limit | +| `ids` | query | array(uuid) | false | Filter results by job IDs | +| `status` | query | string | false | Filter results by status | | `tags` | query | object | false | Provisioner tags to filter by (JSON of the form {'tag1':'value1','tag2':'value2'}) | +#### Enumerated Values + +| Parameter | Value | +|-----------|-------------| +| `status` | `pending` | +| `status` | `running` | +| `status` | `succeeded` | +| `status` | `canceling` | +| `status` | `canceled` | +| `status` | `failed` | +| `status` | `unknown` | +| `status` | `pending` | +| `status` | `running` | +| `status` | `succeeded` | +| `status` | `canceling` | +| `status` | `canceled` | +| `status` | `failed` | + ### Example responses > 200 Response @@ -31,7 +52,10 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/provisi "created_at": "2019-08-24T14:15:22Z", "current_job": { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "status": "pending" + "status": "pending", + "template_display_name": "string", + "template_icon": "string", + "template_name": "string" }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "key_id": "1e779c8a-6786-4c89-b7c3-a6666f5fd6b5", @@ -41,7 +65,10 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/provisi "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "previous_job": { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "status": "pending" + "status": "pending", + "template_display_name": "string", + "template_icon": "string", + "template_name": "string" }, "provisioners": [ "string" @@ -66,26 +93,29 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/provisi Status Code **200** -| Name | Type | Required | Restrictions | Description | -|---------------------|--------------------------------------------------------------------------------|----------|--------------|------------------| -| `[array item]` | array | false | | | -| `» api_version` | string | false | | | -| `» created_at` | string(date-time) | false | | | -| `» current_job` | [codersdk.ProvisionerDaemonJob](schemas.md#codersdkprovisionerdaemonjob) | false | | | -| `»» id` | string(uuid) | false | | | -| `»» status` | [codersdk.ProvisionerJobStatus](schemas.md#codersdkprovisionerjobstatus) | false | | | -| `» id` | string(uuid) | false | | | -| `» key_id` | string(uuid) | false | | | -| `» key_name` | string | false | | Optional fields. | -| `» last_seen_at` | string(date-time) | false | | | -| `» name` | string | false | | | -| `» organization_id` | string(uuid) | false | | | -| `» previous_job` | [codersdk.ProvisionerDaemonJob](schemas.md#codersdkprovisionerdaemonjob) | false | | | -| `» provisioners` | array | false | | | -| `» status` | [codersdk.ProvisionerDaemonStatus](schemas.md#codersdkprovisionerdaemonstatus) | false | | | -| `» tags` | object | false | | | -| `»» [any property]` | string | false | | | -| `» version` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|----------------------------|--------------------------------------------------------------------------------|----------|--------------|------------------| +| `[array item]` | array | false | | | +| `» api_version` | string | false | | | +| `» created_at` | string(date-time) | false | | | +| `» current_job` | [codersdk.ProvisionerDaemonJob](schemas.md#codersdkprovisionerdaemonjob) | false | | | +| `»» id` | string(uuid) | false | | | +| `»» status` | [codersdk.ProvisionerJobStatus](schemas.md#codersdkprovisionerjobstatus) | false | | | +| `»» template_display_name` | string | false | | | +| `»» template_icon` | string | false | | | +| `»» template_name` | string | false | | | +| `» id` | string(uuid) | false | | | +| `» key_id` | string(uuid) | false | | | +| `» key_name` | string | false | | Optional fields. | +| `» last_seen_at` | string(date-time) | false | | | +| `» name` | string | false | | | +| `» organization_id` | string(uuid) | false | | | +| `» previous_job` | [codersdk.ProvisionerDaemonJob](schemas.md#codersdkprovisionerdaemonjob) | false | | | +| `» provisioners` | array | false | | | +| `» status` | [codersdk.ProvisionerDaemonStatus](schemas.md#codersdkprovisionerdaemonstatus) | false | | | +| `» tags` | object | false | | | +| `»» [any property]` | string | false | | | +| `» version` | string | false | | | #### Enumerated Values diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index db6fc2a51f58e..b3e4821c2e39e 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -554,6 +554,10 @@ | `logout` | | `register` | | `request_password_reset` | +| `connect` | +| `disconnect` | +| `open` | +| `close` | ## codersdk.AuditDiff @@ -783,6 +787,7 @@ ```json { "github": { + "default_provider_configured": true, "enabled": true }, "oidc": { @@ -799,12 +804,12 @@ ### Properties -| Name | Type | Required | Restrictions | Description | -|------------------------|----------------------------------------------------|----------|--------------|-------------| -| `github` | [codersdk.AuthMethod](#codersdkauthmethod) | false | | | -| `oidc` | [codersdk.OIDCAuthMethod](#codersdkoidcauthmethod) | false | | | -| `password` | [codersdk.AuthMethod](#codersdkauthmethod) | false | | | -| `terms_of_service_url` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|------------------------|--------------------------------------------------------|----------|--------------|-------------| +| `github` | [codersdk.GithubAuthMethod](#codersdkgithubauthmethod) | false | | | +| `oidc` | [codersdk.OIDCAuthMethod](#codersdkoidcauthmethod) | false | | | +| `password` | [codersdk.AuthMethod](#codersdkauthmethod) | false | | | +| `terms_of_service_url` | string | false | | | ## codersdk.AuthorizationCheck @@ -1314,6 +1319,7 @@ This is required on creation to enable a user-flow of validating a template work ], "build_reason": "autostart", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", + "request_id": "266ea41d-adf5-480b-af50-15b940c2b846", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", "resource_type": "template", "time": "2019-08-24T14:15:22Z" @@ -1328,6 +1334,7 @@ This is required on creation to enable a user-flow of validating a template work | `additional_fields` | array of integer | false | | | | `build_reason` | [codersdk.BuildReason](#codersdkbuildreason) | false | | | | `organization_id` | string | false | | | +| `request_id` | string | false | | | | `resource_id` | string | false | | | | `resource_type` | [codersdk.ResourceType](#codersdkresourcetype) | false | | | | `time` | string | false | | | @@ -1971,6 +1978,8 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o ], "client_id": "string", "client_secret": "string", + "default_provider_enable": true, + "device_flow": true, "enterprise_base_url": "string" } }, @@ -2018,6 +2027,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "sign_in_text": "string", "signups_disabled_text": "string", "skip_issuer_checks": true, + "source_user_info_from_access_token": true, "user_role_field": "string", "user_role_mapping": {}, "user_roles_default": [ @@ -2441,6 +2451,8 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o ], "client_id": "string", "client_secret": "string", + "default_provider_enable": true, + "device_flow": true, "enterprise_base_url": "string" } }, @@ -2488,6 +2500,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "sign_in_text": "string", "signups_disabled_text": "string", "skip_issuer_checks": true, + "source_user_info_from_access_token": true, "user_role_field": "string", "user_role_mapping": {}, "user_roles_default": [ @@ -3084,12 +3097,28 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith ### Properties -| Name | Type | Required | Restrictions | Description | -|--------------|--------|----------|--------------|-------------| -| `created_at` | string | false | | | -| `public_key` | string | false | | | -| `updated_at` | string | false | | | -| `user_id` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|--------------|--------|----------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `created_at` | string | false | | | +| `public_key` | string | false | | Public key is the SSH public key in OpenSSH format. Example: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID3OmYJvT7q1cF1azbybYy0OZ9yrXfA+M6Lr4vzX5zlp\n" Note: The key includes a trailing newline (\n). | +| `updated_at` | string | false | | | +| `user_id` | string | false | | | + +## codersdk.GithubAuthMethod + +```json +{ + "default_provider_configured": true, + "enabled": true +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|-------------------------------|---------|----------|--------------|-------------| +| `default_provider_configured` | boolean | false | | | +| `enabled` | boolean | false | | | ## codersdk.Group @@ -3797,6 +3826,8 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith ], "client_id": "string", "client_secret": "string", + "default_provider_enable": true, + "device_flow": true, "enterprise_base_url": "string" } } @@ -3822,21 +3853,25 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith ], "client_id": "string", "client_secret": "string", + "default_provider_enable": true, + "device_flow": true, "enterprise_base_url": "string" } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -|-----------------------|-----------------|----------|--------------|-------------| -| `allow_everyone` | boolean | false | | | -| `allow_signups` | boolean | false | | | -| `allowed_orgs` | array of string | false | | | -| `allowed_teams` | array of string | false | | | -| `client_id` | string | false | | | -| `client_secret` | string | false | | | -| `enterprise_base_url` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|---------------------------|-----------------|----------|--------------|-------------| +| `allow_everyone` | boolean | false | | | +| `allow_signups` | boolean | false | | | +| `allowed_orgs` | array of string | false | | | +| `allowed_teams` | array of string | false | | | +| `client_id` | string | false | | | +| `client_secret` | string | false | | | +| `default_provider_enable` | boolean | false | | | +| `device_flow` | boolean | false | | | +| `enterprise_base_url` | string | false | | | ## codersdk.OAuth2ProviderApp @@ -3983,6 +4018,7 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith "sign_in_text": "string", "signups_disabled_text": "string", "skip_issuer_checks": true, + "source_user_info_from_access_token": true, "user_role_field": "string", "user_role_mapping": {}, "user_roles_default": [ @@ -3994,37 +4030,38 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith ### Properties -| Name | Type | Required | Restrictions | Description | -|-------------------------------|----------------------------------|----------|--------------|----------------------------------------------------------------------------------| -| `allow_signups` | boolean | false | | | -| `auth_url_params` | object | false | | | -| `client_cert_file` | string | false | | | -| `client_id` | string | false | | | -| `client_key_file` | string | false | | Client key file & ClientCertFile are used in place of ClientSecret for PKI auth. | -| `client_secret` | string | false | | | -| `email_domain` | array of string | false | | | -| `email_field` | string | false | | | -| `group_allow_list` | array of string | false | | | -| `group_auto_create` | boolean | false | | | -| `group_mapping` | object | false | | | -| `group_regex_filter` | [serpent.Regexp](#serpentregexp) | false | | | -| `groups_field` | string | false | | | -| `icon_url` | [serpent.URL](#serpenturl) | false | | | -| `ignore_email_verified` | boolean | false | | | -| `ignore_user_info` | boolean | false | | | -| `issuer_url` | string | false | | | -| `name_field` | string | false | | | -| `organization_assign_default` | boolean | false | | | -| `organization_field` | string | false | | | -| `organization_mapping` | object | false | | | -| `scopes` | array of string | false | | | -| `sign_in_text` | string | false | | | -| `signups_disabled_text` | string | false | | | -| `skip_issuer_checks` | boolean | false | | | -| `user_role_field` | string | false | | | -| `user_role_mapping` | object | false | | | -| `user_roles_default` | array of string | false | | | -| `username_field` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|--------------------------------------|----------------------------------|----------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `allow_signups` | boolean | false | | | +| `auth_url_params` | object | false | | | +| `client_cert_file` | string | false | | | +| `client_id` | string | false | | | +| `client_key_file` | string | false | | Client key file & ClientCertFile are used in place of ClientSecret for PKI auth. | +| `client_secret` | string | false | | | +| `email_domain` | array of string | false | | | +| `email_field` | string | false | | | +| `group_allow_list` | array of string | false | | | +| `group_auto_create` | boolean | false | | | +| `group_mapping` | object | false | | | +| `group_regex_filter` | [serpent.Regexp](#serpentregexp) | false | | | +| `groups_field` | string | false | | | +| `icon_url` | [serpent.URL](#serpenturl) | false | | | +| `ignore_email_verified` | boolean | false | | | +| `ignore_user_info` | boolean | false | | Ignore user info & UserInfoFromAccessToken are mutually exclusive. Only 1 can be set to true. Ideally this would be an enum with 3 states, ['none', 'userinfo', 'access_token']. However, for backward compatibility, `ignore_user_info` must remain. And `access_token` is a niche, non-spec compliant edge case. So it's use is rare, and should not be advised. | +| `issuer_url` | string | false | | | +| `name_field` | string | false | | | +| `organization_assign_default` | boolean | false | | | +| `organization_field` | string | false | | | +| `organization_mapping` | object | false | | | +| `scopes` | array of string | false | | | +| `sign_in_text` | string | false | | | +| `signups_disabled_text` | string | false | | | +| `skip_issuer_checks` | boolean | false | | | +| `source_user_info_from_access_token` | boolean | false | | Source user info from access token as mentioned above is an edge case. This allows sourcing the user_info from the access token itself instead of a user_info endpoint. This assumes the access token is a valid JWT with a set of claims to be merged with the id_token. | +| `user_role_field` | string | false | | | +| `user_role_mapping` | object | false | | | +| `user_roles_default` | array of string | false | | | +| `username_field` | string | false | | | ## codersdk.Organization @@ -4152,6 +4189,54 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith | » `[any property]` | array of string | false | | | | `organization_assign_default` | boolean | false | | Organization assign default will ensure the default org is always included for every user, regardless of their claims. This preserves legacy behavior. | +## codersdk.PatchGroupIDPSyncConfigRequest + +```json +{ + "auto_create_missing_groups": true, + "field": "string", + "regex_filter": {} +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|------------------------------|--------------------------------|----------|--------------|-------------| +| `auto_create_missing_groups` | boolean | false | | | +| `field` | string | false | | | +| `regex_filter` | [regexp.Regexp](#regexpregexp) | false | | | + +## codersdk.PatchGroupIDPSyncMappingRequest + +```json +{ + "add": [ + { + "gets": "string", + "given": "string" + } + ], + "remove": [ + { + "gets": "string", + "given": "string" + } + ] +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|-----------|-----------------|----------|--------------|----------------------------------------------------------| +| `add` | array of object | false | | | +| `» gets` | string | false | | The ID of the Coder resource the user should be added to | +| `» given` | string | false | | The IdP claim the user has | +| `remove` | array of object | false | | | +| `» gets` | string | false | | The ID of the Coder resource the user should be added to | +| `» given` | string | false | | The IdP claim the user has | + ## codersdk.PatchGroupRequest ```json @@ -4180,6 +4265,96 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith | `quota_allowance` | integer | false | | | | `remove_users` | array of string | false | | | +## codersdk.PatchOrganizationIDPSyncConfigRequest + +```json +{ + "assign_default": true, + "field": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|------------------|---------|----------|--------------|-------------| +| `assign_default` | boolean | false | | | +| `field` | string | false | | | + +## codersdk.PatchOrganizationIDPSyncMappingRequest + +```json +{ + "add": [ + { + "gets": "string", + "given": "string" + } + ], + "remove": [ + { + "gets": "string", + "given": "string" + } + ] +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|-----------|-----------------|----------|--------------|----------------------------------------------------------| +| `add` | array of object | false | | | +| `» gets` | string | false | | The ID of the Coder resource the user should be added to | +| `» given` | string | false | | The IdP claim the user has | +| `remove` | array of object | false | | | +| `» gets` | string | false | | The ID of the Coder resource the user should be added to | +| `» given` | string | false | | The IdP claim the user has | + +## codersdk.PatchRoleIDPSyncConfigRequest + +```json +{ + "field": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|---------|--------|----------|--------------|-------------| +| `field` | string | false | | | + +## codersdk.PatchRoleIDPSyncMappingRequest + +```json +{ + "add": [ + { + "gets": "string", + "given": "string" + } + ], + "remove": [ + { + "gets": "string", + "given": "string" + } + ] +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|-----------|-----------------|----------|--------------|----------------------------------------------------------| +| `add` | array of object | false | | | +| `» gets` | string | false | | The ID of the Coder resource the user should be added to | +| `» given` | string | false | | The IdP claim the user has | +| `remove` | array of object | false | | | +| `» gets` | string | false | | The ID of the Coder resource the user should be added to | +| `» given` | string | false | | The IdP claim the user has | + ## codersdk.PatchTemplateVersionRequest ```json @@ -4289,6 +4464,45 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith | `address` | [serpent.HostPort](#serpenthostport) | false | | | | `enable` | boolean | false | | | +## codersdk.Preset + +```json +{ + "id": "string", + "name": "string", + "parameters": [ + { + "name": "string", + "value": "string" + } + ] +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|--------------|---------------------------------------------------------------|----------|--------------|-------------| +| `id` | string | false | | | +| `name` | string | false | | | +| `parameters` | array of [codersdk.PresetParameter](#codersdkpresetparameter) | false | | | + +## codersdk.PresetParameter + +```json +{ + "name": "string", + "value": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|---------|--------|----------|--------------|-------------| +| `name` | string | false | | | +| `value` | string | false | | | + ## codersdk.PrometheusConfig ```json @@ -4350,7 +4564,10 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith "created_at": "2019-08-24T14:15:22Z", "current_job": { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "status": "pending" + "status": "pending", + "template_display_name": "string", + "template_icon": "string", + "template_name": "string" }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "key_id": "1e779c8a-6786-4c89-b7c3-a6666f5fd6b5", @@ -4360,7 +4577,10 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "previous_job": { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "status": "pending" + "status": "pending", + "template_display_name": "string", + "template_icon": "string", + "template_name": "string" }, "provisioners": [ "string" @@ -4407,16 +4627,22 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith ```json { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "status": "pending" + "status": "pending", + "template_display_name": "string", + "template_icon": "string", + "template_name": "string" } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -|----------|----------------------------------------------------------------|----------|--------------|-------------| -| `id` | string | false | | | -| `status` | [codersdk.ProvisionerJobStatus](#codersdkprovisionerjobstatus) | false | | | +| Name | Type | Required | Restrictions | Description | +|-------------------------|----------------------------------------------------------------|----------|--------------|-------------| +| `id` | string | false | | | +| `status` | [codersdk.ProvisionerJobStatus](#codersdkprovisionerjobstatus) | false | | | +| `template_display_name` | string | false | | | +| `template_icon` | string | false | | | +| `template_name` | string | false | | | #### Enumerated Values @@ -4464,6 +4690,15 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" }, + "metadata": { + "template_display_name": "string", + "template_icon": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "template_name": "string", + "template_version_name": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", + "workspace_name": "string" + }, "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, @@ -4480,26 +4715,27 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith ### Properties -| Name | Type | Required | Restrictions | Description | -|---------------------|----------------------------------------------------------------|----------|--------------|-------------| -| `available_workers` | array of string | false | | | -| `canceled_at` | string | false | | | -| `completed_at` | string | false | | | -| `created_at` | string | false | | | -| `error` | string | false | | | -| `error_code` | [codersdk.JobErrorCode](#codersdkjoberrorcode) | false | | | -| `file_id` | string | false | | | -| `id` | string | false | | | -| `input` | [codersdk.ProvisionerJobInput](#codersdkprovisionerjobinput) | false | | | -| `organization_id` | string | false | | | -| `queue_position` | integer | false | | | -| `queue_size` | integer | false | | | -| `started_at` | string | false | | | -| `status` | [codersdk.ProvisionerJobStatus](#codersdkprovisionerjobstatus) | false | | | -| `tags` | object | false | | | -| » `[any property]` | string | false | | | -| `type` | [codersdk.ProvisionerJobType](#codersdkprovisionerjobtype) | false | | | -| `worker_id` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|---------------------|--------------------------------------------------------------------|----------|--------------|-------------| +| `available_workers` | array of string | false | | | +| `canceled_at` | string | false | | | +| `completed_at` | string | false | | | +| `created_at` | string | false | | | +| `error` | string | false | | | +| `error_code` | [codersdk.JobErrorCode](#codersdkjoberrorcode) | false | | | +| `file_id` | string | false | | | +| `id` | string | false | | | +| `input` | [codersdk.ProvisionerJobInput](#codersdkprovisionerjobinput) | false | | | +| `metadata` | [codersdk.ProvisionerJobMetadata](#codersdkprovisionerjobmetadata) | false | | | +| `organization_id` | string | false | | | +| `queue_position` | integer | false | | | +| `queue_size` | integer | false | | | +| `started_at` | string | false | | | +| `status` | [codersdk.ProvisionerJobStatus](#codersdkprovisionerjobstatus) | false | | | +| `tags` | object | false | | | +| » `[any property]` | string | false | | | +| `type` | [codersdk.ProvisionerJobType](#codersdkprovisionerjobtype) | false | | | +| `worker_id` | string | false | | | #### Enumerated Values @@ -4565,6 +4801,32 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith | `log_level` | `warn` | | `log_level` | `error` | +## codersdk.ProvisionerJobMetadata + +```json +{ + "template_display_name": "string", + "template_icon": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "template_name": "string", + "template_version_name": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", + "workspace_name": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|-------------------------|--------|----------|--------------|-------------| +| `template_display_name` | string | false | | | +| `template_icon` | string | false | | | +| `template_id` | string | false | | | +| `template_name` | string | false | | | +| `template_version_name` | string | false | | | +| `workspace_id` | string | false | | | +| `workspace_name` | string | false | | | + ## codersdk.ProvisionerJobStatus ```json @@ -4636,7 +4898,10 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith "created_at": "2019-08-24T14:15:22Z", "current_job": { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "status": "pending" + "status": "pending", + "template_display_name": "string", + "template_icon": "string", + "template_name": "string" }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "key_id": "1e779c8a-6786-4c89-b7c3-a6666f5fd6b5", @@ -4646,7 +4911,10 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "previous_job": { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "status": "pending" + "status": "pending", + "template_display_name": "string", + "template_icon": "string", + "template_name": "string" }, "provisioners": [ "string" @@ -4836,6 +5104,7 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith | `read` | | `read_personal` | | `ssh` | +| `unassign` | | `update` | | `update_personal` | | `use` | @@ -4853,41 +5122,41 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith #### Enumerated Values -| Value | -|---------------------------| -| `*` | -| `api_key` | -| `assign_org_role` | -| `assign_role` | -| `audit_log` | -| `crypto_key` | -| `debug_info` | -| `deployment_config` | -| `deployment_stats` | -| `file` | -| `group` | -| `group_member` | -| `idpsync_settings` | -| `license` | -| `notification_message` | -| `notification_preference` | -| `notification_template` | -| `oauth2_app` | -| `oauth2_app_code_token` | -| `oauth2_app_secret` | -| `organization` | -| `organization_member` | -| `provisioner_daemon` | -| `provisioner_jobs` | -| `provisioner_keys` | -| `replicas` | -| `system` | -| `tailnet_coordinator` | -| `template` | -| `user` | -| `workspace` | -| `workspace_dormant` | -| `workspace_proxy` | +| Value | +|------------------------------------| +| `*` | +| `api_key` | +| `assign_org_role` | +| `assign_role` | +| `audit_log` | +| `crypto_key` | +| `debug_info` | +| `deployment_config` | +| `deployment_stats` | +| `file` | +| `group` | +| `group_member` | +| `idpsync_settings` | +| `license` | +| `notification_message` | +| `notification_preference` | +| `notification_template` | +| `oauth2_app` | +| `oauth2_app_code_token` | +| `oauth2_app_secret` | +| `organization` | +| `organization_member` | +| `provisioner_daemon` | +| `provisioner_jobs` | +| `replicas` | +| `system` | +| `tailnet_coordinator` | +| `template` | +| `user` | +| `workspace` | +| `workspace_agent_resource_monitor` | +| `workspace_dormant` | +| `workspace_proxy` | ## codersdk.RateLimitConfig @@ -5126,6 +5395,8 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith | `idp_sync_settings_organization` | | `idp_sync_settings_group` | | `idp_sync_settings_role` | +| `workspace_agent` | +| `workspace_app` | ## codersdk.Response @@ -5962,6 +6233,15 @@ Restarts will only happen on weekdays in this list on weeks which line up with W "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" }, + "metadata": { + "template_display_name": "string", + "template_icon": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "template_name": "string", + "template_version_name": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", + "workspace_name": "string" + }, "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, @@ -7027,6 +7307,15 @@ If the schedule is empty, the user will be updated to use the default schedule.| "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" }, + "metadata": { + "template_display_name": "string", + "template_icon": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "template_name": "string", + "template_version_name": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", + "workspace_name": "string" + }, "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, @@ -7373,6 +7662,50 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `updated_at` | string | false | | | | `version` | string | false | | | +## codersdk.WorkspaceAgentDevcontainer + +```json +{ + "created_at": "2019-08-24T14:15:22Z", + "id": "string", + "image": "string", + "labels": { + "property1": "string", + "property2": "string" + }, + "name": "string", + "ports": [ + { + "network": "string", + "port": 0, + "process_name": "string" + } + ], + "running": true, + "status": "string", + "volumes": { + "property1": "string", + "property2": "string" + } +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|--------------------|---------------------------------------------------------------------------------------|----------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------| +| `created_at` | string | false | | Created at is the time the container was created. | +| `id` | string | false | | ID is the unique identifier of the container. | +| `image` | string | false | | Image is the name of the container image. | +| `labels` | object | false | | Labels is a map of key-value pairs of container labels. | +| » `[any property]` | string | false | | | +| `name` | string | false | | Name is the human-readable name of the container. | +| `ports` | array of [codersdk.WorkspaceAgentListeningPort](#codersdkworkspaceagentlisteningport) | false | | Ports includes ports exposed by the container. | +| `running` | boolean | false | | Running is true if the container is currently running. | +| `status` | string | false | | Status is the current status of the container. This is somewhat implementation-dependent, but should generally be a human-readable string. | +| `volumes` | object | false | | Volumes is a map of "things" mounted into the container. Again, this is somewhat implementation-dependent. | +| » `[any property]` | string | false | | | + ## codersdk.WorkspaceAgentHealth ```json @@ -7411,6 +7744,48 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `shutdown_error` | | `off` | +## codersdk.WorkspaceAgentListContainersResponse + +```json +{ + "containers": [ + { + "created_at": "2019-08-24T14:15:22Z", + "id": "string", + "image": "string", + "labels": { + "property1": "string", + "property2": "string" + }, + "name": "string", + "ports": [ + { + "network": "string", + "port": 0, + "process_name": "string" + } + ], + "running": true, + "status": "string", + "volumes": { + "property1": "string", + "property2": "string" + } + } + ], + "warnings": [ + "string" + ] +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|--------------|-------------------------------------------------------------------------------------|----------|--------------|---------------------------------------------------------------------------------------------------------------------------------------| +| `containers` | array of [codersdk.WorkspaceAgentDevcontainer](#codersdkworkspaceagentdevcontainer) | false | | Containers is a list of containers visible to the workspace agent. | +| `warnings` | array of string | false | | Warnings is a list of warnings that may have occurred during the process of listing containers. This should not include fatal errors. | + ## codersdk.WorkspaceAgentListeningPort ```json @@ -7769,6 +8144,15 @@ If the schedule is empty, the user will be updated to use the default schedule.| "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" }, + "metadata": { + "template_display_name": "string", + "template_icon": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "template_name": "string", + "template_version_name": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", + "workspace_name": "string" + }, "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, @@ -8433,6 +8817,15 @@ If the schedule is empty, the user will be updated to use the default schedule.| "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" }, + "metadata": { + "template_display_name": "string", + "template_icon": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "template_name": "string", + "template_version_name": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", + "workspace_name": "string" + }, "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, @@ -9531,7 +9924,10 @@ Zero means unspecified. There might be a limit, but the client need not try to r "created_at": "2019-08-24T14:15:22Z", "current_job": { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "status": "pending" + "status": "pending", + "template_display_name": "string", + "template_icon": "string", + "template_name": "string" }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "key_id": "1e779c8a-6786-4c89-b7c3-a6666f5fd6b5", @@ -9541,7 +9937,10 @@ Zero means unspecified. There might be a limit, but the client need not try to r "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "previous_job": { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "status": "pending" + "status": "pending", + "template_display_name": "string", + "template_icon": "string", + "template_name": "string" }, "provisioners": [ "string" @@ -9667,7 +10066,10 @@ Zero means unspecified. There might be a limit, but the client need not try to r "created_at": "2019-08-24T14:15:22Z", "current_job": { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "status": "pending" + "status": "pending", + "template_display_name": "string", + "template_icon": "string", + "template_name": "string" }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "key_id": "1e779c8a-6786-4c89-b7c3-a6666f5fd6b5", @@ -9677,7 +10079,10 @@ Zero means unspecified. There might be a limit, but the client need not try to r "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "previous_job": { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "status": "pending" + "status": "pending", + "template_display_name": "string", + "template_icon": "string", + "template_name": "string" }, "provisioners": [ "string" @@ -9734,7 +10139,10 @@ Zero means unspecified. There might be a limit, but the client need not try to r "created_at": "2019-08-24T14:15:22Z", "current_job": { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "status": "pending" + "status": "pending", + "template_display_name": "string", + "template_icon": "string", + "template_name": "string" }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "key_id": "1e779c8a-6786-4c89-b7c3-a6666f5fd6b5", @@ -9744,7 +10152,10 @@ Zero means unspecified. There might be a limit, but the client need not try to r "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "previous_job": { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "status": "pending" + "status": "pending", + "template_display_name": "string", + "template_icon": "string", + "template_name": "string" }, "provisioners": [ "string" diff --git a/docs/reference/api/templates.md b/docs/reference/api/templates.md index 6378c5f233fb8..ab8b4f1b7c131 100644 --- a/docs/reference/api/templates.md +++ b/docs/reference/api/templates.md @@ -462,6 +462,15 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" }, + "metadata": { + "template_display_name": "string", + "template_icon": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "template_name": "string", + "template_version_name": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", + "workspace_name": "string" + }, "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, @@ -550,6 +559,15 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" }, + "metadata": { + "template_display_name": "string", + "template_icon": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "template_name": "string", + "template_version_name": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", + "workspace_name": "string" + }, "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, @@ -662,6 +680,15 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" }, + "metadata": { + "template_display_name": "string", + "template_icon": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "template_name": "string", + "template_version_name": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", + "workspace_name": "string" + }, "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, @@ -1202,6 +1229,15 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/versions \ "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" }, + "metadata": { + "template_display_name": "string", + "template_icon": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "template_name": "string", + "template_version_name": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", + "workspace_name": "string" + }, "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, @@ -1242,49 +1278,57 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/versions \ Status Code **200** -| Name | Type | Required | Restrictions | Description | -|---------------------------|--------------------------------------------------------------------------|----------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `[array item]` | array | false | | | -| `» archived` | boolean | false | | | -| `» created_at` | string(date-time) | false | | | -| `» created_by` | [codersdk.MinimalUser](schemas.md#codersdkminimaluser) | false | | | -| `»» avatar_url` | string(uri) | false | | | -| `»» id` | string(uuid) | true | | | -| `»» username` | string | true | | | -| `» id` | string(uuid) | false | | | -| `» job` | [codersdk.ProvisionerJob](schemas.md#codersdkprovisionerjob) | false | | | -| `»» available_workers` | array | false | | | -| `»» canceled_at` | string(date-time) | false | | | -| `»» completed_at` | string(date-time) | false | | | -| `»» created_at` | string(date-time) | false | | | -| `»» error` | string | false | | | -| `»» error_code` | [codersdk.JobErrorCode](schemas.md#codersdkjoberrorcode) | false | | | -| `»» file_id` | string(uuid) | false | | | -| `»» id` | string(uuid) | false | | | -| `»» input` | [codersdk.ProvisionerJobInput](schemas.md#codersdkprovisionerjobinput) | false | | | -| `»»» error` | string | false | | | -| `»»» template_version_id` | string(uuid) | false | | | -| `»»» workspace_build_id` | string(uuid) | false | | | -| `»» organization_id` | string(uuid) | false | | | -| `»» queue_position` | integer | false | | | -| `»» queue_size` | integer | false | | | -| `»» started_at` | string(date-time) | false | | | -| `»» status` | [codersdk.ProvisionerJobStatus](schemas.md#codersdkprovisionerjobstatus) | false | | | -| `»» tags` | object | false | | | -| `»»» [any property]` | string | false | | | -| `»» type` | [codersdk.ProvisionerJobType](schemas.md#codersdkprovisionerjobtype) | false | | | -| `»» worker_id` | string(uuid) | false | | | -| `» matched_provisioners` | [codersdk.MatchedProvisioners](schemas.md#codersdkmatchedprovisioners) | false | | | -| `»» available` | integer | false | | Available is the number of provisioner daemons that are available to take jobs. This may be less than the count if some provisioners are busy or have been stopped. | -| `»» count` | integer | false | | Count is the number of provisioner daemons that matched the given tags. If the count is 0, it means no provisioner daemons matched the requested tags. | -| `»» most_recently_seen` | string(date-time) | false | | Most recently seen is the most recently seen time of the set of matched provisioners. If no provisioners matched, this field will be null. | -| `» message` | string | false | | | -| `» name` | string | false | | | -| `» organization_id` | string(uuid) | false | | | -| `» readme` | string | false | | | -| `» template_id` | string(uuid) | false | | | -| `» updated_at` | string(date-time) | false | | | -| `» warnings` | array | false | | | +| Name | Type | Required | Restrictions | Description | +|-----------------------------|------------------------------------------------------------------------------|----------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `[array item]` | array | false | | | +| `» archived` | boolean | false | | | +| `» created_at` | string(date-time) | false | | | +| `» created_by` | [codersdk.MinimalUser](schemas.md#codersdkminimaluser) | false | | | +| `»» avatar_url` | string(uri) | false | | | +| `»» id` | string(uuid) | true | | | +| `»» username` | string | true | | | +| `» id` | string(uuid) | false | | | +| `» job` | [codersdk.ProvisionerJob](schemas.md#codersdkprovisionerjob) | false | | | +| `»» available_workers` | array | false | | | +| `»» canceled_at` | string(date-time) | false | | | +| `»» completed_at` | string(date-time) | false | | | +| `»» created_at` | string(date-time) | false | | | +| `»» error` | string | false | | | +| `»» error_code` | [codersdk.JobErrorCode](schemas.md#codersdkjoberrorcode) | false | | | +| `»» file_id` | string(uuid) | false | | | +| `»» id` | string(uuid) | false | | | +| `»» input` | [codersdk.ProvisionerJobInput](schemas.md#codersdkprovisionerjobinput) | false | | | +| `»»» error` | string | false | | | +| `»»» template_version_id` | string(uuid) | false | | | +| `»»» workspace_build_id` | string(uuid) | false | | | +| `»» metadata` | [codersdk.ProvisionerJobMetadata](schemas.md#codersdkprovisionerjobmetadata) | false | | | +| `»»» template_display_name` | string | false | | | +| `»»» template_icon` | string | false | | | +| `»»» template_id` | string(uuid) | false | | | +| `»»» template_name` | string | false | | | +| `»»» template_version_name` | string | false | | | +| `»»» workspace_id` | string(uuid) | false | | | +| `»»» workspace_name` | string | false | | | +| `»» organization_id` | string(uuid) | false | | | +| `»» queue_position` | integer | false | | | +| `»» queue_size` | integer | false | | | +| `»» started_at` | string(date-time) | false | | | +| `»» status` | [codersdk.ProvisionerJobStatus](schemas.md#codersdkprovisionerjobstatus) | false | | | +| `»» tags` | object | false | | | +| `»»» [any property]` | string | false | | | +| `»» type` | [codersdk.ProvisionerJobType](schemas.md#codersdkprovisionerjobtype) | false | | | +| `»» worker_id` | string(uuid) | false | | | +| `» matched_provisioners` | [codersdk.MatchedProvisioners](schemas.md#codersdkmatchedprovisioners) | false | | | +| `»» available` | integer | false | | Available is the number of provisioner daemons that are available to take jobs. This may be less than the count if some provisioners are busy or have been stopped. | +| `»» count` | integer | false | | Count is the number of provisioner daemons that matched the given tags. If the count is 0, it means no provisioner daemons matched the requested tags. | +| `»» most_recently_seen` | string(date-time) | false | | Most recently seen is the most recently seen time of the set of matched provisioners. If no provisioners matched, this field will be null. | +| `» message` | string | false | | | +| `» name` | string | false | | | +| `» organization_id` | string(uuid) | false | | | +| `» readme` | string | false | | | +| `» template_id` | string(uuid) | false | | | +| `» updated_at` | string(date-time) | false | | | +| `» warnings` | array | false | | | #### Enumerated Values @@ -1462,6 +1506,15 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/versions/{templ "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" }, + "metadata": { + "template_display_name": "string", + "template_icon": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "template_name": "string", + "template_version_name": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", + "workspace_name": "string" + }, "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, @@ -1502,49 +1555,57 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/versions/{templ Status Code **200** -| Name | Type | Required | Restrictions | Description | -|---------------------------|--------------------------------------------------------------------------|----------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `[array item]` | array | false | | | -| `» archived` | boolean | false | | | -| `» created_at` | string(date-time) | false | | | -| `» created_by` | [codersdk.MinimalUser](schemas.md#codersdkminimaluser) | false | | | -| `»» avatar_url` | string(uri) | false | | | -| `»» id` | string(uuid) | true | | | -| `»» username` | string | true | | | -| `» id` | string(uuid) | false | | | -| `» job` | [codersdk.ProvisionerJob](schemas.md#codersdkprovisionerjob) | false | | | -| `»» available_workers` | array | false | | | -| `»» canceled_at` | string(date-time) | false | | | -| `»» completed_at` | string(date-time) | false | | | -| `»» created_at` | string(date-time) | false | | | -| `»» error` | string | false | | | -| `»» error_code` | [codersdk.JobErrorCode](schemas.md#codersdkjoberrorcode) | false | | | -| `»» file_id` | string(uuid) | false | | | -| `»» id` | string(uuid) | false | | | -| `»» input` | [codersdk.ProvisionerJobInput](schemas.md#codersdkprovisionerjobinput) | false | | | -| `»»» error` | string | false | | | -| `»»» template_version_id` | string(uuid) | false | | | -| `»»» workspace_build_id` | string(uuid) | false | | | -| `»» organization_id` | string(uuid) | false | | | -| `»» queue_position` | integer | false | | | -| `»» queue_size` | integer | false | | | -| `»» started_at` | string(date-time) | false | | | -| `»» status` | [codersdk.ProvisionerJobStatus](schemas.md#codersdkprovisionerjobstatus) | false | | | -| `»» tags` | object | false | | | -| `»»» [any property]` | string | false | | | -| `»» type` | [codersdk.ProvisionerJobType](schemas.md#codersdkprovisionerjobtype) | false | | | -| `»» worker_id` | string(uuid) | false | | | -| `» matched_provisioners` | [codersdk.MatchedProvisioners](schemas.md#codersdkmatchedprovisioners) | false | | | -| `»» available` | integer | false | | Available is the number of provisioner daemons that are available to take jobs. This may be less than the count if some provisioners are busy or have been stopped. | -| `»» count` | integer | false | | Count is the number of provisioner daemons that matched the given tags. If the count is 0, it means no provisioner daemons matched the requested tags. | -| `»» most_recently_seen` | string(date-time) | false | | Most recently seen is the most recently seen time of the set of matched provisioners. If no provisioners matched, this field will be null. | -| `» message` | string | false | | | -| `» name` | string | false | | | -| `» organization_id` | string(uuid) | false | | | -| `» readme` | string | false | | | -| `» template_id` | string(uuid) | false | | | -| `» updated_at` | string(date-time) | false | | | -| `» warnings` | array | false | | | +| Name | Type | Required | Restrictions | Description | +|-----------------------------|------------------------------------------------------------------------------|----------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `[array item]` | array | false | | | +| `» archived` | boolean | false | | | +| `» created_at` | string(date-time) | false | | | +| `» created_by` | [codersdk.MinimalUser](schemas.md#codersdkminimaluser) | false | | | +| `»» avatar_url` | string(uri) | false | | | +| `»» id` | string(uuid) | true | | | +| `»» username` | string | true | | | +| `» id` | string(uuid) | false | | | +| `» job` | [codersdk.ProvisionerJob](schemas.md#codersdkprovisionerjob) | false | | | +| `»» available_workers` | array | false | | | +| `»» canceled_at` | string(date-time) | false | | | +| `»» completed_at` | string(date-time) | false | | | +| `»» created_at` | string(date-time) | false | | | +| `»» error` | string | false | | | +| `»» error_code` | [codersdk.JobErrorCode](schemas.md#codersdkjoberrorcode) | false | | | +| `»» file_id` | string(uuid) | false | | | +| `»» id` | string(uuid) | false | | | +| `»» input` | [codersdk.ProvisionerJobInput](schemas.md#codersdkprovisionerjobinput) | false | | | +| `»»» error` | string | false | | | +| `»»» template_version_id` | string(uuid) | false | | | +| `»»» workspace_build_id` | string(uuid) | false | | | +| `»» metadata` | [codersdk.ProvisionerJobMetadata](schemas.md#codersdkprovisionerjobmetadata) | false | | | +| `»»» template_display_name` | string | false | | | +| `»»» template_icon` | string | false | | | +| `»»» template_id` | string(uuid) | false | | | +| `»»» template_name` | string | false | | | +| `»»» template_version_name` | string | false | | | +| `»»» workspace_id` | string(uuid) | false | | | +| `»»» workspace_name` | string | false | | | +| `»» organization_id` | string(uuid) | false | | | +| `»» queue_position` | integer | false | | | +| `»» queue_size` | integer | false | | | +| `»» started_at` | string(date-time) | false | | | +| `»» status` | [codersdk.ProvisionerJobStatus](schemas.md#codersdkprovisionerjobstatus) | false | | | +| `»» tags` | object | false | | | +| `»»» [any property]` | string | false | | | +| `»» type` | [codersdk.ProvisionerJobType](schemas.md#codersdkprovisionerjobtype) | false | | | +| `»» worker_id` | string(uuid) | false | | | +| `» matched_provisioners` | [codersdk.MatchedProvisioners](schemas.md#codersdkmatchedprovisioners) | false | | | +| `»» available` | integer | false | | Available is the number of provisioner daemons that are available to take jobs. This may be less than the count if some provisioners are busy or have been stopped. | +| `»» count` | integer | false | | Count is the number of provisioner daemons that matched the given tags. If the count is 0, it means no provisioner daemons matched the requested tags. | +| `»» most_recently_seen` | string(date-time) | false | | Most recently seen is the most recently seen time of the set of matched provisioners. If no provisioners matched, this field will be null. | +| `» message` | string | false | | | +| `» name` | string | false | | | +| `» organization_id` | string(uuid) | false | | | +| `» readme` | string | false | | | +| `» template_id` | string(uuid) | false | | | +| `» updated_at` | string(date-time) | false | | | +| `» warnings` | array | false | | | #### Enumerated Values @@ -1612,6 +1673,15 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion} \ "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" }, + "metadata": { + "template_display_name": "string", + "template_icon": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "template_name": "string", + "template_version_name": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", + "workspace_name": "string" + }, "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, @@ -1709,6 +1779,15 @@ curl -X PATCH http://coder-server:8080/api/v2/templateversions/{templateversion} "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" }, + "metadata": { + "template_display_name": "string", + "template_icon": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "template_name": "string", + "template_version_name": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", + "workspace_name": "string" + }, "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, @@ -1896,6 +1975,15 @@ curl -X POST http://coder-server:8080/api/v2/templateversions/{templateversion}/ "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" }, + "metadata": { + "template_display_name": "string", + "template_icon": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "template_name": "string", + "template_version_name": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", + "workspace_name": "string" + }, "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, @@ -1959,6 +2047,15 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/d "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" }, + "metadata": { + "template_display_name": "string", + "template_icon": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "template_name": "string", + "template_version_name": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", + "workspace_name": "string" + }, "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, @@ -2575,6 +2672,65 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/p To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Get template version presets + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/presets \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /templateversions/{templateversion}/presets` + +### Parameters + +| Name | In | Type | Required | Description | +|-------------------|------|--------------|----------|---------------------| +| `templateversion` | path | string(uuid) | true | Template version ID | + +### Example responses + +> 200 Response + +```json +[ + { + "id": "string", + "name": "string", + "parameters": [ + { + "name": "string", + "value": "string" + } + ] + } +] +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|-------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.Preset](schemas.md#codersdkpreset) | + +

Response Schema

+ +Status Code **200** + +| Name | Type | Required | Restrictions | Description | +|----------------|--------|----------|--------------|-------------| +| `[array item]` | array | false | | | +| `» id` | string | false | | | +| `» name` | string | false | | | +| `» parameters` | array | false | | | +| `»» name` | string | false | | | +| `»» value` | string | false | | | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Get resources by template version ### Code samples diff --git a/docs/reference/api/users.md b/docs/reference/api/users.md index d8aac77cfa83b..df0a8ca094df2 100644 --- a/docs/reference/api/users.md +++ b/docs/reference/api/users.md @@ -159,6 +159,7 @@ curl -X GET http://coder-server:8080/api/v2/users/authmethods \ ```json { "github": { + "default_provider_configured": true, "enabled": true }, "oidc": { @@ -337,6 +338,41 @@ curl -X GET http://coder-server:8080/api/v2/users/oauth2/github/callback \ To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Get Github device auth + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/users/oauth2/github/device \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /users/oauth2/github/device` + +### Example responses + +> 200 Response + +```json +{ + "device_code": "string", + "expires_in": 0, + "interval": 0, + "user_code": "string", + "verification_uri": "string" +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|----------------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.ExternalAuthDevice](schemas.md#codersdkexternalauthdevice) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## OpenID Connect Callback ### Code samples diff --git a/docs/reference/api/workspaces.md b/docs/reference/api/workspaces.md index e39e553927bf0..7264b6dbb3939 100644 --- a/docs/reference/api/workspaces.md +++ b/docs/reference/api/workspaces.md @@ -91,6 +91,15 @@ of the template will be used. "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" }, + "metadata": { + "template_display_name": "string", + "template_icon": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "template_name": "string", + "template_version_name": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", + "workspace_name": "string" + }, "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, @@ -332,6 +341,15 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" }, + "metadata": { + "template_display_name": "string", + "template_icon": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "template_name": "string", + "template_version_name": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", + "workspace_name": "string" + }, "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, @@ -597,6 +615,15 @@ of the template will be used. "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" }, + "metadata": { + "template_display_name": "string", + "template_icon": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "template_name": "string", + "template_version_name": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", + "workspace_name": "string" + }, "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, @@ -841,6 +868,15 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \ "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" }, + "metadata": { + "template_display_name": "string", + "template_icon": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "template_name": "string", + "template_version_name": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", + "workspace_name": "string" + }, "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, @@ -1079,6 +1115,15 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \ "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" }, + "metadata": { + "template_display_name": "string", + "template_icon": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "template_name": "string", + "template_version_name": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", + "workspace_name": "string" + }, "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, @@ -1436,6 +1481,15 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/dormant \ "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" }, + "metadata": { + "template_display_name": "string", + "template_icon": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "template_name": "string", + "template_version_name": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", + "workspace_name": "string" + }, "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, diff --git a/docs/reference/cli/notifications.md b/docs/reference/cli/notifications.md index 169776876e315..14642fd8ddb9f 100644 --- a/docs/reference/cli/notifications.md +++ b/docs/reference/cli/notifications.md @@ -26,11 +26,17 @@ server or Webhook not responding).: - Resume Coder notifications: $ coder notifications resume + + - Send a test notification. Administrators can use this to verify the notification +target settings.: + + $ coder notifications test ``` ## Subcommands -| Name | Purpose | -|--------------------------------------------------|----------------------| -| [pause](./notifications_pause.md) | Pause notifications | -| [resume](./notifications_resume.md) | Resume notifications | +| Name | Purpose | +|--------------------------------------------------|--------------------------| +| [pause](./notifications_pause.md) | Pause notifications | +| [resume](./notifications_resume.md) | Resume notifications | +| [test](./notifications_test.md) | Send a test notification | diff --git a/docs/reference/cli/notifications_test.md b/docs/reference/cli/notifications_test.md new file mode 100644 index 0000000000000..794c3e0d35a3b --- /dev/null +++ b/docs/reference/cli/notifications_test.md @@ -0,0 +1,10 @@ + +# notifications test + +Send a test notification + +## Usage + +```console +coder notifications test +``` diff --git a/docs/reference/cli/ping.md b/docs/reference/cli/ping.md index 8fbc1eaf36e8e..829f131818901 100644 --- a/docs/reference/cli/ping.md +++ b/docs/reference/cli/ping.md @@ -36,3 +36,19 @@ Specifies how long to wait for a ping to complete. | Type | int | Specifies the number of pings to perform. By default, pings will continue until interrupted. + +### --time + +| | | +|------|-------------------| +| Type | bool | + +Show the response time of each pong in local time. + +### --utc + +| | | +|------|-------------------| +| Type | bool | + +Show the response time of each pong in UTC (implies --time). diff --git a/docs/reference/cli/provisioner_jobs_list.md b/docs/reference/cli/provisioner_jobs_list.md index 03e187b1c6720..2cd40049e2400 100644 --- a/docs/reference/cli/provisioner_jobs_list.md +++ b/docs/reference/cli/provisioner_jobs_list.md @@ -45,10 +45,10 @@ Select which organization (uuid or name) to use. ### -c, --column -| | | -|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Type | [id\|created at\|started at\|completed at\|canceled at\|error\|error code\|status\|worker id\|file id\|tags\|queue position\|queue size\|organization id\|template version id\|workspace build id\|type\|available workers\|organization\|queue] | -| Default | created at,id,organization,status,type,queue,tags | +| | | +|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Type | [id\|created at\|started at\|completed at\|canceled at\|error\|error code\|status\|worker id\|file id\|tags\|queue position\|queue size\|organization id\|template version id\|workspace build id\|type\|available workers\|template version name\|template id\|template name\|template display name\|template icon\|workspace id\|workspace name\|organization\|queue] | +| Default | created at,id,organization,status,type,queue,tags | Columns to display in table output. diff --git a/docs/reference/cli/provisioner_list.md b/docs/reference/cli/provisioner_list.md index 11abd7dcc3d75..93718ddd01ea8 100644 --- a/docs/reference/cli/provisioner_list.md +++ b/docs/reference/cli/provisioner_list.md @@ -26,10 +26,10 @@ Select which organization (uuid or name) to use. ### -c, --column -| | | -|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Type | [id\|organization id\|created at\|last seen at\|name\|version\|api version\|tags\|key name\|status\|current job id\|current job status\|previous job id\|previous job status\|organization] | -| Default | name,organization,status,key name,created at,last seen at,version,tags | +| | | +|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Type | [id\|organization id\|created at\|last seen at\|name\|version\|api version\|tags\|key name\|status\|current job id\|current job status\|current job template name\|current job template icon\|current job template display name\|previous job id\|previous job status\|previous job template name\|previous job template icon\|previous job template display name\|organization] | +| Default | name,organization,status,key name,created at,last seen at,version,tags | Columns to display in table output. diff --git a/docs/reference/cli/server.md b/docs/reference/cli/server.md index 98cb2a90c20da..91d565952d943 100644 --- a/docs/reference/cli/server.md +++ b/docs/reference/cli/server.md @@ -362,6 +362,28 @@ Client ID for Login with GitHub. Client secret for Login with GitHub. +### --oauth2-github-device-flow + +| | | +|-------------|-----------------------------------------------| +| Type | bool | +| Environment | $CODER_OAUTH2_GITHUB_DEVICE_FLOW | +| YAML | oauth2.github.deviceFlow | +| Default | false | + +Enable device flow for Login with GitHub. + +### --oauth2-github-default-provider-enable + +| | | +|-------------|-----------------------------------------------------------| +| Type | bool | +| Environment | $CODER_OAUTH2_GITHUB_DEFAULT_PROVIDER_ENABLE | +| YAML | oauth2.github.defaultProviderEnable | +| Default | true | + +Enable the default GitHub OAuth2 provider managed by Coder. + ### --oauth2-github-allowed-orgs | | | diff --git a/docs/reference/cli/tokens_remove.md b/docs/reference/cli/tokens_remove.md index 8825040f5e3a7..ae443f6ad083e 100644 --- a/docs/reference/cli/tokens_remove.md +++ b/docs/reference/cli/tokens_remove.md @@ -11,5 +11,5 @@ Aliases: ## Usage ```console -coder tokens remove +coder tokens remove ``` diff --git a/docs/tutorials/best-practices/organizations.md b/docs/tutorials/best-practices/organizations.md index 473bf832e11d8..7228f8a3006aa 100644 --- a/docs/tutorials/best-practices/organizations.md +++ b/docs/tutorials/best-practices/organizations.md @@ -1,7 +1,5 @@ # Organizations - best practices -December 9, 2024 - --- Coder [Organizations](../../admin/users/organizations.md) allow administrators @@ -80,10 +78,8 @@ cannot access. Instead, the control plane sends simple "provisioner jobs" to the provisioner and the provisioner is responsible for executing the Terraform. There are planned improvements to the troubleshooting provisioners process. -Follow these GitHub issues for more details: +Follow this GitHub issue for more details: -- [coder/coder#15048](https://github.com/coder/coder/issues/15048) -- [coder/coder#15087](https://github.com/coder/coder/issues/15087) - [coder/coder#15192](https://github.com/coder/coder/issues/15192) ## Identity Provider (SSO) Sync @@ -94,17 +90,6 @@ provider such as Okta. A single claim from the identity provider (like `memberOf`) can be used to sync site-wide roles, organizations, groups, and organization roles. -### Planned enhancements - -Site-wide role sync is managed via server flags. We plan on changing this to -runtime configuration so Coder does not need a re-deploy: - -- Issue [coder/internal#86](https://github.com/coder/internal/issues/86) - -Make all sync configurable via the dashboard UI: - -- [coder/coder#15290](https://github.com/coder/coder/issues/15290) - Regex filters and mapping can be configured to ensure the proper resources are allocated in Coder. Learn more about [IDP sync](../../admin/users/idp-sync.md). diff --git a/docs/tutorials/faqs.md b/docs/tutorials/faqs.md index 06cc76adbe9b1..184e6dedb2ee1 100644 --- a/docs/tutorials/faqs.md +++ b/docs/tutorials/faqs.md @@ -11,28 +11,61 @@ For other community resources, see our ## How do I add a Premium trial license? Visit or contact - [sales@coder.com](mailto:sales@coder.com?subject=License) to get a trial key. -You can add a license through the UI or CLI. +
-In the UI, click the Deployment tab -> Licenses and upload the `jwt` license -file. +You can add a license through the UI or CLI -> To add the license with the CLI, first -> [install the Coder CLI](../install/cli.md) and server to the latest release. + -If the license is a text string: +
-```sh -coder licenses add -l 1f5...765 -``` +### Coder UI -If the license is in a file: +1. With an `Owner` account, go to **Admin settings** > **Deployment**. -```sh -coder licenses add -f -``` +1. Select **Licenses** from the sidebar, then **Add a license**: + + ![Add a license from the licenses screen](../images/admin/licenses/licenses-nolicense.png) + +1. On the **Add a license** screen, drag your `.jwt` license file into the + **Upload Your License** section, or paste your license in the + **Paste Your License** text box, then select **Upload License**: + + ![Add a license screen](../images/admin/licenses/add-license-ui.png) + +### Coder CLI + +1. Ensure you have the [Coder CLI](../install/cli.md) installed. +1. Save your license key to disk and make note of the path. +1. Open a terminal. +1. Log in to your Coder deployment: + + ```shell + coder login + ``` + +1. Run `coder licenses add`: + + - For a `.jwt` license file: + + ```shell + coder licenses add -f + ``` + + - For a text string: + + ```sh + coder licenses add -l 1f5...765 + ``` + +
+ +
+ +Visit the [licensing documentation](../admin/licensing/index.md) for more +information about licenses. ## I'm experiencing networking issues, so want to disable Tailscale, STUN, Direct connections and force use of websocket diff --git a/docs/tutorials/quickstart.md b/docs/tutorials/quickstart.md index 4f66165fd7459..feff2971077ee 100644 --- a/docs/tutorials/quickstart.md +++ b/docs/tutorials/quickstart.md @@ -82,18 +82,22 @@ persistent environment from your main device, a tablet, or your phone. ## Configure Coder with a new Workspace -1. If you're running Coder locally, go to . +1. Coder will attempt to open the setup page in your browser. If it doesn't open + automatically, go to . - If you get a browser warning similar to `Secure Site Not Available`, you can ignore the warning and continue to the setup page. - If your Coder server is on a network or cloud device, locate the message in - your terminal that reads, - `View the Web UI: https://..try.coder.app`. The server - begins to stream logs immediately and you might have to scroll up to find it. + If your Coder server is on a network or cloud device, or you are having + trouble viewing the page, locate the web UI URL in Coder logs in your + terminal. It looks like `https://..try.coder.app`. + It's one of the first lines of output, so you might have to scroll up to find + it. -1. On the **Welcome to Coder** page, enter the information to create an admin - user, then select **Create account**. +1. On the **Welcome to Coder** page, to use your GitHub account to log in, + select **Continue with GitHub**. + You can also enter an email and password to create a new admin account on + the Coder deployment: ![Welcome to Coder - Create admin user](../images/screenshots/welcome-create-admin-user.png)_Welcome to Coder - Create admin user_ diff --git a/docs/user-guides/workspace-access/zed.md b/docs/user-guides/workspace-access/zed.md index 14a02e08a611f..2bcb4f12a2209 100644 --- a/docs/user-guides/workspace-access/zed.md +++ b/docs/user-guides/workspace-access/zed.md @@ -27,11 +27,6 @@ Use the Coder CLI to log in and configure SSH, then connect to your workspace wi ### Windows - > **Important:** If you plan to use the built-in PostgreSQL database, you will - > need to ensure that the - > [Visual C++ Runtime](https://learn.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist#latest-microsoft-visual-c-redistributable-version) - > is installed. - Use [GitHub releases](https://github.com/coder/coder/releases) to download the Windows installer (`.msi`) or standalone binary (`.exe`). diff --git a/dogfood/contents/Dockerfile b/dogfood/contents/Dockerfile index 2de358c5c91e6..1aac42579b9a3 100644 --- a/dogfood/contents/Dockerfile +++ b/dogfood/contents/Dockerfile @@ -160,6 +160,7 @@ RUN apt-get update --quiet && apt-get install --yes \ kubectl \ language-pack-en \ less \ + libgbm-dev \ libssl-dev \ lsb-release \ man \ @@ -195,9 +196,9 @@ RUN apt-get update --quiet && apt-get install --yes \ # Configure FIPS-compliant policies update-crypto-policies --set FIPS -# NOTE: In scripts/Dockerfile.base we specifically install Terraform version 1.9.8. +# NOTE: In scripts/Dockerfile.base we specifically install Terraform version 1.10.5. # Installing the same version here to match. -RUN wget -O /tmp/terraform.zip "https://releases.hashicorp.com/terraform/1.9.8/terraform_1.9.8_linux_amd64.zip" && \ +RUN wget -O /tmp/terraform.zip "https://releases.hashicorp.com/terraform/1.10.5/terraform_1.10.5_linux_amd64.zip" && \ unzip /tmp/terraform.zip -d /usr/local/bin && \ rm -f /tmp/terraform.zip && \ chmod +x /usr/local/bin/terraform && \ diff --git a/dogfood/contents/main.tf b/dogfood/contents/main.tf index 62329c7b1c616..998b463f82ab2 100644 --- a/dogfood/contents/main.tf +++ b/dogfood/contents/main.tf @@ -1,7 +1,8 @@ terraform { required_providers { coder = { - source = "coder/coder" + source = "coder/coder" + version = "2.2.0-pre0" } docker = { source = "kreuzwerker/docker" @@ -84,6 +85,38 @@ data "coder_parameter" "region" { } } +data "coder_parameter" "res_mon_memory_threshold" { + type = "number" + name = "Memory usage threshold" + default = 80 + description = "The memory usage threshold used in resources monitoring to trigger notifications." + mutable = true + validation { + min = 0 + max = 100 + } +} + +data "coder_parameter" "res_mon_volume_threshold" { + type = "number" + name = "Volume usage threshold" + default = 90 + description = "The volume usage threshold used in resources monitoring to trigger notifications." + mutable = true + validation { + min = 0 + max = 100 + } +} + +data "coder_parameter" "res_mon_volume_path" { + type = "string" + name = "Volume path" + default = "/home/coder" + description = "The path monitored in resources monitoring to trigger notifications." + mutable = true +} + provider "docker" { host = lookup(local.docker_host, data.coder_parameter.region.value) } @@ -249,7 +282,7 @@ resource "coder_agent" "dev" { key = "swap_usage_host" order = 4 script = <&1 | awk ' $0 ~ "Word of the Day: [A-z]+" { print $5; exit }' EOT interval = 86400 timeout = 5 } + resources_monitoring { + memory { + enabled = true + threshold = data.coder_parameter.res_mon_memory_threshold.value + } + volume { + enabled = true + threshold = data.coder_parameter.res_mon_volume_threshold.value + path = data.coder_parameter.res_mon_volume_path.value + } + } + startup_script = <<-EOT + #!/usr/bin/env bash set -eux -o pipefail # Allow synchronization between scripts. @@ -304,6 +350,7 @@ resource "coder_agent" "dev" { while ! [[ -f "${local.repo_dir}/site/package.json" ]]; do sleep 1 done + cd "${local.repo_dir}" && make clean cd "${local.repo_dir}/site" && pnpm install && pnpm playwright:install EOT } @@ -373,6 +420,7 @@ resource "docker_container" "workspace" { "CODER_PROC_PRIO_MGMT=1", "CODER_PROC_OOM_SCORE=10", "CODER_PROC_NICE_SCORE=1", + "CODER_AGENT_DEVCONTAINERS_ENABLE=1", ] host { host = "host.docker.internal" diff --git a/dogfood/contents/nix.hash b/dogfood/contents/nix.hash index 426d2750e7555..d1b017c8b61e9 100644 --- a/dogfood/contents/nix.hash +++ b/dogfood/contents/nix.hash @@ -1,2 +1,2 @@ -91e81c240fcf9f72e4c67497b68ba247a3f901147b61736072eb234e03db87b5 flake.nix -b43d86368a0d2713d646d57e964dc2ac49744f5e11b6395fabed2d49596c1615 flake.lock +f41c80bd08bfef063a9cfe907d0ea1f377974ebe011751f64008a3a07a6b152a flake.nix +32c441011f1f3054a688c036a85eac5e4c3dbef0f8cfa4ab85acd82da577dc35 flake.lock diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index b72a64c2eeae4..53f03dd60ae63 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -27,6 +27,8 @@ var AuditActionMap = map[string][]codersdk.AuditAction{ "Group": {codersdk.AuditActionCreate, codersdk.AuditActionWrite, codersdk.AuditActionDelete}, "APIKey": {codersdk.AuditActionLogin, codersdk.AuditActionLogout, codersdk.AuditActionRegister, codersdk.AuditActionCreate, codersdk.AuditActionDelete}, "License": {codersdk.AuditActionCreate, codersdk.AuditActionDelete}, + "WorkspaceAgent": {codersdk.AuditActionConnect, codersdk.AuditActionDisconnect}, + "WorkspaceApp": {codersdk.AuditActionOpen, codersdk.AuditActionClose}, } type Action string @@ -170,22 +172,23 @@ var auditableResourcesTypes = map[any]map[string]Action{ "next_start_at": ActionTrack, }, &database.WorkspaceBuild{}: { - "id": ActionIgnore, - "created_at": ActionIgnore, - "updated_at": ActionIgnore, - "workspace_id": ActionIgnore, - "template_version_id": ActionTrack, - "build_number": ActionIgnore, - "transition": ActionIgnore, - "initiator_id": ActionIgnore, - "provisioner_state": ActionIgnore, - "job_id": ActionIgnore, - "deadline": ActionIgnore, - "reason": ActionIgnore, - "daily_cost": ActionIgnore, - "max_deadline": ActionIgnore, - "initiator_by_avatar_url": ActionIgnore, - "initiator_by_username": ActionIgnore, + "id": ActionIgnore, + "created_at": ActionIgnore, + "updated_at": ActionIgnore, + "workspace_id": ActionIgnore, + "template_version_id": ActionTrack, + "build_number": ActionIgnore, + "transition": ActionIgnore, + "initiator_id": ActionIgnore, + "provisioner_state": ActionIgnore, + "job_id": ActionIgnore, + "deadline": ActionIgnore, + "reason": ActionIgnore, + "daily_cost": ActionIgnore, + "max_deadline": ActionIgnore, + "initiator_by_avatar_url": ActionIgnore, + "initiator_by_username": ActionIgnore, + "template_version_preset_id": ActionIgnore, // Never changes. }, &database.AuditableGroup{}: { "id": ActionTrack, @@ -272,6 +275,7 @@ var auditableResourcesTypes = map[any]map[string]Action{ "id": ActionIgnore, "name": ActionTrack, "description": ActionTrack, + "deleted": ActionTrack, "created_at": ActionIgnore, "updated_at": ActionTrack, "is_default": ActionTrack, @@ -306,6 +310,59 @@ var auditableResourcesTypes = map[any]map[string]Action{ "field": ActionTrack, "mapping": ActionTrack, }, + &database.WorkspaceAgent{}: { + "id": ActionIgnore, + "created_at": ActionIgnore, + "updated_at": ActionIgnore, + "name": ActionIgnore, + "first_connected_at": ActionIgnore, + "last_connected_at": ActionIgnore, + "disconnected_at": ActionIgnore, + "resource_id": ActionIgnore, + "auth_token": ActionIgnore, + "auth_instance_id": ActionIgnore, + "architecture": ActionIgnore, + "environment_variables": ActionIgnore, + "operating_system": ActionIgnore, + "instance_metadata": ActionIgnore, + "resource_metadata": ActionIgnore, + "directory": ActionIgnore, + "version": ActionIgnore, + "last_connected_replica_id": ActionIgnore, + "connection_timeout_seconds": ActionIgnore, + "troubleshooting_url": ActionIgnore, + "motd_file": ActionIgnore, + "lifecycle_state": ActionIgnore, + "expanded_directory": ActionIgnore, + "logs_length": ActionIgnore, + "logs_overflowed": ActionIgnore, + "started_at": ActionIgnore, + "ready_at": ActionIgnore, + "subsystems": ActionIgnore, + "display_apps": ActionIgnore, + "api_version": ActionIgnore, + "display_order": ActionIgnore, + }, + &database.WorkspaceApp{}: { + "id": ActionIgnore, + "created_at": ActionIgnore, + "agent_id": ActionIgnore, + "display_name": ActionIgnore, + "icon": ActionIgnore, + "command": ActionIgnore, + "url": ActionIgnore, + "healthcheck_url": ActionIgnore, + "healthcheck_interval": ActionIgnore, + "healthcheck_threshold": ActionIgnore, + "health": ActionIgnore, + "subdomain": ActionIgnore, + "sharing_level": ActionIgnore, + "slug": ActionIgnore, + "external": ActionIgnore, + "display_order": ActionIgnore, + "hidden": ActionIgnore, + "open_in": ActionIgnore, + }, } // auditMap converts a map of struct pointers to a map of struct names as diff --git a/enterprise/cli/provisionerdaemonstart.go b/enterprise/cli/provisionerdaemonstart.go index 3c3f1f0712800..8d7d319d39c2b 100644 --- a/enterprise/cli/provisionerdaemonstart.go +++ b/enterprise/cli/provisionerdaemonstart.go @@ -236,10 +236,11 @@ func (r *RootCmd) provisionerDaemonStart() *serpent.Command { ProvisionerKey: provisionerKey, }) }, &provisionerd.Options{ - Logger: logger, - UpdateInterval: 500 * time.Millisecond, - Connector: connector, - Metrics: metrics, + Logger: logger, + UpdateInterval: 500 * time.Millisecond, + Connector: connector, + Metrics: metrics, + ExternalProvisioner: true, }) waitForProvisionerJobs := false diff --git a/enterprise/cli/proxyserver.go b/enterprise/cli/proxyserver.go index 9e10048146481..a4a989ae0460f 100644 --- a/enterprise/cli/proxyserver.go +++ b/enterprise/cli/proxyserver.go @@ -205,7 +205,7 @@ func (r *RootCmd) proxyServer() *serpent.Command { httpClient.Transport = headerTransport accessURL := cfg.AccessURL.String() - cliui.Infof(inv.Stdout, lipgloss.NewStyle(). + cliui.Info(inv.Stdout, lipgloss.NewStyle(). Border(lipgloss.DoubleBorder()). Align(lipgloss.Center). Padding(0, 3). diff --git a/enterprise/cli/server_dbcrypt_test.go b/enterprise/cli/server_dbcrypt_test.go index 070f172bcbe7b..06851dd0a2eaf 100644 --- a/enterprise/cli/server_dbcrypt_test.go +++ b/enterprise/cli/server_dbcrypt_test.go @@ -44,7 +44,7 @@ func TestServerDBCrypt(t *testing.T) { db := database.New(sqlDB) // Populate the database with some unencrypted data. - t.Logf("Generating unencrypted data") + t.Log("Generating unencrypted data") users := genData(t, db) // Setup an initial cipher A @@ -57,7 +57,7 @@ func TestServerDBCrypt(t *testing.T) { require.NoError(t, err) // Populate the database with some encrypted data using cipher A. - t.Logf("Generating data encrypted with cipher A") + t.Log("Generating data encrypted with cipher A") newUsers := genData(t, cryptdb) // Validate that newly created users were encrypted with cipher A @@ -67,7 +67,7 @@ func TestServerDBCrypt(t *testing.T) { users = append(users, newUsers...) // Encrypt all the data with the initial cipher. - t.Logf("Encrypting all data with cipher A") + t.Log("Encrypting all data with cipher A") inv, _ := newCLI(t, "server", "dbcrypt", "rotate", "--postgres-url", connectionURL, "--new-key", base64.StdEncoding.EncodeToString([]byte(keyA)), @@ -89,7 +89,7 @@ func TestServerDBCrypt(t *testing.T) { cipherBA, err := dbcrypt.NewCiphers([]byte(keyB), []byte(keyA)) require.NoError(t, err) - t.Logf("Enrypting all data with cipher B") + t.Log("Enrypting all data with cipher B") inv, _ = newCLI(t, "server", "dbcrypt", "rotate", "--postgres-url", connectionURL, "--new-key", base64.StdEncoding.EncodeToString([]byte(keyB)), @@ -108,7 +108,7 @@ func TestServerDBCrypt(t *testing.T) { } // Assert that we can revoke the old key. - t.Logf("Revoking cipher A") + t.Log("Revoking cipher A") err = db.RevokeDBCryptKey(ctx, cipherA[0].HexDigest()) require.NoError(t, err, "failed to revoke old key") @@ -124,7 +124,7 @@ func TestServerDBCrypt(t *testing.T) { require.Empty(t, oldKey.ActiveKeyDigest.String, "expected the old key to not be active") // Revoking the new key should fail. - t.Logf("Attempting to revoke cipher B should fail as it is still in use") + t.Log("Attempting to revoke cipher B should fail as it is still in use") err = db.RevokeDBCryptKey(ctx, cipherBA[0].HexDigest()) require.Error(t, err, "expected to fail to revoke the new key") var pgErr *pq.Error @@ -132,7 +132,7 @@ func TestServerDBCrypt(t *testing.T) { require.EqualValues(t, "23503", pgErr.Code, "expected a foreign key constraint violation error") // Decrypt the data using only cipher B. This should result in the key being revoked. - t.Logf("Decrypting with cipher B") + t.Log("Decrypting with cipher B") inv, _ = newCLI(t, "server", "dbcrypt", "decrypt", "--postgres-url", connectionURL, "--keys", base64.StdEncoding.EncodeToString([]byte(keyB)), @@ -162,7 +162,7 @@ func TestServerDBCrypt(t *testing.T) { cipherC, err := dbcrypt.NewCiphers([]byte(keyC)) require.NoError(t, err) - t.Logf("Re-encrypting with cipher C") + t.Log("Re-encrypting with cipher C") inv, _ = newCLI(t, "server", "dbcrypt", "rotate", "--postgres-url", connectionURL, "--new-key", base64.StdEncoding.EncodeToString([]byte(keyC)), @@ -181,7 +181,7 @@ func TestServerDBCrypt(t *testing.T) { } // Now delete all the encrypted data. - t.Logf("Deleting all encrypted data") + t.Log("Deleting all encrypted data") inv, _ = newCLI(t, "server", "dbcrypt", "delete", "--postgres-url", connectionURL, "--external-token-encryption-keys", base64.StdEncoding.EncodeToString([]byte(keyC)), diff --git a/enterprise/cli/testdata/coder_provisioner_jobs_list_--help.golden b/enterprise/cli/testdata/coder_provisioner_jobs_list_--help.golden index 585e918c23e7b..d6eb9a7681a07 100644 --- a/enterprise/cli/testdata/coder_provisioner_jobs_list_--help.golden +++ b/enterprise/cli/testdata/coder_provisioner_jobs_list_--help.golden @@ -11,7 +11,7 @@ OPTIONS: -O, --org string, $CODER_ORGANIZATION Select which organization (uuid or name) to use. - -c, --column [id|created at|started at|completed at|canceled at|error|error code|status|worker id|file id|tags|queue position|queue size|organization id|template version id|workspace build id|type|available workers|organization|queue] (default: created at,id,organization,status,type,queue,tags) + -c, --column [id|created at|started at|completed at|canceled at|error|error code|status|worker id|file id|tags|queue position|queue size|organization id|template version id|workspace build id|type|available workers|template version name|template id|template name|template display name|template icon|workspace id|workspace name|organization|queue] (default: created at,id,organization,status,type,queue,tags) Columns to display in table output. -l, --limit int, $CODER_PROVISIONER_JOB_LIST_LIMIT (default: 50) diff --git a/enterprise/cli/testdata/coder_provisioner_list_--help.golden b/enterprise/cli/testdata/coder_provisioner_list_--help.golden index a9943cb9da392..111eb8315b162 100644 --- a/enterprise/cli/testdata/coder_provisioner_list_--help.golden +++ b/enterprise/cli/testdata/coder_provisioner_list_--help.golden @@ -11,7 +11,7 @@ OPTIONS: -O, --org string, $CODER_ORGANIZATION Select which organization (uuid or name) to use. - -c, --column [id|organization id|created at|last seen at|name|version|api version|tags|key name|status|current job id|current job status|previous job id|previous job status|organization] (default: name,organization,status,key name,created at,last seen at,version,tags) + -c, --column [id|organization id|created at|last seen at|name|version|api version|tags|key name|status|current job id|current job status|current job template name|current job template icon|current job template display name|previous job id|previous job status|previous job template name|previous job template icon|previous job template display name|organization] (default: name,organization,status,key name,created at,last seen at,version,tags) Columns to display in table output. -o, --output table|json (default: table) diff --git a/enterprise/cli/testdata/coder_server_--help.golden b/enterprise/cli/testdata/coder_server_--help.golden index ebaf1a5ac0bbd..f0b3e4b0aaac7 100644 --- a/enterprise/cli/testdata/coder_server_--help.golden +++ b/enterprise/cli/testdata/coder_server_--help.golden @@ -499,6 +499,12 @@ OAUTH2 / GITHUB OPTIONS: --oauth2-github-client-secret string, $CODER_OAUTH2_GITHUB_CLIENT_SECRET Client secret for Login with GitHub. + --oauth2-github-default-provider-enable bool, $CODER_OAUTH2_GITHUB_DEFAULT_PROVIDER_ENABLE (default: true) + Enable the default GitHub OAuth2 provider managed by Coder. + + --oauth2-github-device-flow bool, $CODER_OAUTH2_GITHUB_DEVICE_FLOW (default: false) + Enable device flow for Login with GitHub. + --oauth2-github-enterprise-base-url string, $CODER_OAUTH2_GITHUB_ENTERPRISE_BASE_URL Base URL of a GitHub Enterprise deployment to use for Login with GitHub. diff --git a/enterprise/cmd/coder/main.go b/enterprise/cmd/coder/main.go index 803903f390e5a..217cca324b762 100644 --- a/enterprise/cmd/coder/main.go +++ b/enterprise/cmd/coder/main.go @@ -8,6 +8,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/coder/coder/v2/agent/agentexec" + _ "github.com/coder/coder/v2/buildinfo/resources" entcli "github.com/coder/coder/v2/enterprise/cli" ) diff --git a/enterprise/coderd/audit_test.go b/enterprise/coderd/audit_test.go index d5616ea3888b9..271671491860d 100644 --- a/enterprise/coderd/audit_test.go +++ b/enterprise/coderd/audit_test.go @@ -75,10 +75,6 @@ func TestEnterpriseAuditLogs(t *testing.T) { require.Equal(t, int64(1), alogs.Count) require.Len(t, alogs.AuditLogs, 1) - require.Equal(t, &codersdk.MinimalOrganization{ - ID: o.ID, - }, alogs.AuditLogs[0].Organization) - // OrganizationID is deprecated, but make sure it is set. require.Equal(t, o.ID, alogs.AuditLogs[0].OrganizationID) diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index b32f763720b9d..2a91fbbfd6f93 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -295,7 +295,10 @@ func New(ctx context.Context, options *Options) (_ *API, err error) { r.Route("/organization", func(r chi.Router) { r.Get("/", api.organizationIDPSyncSettings) r.Patch("/", api.patchOrganizationIDPSyncSettings) + r.Patch("/config", api.patchOrganizationIDPSyncConfig) + r.Patch("/mapping", api.patchOrganizationIDPSyncMapping) }) + r.Get("/available-fields", api.deploymentIDPSyncClaimFields) r.Get("/field-values", api.deploymentIDPSyncClaimFieldValues) }) @@ -307,11 +310,17 @@ func New(ctx context.Context, options *Options) (_ *API, err error) { httpmw.ExtractOrganizationParam(api.Database), ) r.Route("/organizations/{organization}/settings", func(r chi.Router) { - r.Get("/idpsync/available-fields", api.organizationIDPSyncClaimFields) r.Get("/idpsync/groups", api.groupIDPSyncSettings) r.Patch("/idpsync/groups", api.patchGroupIDPSyncSettings) + r.Patch("/idpsync/groups/config", api.patchGroupIDPSyncConfig) + r.Patch("/idpsync/groups/mapping", api.patchGroupIDPSyncMapping) + r.Get("/idpsync/roles", api.roleIDPSyncSettings) r.Patch("/idpsync/roles", api.patchRoleIDPSyncSettings) + r.Patch("/idpsync/roles/config", api.patchRoleIDPSyncConfig) + r.Patch("/idpsync/roles/mapping", api.patchRoleIDPSyncMapping) + + r.Get("/idpsync/available-fields", api.organizationIDPSyncClaimFields) r.Get("/idpsync/field-values", api.organizationIDPSyncClaimFieldValues) }) }) diff --git a/enterprise/coderd/enidpsync/enidpsync.go b/enterprise/coderd/enidpsync/enidpsync.go index c7ba8dd3ecdc6..2020a4300ebc6 100644 --- a/enterprise/coderd/enidpsync/enidpsync.go +++ b/enterprise/coderd/enidpsync/enidpsync.go @@ -7,6 +7,8 @@ import ( "github.com/coder/coder/v2/coderd/runtimeconfig" ) +var _ idpsync.IDPSync = &EnterpriseIDPSync{} + // EnterpriseIDPSync enabled syncing user information from an external IDP. // The sync is an enterprise feature, so this struct wraps the AGPL implementation // and extends it with enterprise capabilities. These capabilities can entirely diff --git a/enterprise/coderd/enidpsync/organizations.go b/enterprise/coderd/enidpsync/organizations.go index 313d90fac8a9f..826144afc1492 100644 --- a/enterprise/coderd/enidpsync/organizations.go +++ b/enterprise/coderd/enidpsync/organizations.go @@ -19,6 +19,8 @@ func (e EnterpriseIDPSync) OrganizationSyncEnabled(ctx context.Context, db datab return false } + // If this logic is ever updated, make sure to update the corresponding + // checkIDPOrgSync in coderd/telemetry/telemetry.go. settings, err := e.OrganizationSyncSettings(ctx, db) if err == nil && settings.Field != "" { return true diff --git a/enterprise/coderd/enidpsync/organizations_test.go b/enterprise/coderd/enidpsync/organizations_test.go index 36dbedf3a466d..391535c9478d7 100644 --- a/enterprise/coderd/enidpsync/organizations_test.go +++ b/enterprise/coderd/enidpsync/organizations_test.go @@ -300,7 +300,7 @@ func TestOrganizationSync(t *testing.T) { // Create a new sync object sync := enidpsync.NewSync(logger, runtimeconfig.NewManager(), caseData.Entitlements, caseData.Settings) if caseData.RuntimeSettings != nil { - err := sync.UpdateOrganizationSettings(ctx, rdb, *caseData.RuntimeSettings) + err := sync.UpdateOrganizationSyncSettings(ctx, rdb, *caseData.RuntimeSettings) require.NoError(t, err) } diff --git a/enterprise/coderd/groups.go b/enterprise/coderd/groups.go index 8d5a7fceefaec..9771dd9800bb0 100644 --- a/enterprise/coderd/groups.go +++ b/enterprise/coderd/groups.go @@ -440,7 +440,10 @@ func (api *API) groups(rw http.ResponseWriter, r *http.Request) { parser := httpapi.NewQueryParamParser() // Organization selector can be an org ID or name filter.OrganizationID = parser.UUIDorName(r.URL.Query(), uuid.Nil, "organization", func(orgName string) (uuid.UUID, error) { - org, err := api.Database.GetOrganizationByName(ctx, orgName) + org, err := api.Database.GetOrganizationByName(ctx, database.GetOrganizationByNameParams{ + Name: orgName, + Deleted: false, + }) if err != nil { return uuid.Nil, xerrors.Errorf("organization %q not found", orgName) } diff --git a/enterprise/coderd/idpsync.go b/enterprise/coderd/idpsync.go index 192d61ea996c6..2dcee572eb692 100644 --- a/enterprise/coderd/idpsync.go +++ b/enterprise/coderd/idpsync.go @@ -3,6 +3,7 @@ package coderd import ( "fmt" "net/http" + "slices" "github.com/google/uuid" @@ -14,6 +15,7 @@ import ( "github.com/coder/coder/v2/coderd/idpsync" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" + "github.com/coder/coder/v2/coderd/util/slice" "github.com/coder/coder/v2/codersdk" ) @@ -59,7 +61,6 @@ func (api *API) patchGroupIDPSyncSettings(rw http.ResponseWriter, r *http.Reques ctx := r.Context() org := httpmw.OrganizationParam(r) auditor := *api.AGPL.Auditor.Load() - aReq, commitAudit := audit.InitRequest[idpsync.GroupSyncSettings](rw, &audit.RequestParams{ Audit: auditor, Log: api.Logger, @@ -102,7 +103,7 @@ func (api *API) patchGroupIDPSyncSettings(rw http.ResponseWriter, r *http.Reques } aReq.Old = *existing - err = api.IDPSync.UpdateGroupSettings(sysCtx, org.ID, api.Database, idpsync.GroupSyncSettings{ + err = api.IDPSync.UpdateGroupSyncSettings(sysCtx, org.ID, api.Database, idpsync.GroupSyncSettings{ Field: req.Field, Mapping: req.Mapping, RegexFilter: req.RegexFilter, @@ -130,6 +131,153 @@ func (api *API) patchGroupIDPSyncSettings(rw http.ResponseWriter, r *http.Reques }) } +// @Summary Update group IdP Sync config +// @ID update-group-idp-sync-config +// @Security CoderSessionToken +// @Produce json +// @Accept json +// @Tags Enterprise +// @Success 200 {object} codersdk.GroupSyncSettings +// @Param organization path string true "Organization ID or name" format(uuid) +// @Param request body codersdk.PatchGroupIDPSyncConfigRequest true "New config values" +// @Router /organizations/{organization}/settings/idpsync/groups/config [patch] +func (api *API) patchGroupIDPSyncConfig(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + org := httpmw.OrganizationParam(r) + auditor := *api.AGPL.Auditor.Load() + aReq, commitAudit := audit.InitRequest[idpsync.GroupSyncSettings](rw, &audit.RequestParams{ + Audit: auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionWrite, + OrganizationID: org.ID, + }) + defer commitAudit() + + if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceIdpsyncSettings.InOrg(org.ID)) { + httpapi.Forbidden(rw) + return + } + + var req codersdk.PatchGroupIDPSyncConfigRequest + if !httpapi.Read(ctx, rw, r, &req) { + return + } + + var settings idpsync.GroupSyncSettings + //nolint:gocritic // Requires system context to update runtime config + sysCtx := dbauthz.AsSystemRestricted(ctx) + err := database.ReadModifyUpdate(api.Database, func(tx database.Store) error { + existing, err := api.IDPSync.GroupSyncSettings(sysCtx, org.ID, tx) + if err != nil { + return err + } + aReq.Old = *existing + + settings = idpsync.GroupSyncSettings{ + Field: req.Field, + RegexFilter: req.RegexFilter, + AutoCreateMissing: req.AutoCreateMissing, + LegacyNameMapping: existing.LegacyNameMapping, + Mapping: existing.Mapping, + } + + err = api.IDPSync.UpdateGroupSyncSettings(sysCtx, org.ID, tx, settings) + if err != nil { + return err + } + + return nil + }) + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + + aReq.New = settings + httpapi.Write(ctx, rw, http.StatusOK, codersdk.GroupSyncSettings{ + Field: settings.Field, + RegexFilter: settings.RegexFilter, + AutoCreateMissing: settings.AutoCreateMissing, + LegacyNameMapping: settings.LegacyNameMapping, + Mapping: settings.Mapping, + }) +} + +// @Summary Update group IdP Sync mapping +// @ID update-group-idp-sync-mapping +// @Security CoderSessionToken +// @Produce json +// @Accept json +// @Tags Enterprise +// @Success 200 {object} codersdk.GroupSyncSettings +// @Param organization path string true "Organization ID or name" format(uuid) +// @Param request body codersdk.PatchGroupIDPSyncMappingRequest true "Description of the mappings to add and remove" +// @Router /organizations/{organization}/settings/idpsync/groups/mapping [patch] +func (api *API) patchGroupIDPSyncMapping(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + org := httpmw.OrganizationParam(r) + auditor := *api.AGPL.Auditor.Load() + aReq, commitAudit := audit.InitRequest[idpsync.GroupSyncSettings](rw, &audit.RequestParams{ + Audit: auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionWrite, + OrganizationID: org.ID, + }) + defer commitAudit() + + if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceIdpsyncSettings.InOrg(org.ID)) { + httpapi.Forbidden(rw) + return + } + + var req codersdk.PatchGroupIDPSyncMappingRequest + if !httpapi.Read(ctx, rw, r, &req) { + return + } + + var settings idpsync.GroupSyncSettings + //nolint:gocritic // Requires system context to update runtime config + sysCtx := dbauthz.AsSystemRestricted(ctx) + err := database.ReadModifyUpdate(api.Database, func(tx database.Store) error { + existing, err := api.IDPSync.GroupSyncSettings(sysCtx, org.ID, tx) + if err != nil { + return err + } + aReq.Old = *existing + + newMapping := applyIDPSyncMappingDiff(existing.Mapping, req.Add, req.Remove) + settings = idpsync.GroupSyncSettings{ + Field: existing.Field, + RegexFilter: existing.RegexFilter, + AutoCreateMissing: existing.AutoCreateMissing, + LegacyNameMapping: existing.LegacyNameMapping, + Mapping: newMapping, + } + + err = api.IDPSync.UpdateGroupSyncSettings(sysCtx, org.ID, tx, settings) + if err != nil { + return err + } + + return nil + }) + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + + aReq.New = settings + httpapi.Write(ctx, rw, http.StatusOK, codersdk.GroupSyncSettings{ + Field: settings.Field, + RegexFilter: settings.RegexFilter, + AutoCreateMissing: settings.AutoCreateMissing, + LegacyNameMapping: settings.LegacyNameMapping, + Mapping: settings.Mapping, + }) +} + // @Summary Get role IdP Sync settings by organization // @ID get-role-idp-sync-settings-by-organization // @Security CoderSessionToken @@ -201,7 +349,7 @@ func (api *API) patchRoleIDPSyncSettings(rw http.ResponseWriter, r *http.Request } aReq.Old = *existing - err = api.IDPSync.UpdateRoleSettings(sysCtx, org.ID, api.Database, idpsync.RoleSyncSettings{ + err = api.IDPSync.UpdateRoleSyncSettings(sysCtx, org.ID, api.Database, idpsync.RoleSyncSettings{ Field: req.Field, Mapping: req.Mapping, }) @@ -223,6 +371,141 @@ func (api *API) patchRoleIDPSyncSettings(rw http.ResponseWriter, r *http.Request }) } +// @Summary Update role IdP Sync config +// @ID update-role-idp-sync-config +// @Security CoderSessionToken +// @Produce json +// @Accept json +// @Tags Enterprise +// @Success 200 {object} codersdk.RoleSyncSettings +// @Param organization path string true "Organization ID or name" format(uuid) +// @Param request body codersdk.PatchRoleIDPSyncConfigRequest true "New config values" +// @Router /organizations/{organization}/settings/idpsync/roles/config [patch] +func (api *API) patchRoleIDPSyncConfig(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + org := httpmw.OrganizationParam(r) + auditor := *api.AGPL.Auditor.Load() + aReq, commitAudit := audit.InitRequest[idpsync.RoleSyncSettings](rw, &audit.RequestParams{ + Audit: auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionWrite, + OrganizationID: org.ID, + }) + defer commitAudit() + + if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceIdpsyncSettings.InOrg(org.ID)) { + httpapi.Forbidden(rw) + return + } + + var req codersdk.PatchRoleIDPSyncConfigRequest + if !httpapi.Read(ctx, rw, r, &req) { + return + } + + var settings idpsync.RoleSyncSettings + //nolint:gocritic // Requires system context to update runtime config + sysCtx := dbauthz.AsSystemRestricted(ctx) + err := database.ReadModifyUpdate(api.Database, func(tx database.Store) error { + existing, err := api.IDPSync.RoleSyncSettings(sysCtx, org.ID, tx) + if err != nil { + return err + } + aReq.Old = *existing + + settings = idpsync.RoleSyncSettings{ + Field: req.Field, + Mapping: existing.Mapping, + } + + err = api.IDPSync.UpdateRoleSyncSettings(sysCtx, org.ID, tx, settings) + if err != nil { + return err + } + + return nil + }) + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + + aReq.New = settings + httpapi.Write(ctx, rw, http.StatusOK, codersdk.RoleSyncSettings{ + Field: settings.Field, + Mapping: settings.Mapping, + }) +} + +// @Summary Update role IdP Sync mapping +// @ID update-role-idp-sync-mapping +// @Security CoderSessionToken +// @Produce json +// @Accept json +// @Tags Enterprise +// @Success 200 {object} codersdk.RoleSyncSettings +// @Param organization path string true "Organization ID or name" format(uuid) +// @Param request body codersdk.PatchRoleIDPSyncMappingRequest true "Description of the mappings to add and remove" +// @Router /organizations/{organization}/settings/idpsync/roles/mapping [patch] +func (api *API) patchRoleIDPSyncMapping(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + org := httpmw.OrganizationParam(r) + auditor := *api.AGPL.Auditor.Load() + aReq, commitAudit := audit.InitRequest[idpsync.RoleSyncSettings](rw, &audit.RequestParams{ + Audit: auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionWrite, + OrganizationID: org.ID, + }) + defer commitAudit() + + if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceIdpsyncSettings.InOrg(org.ID)) { + httpapi.Forbidden(rw) + return + } + + var req codersdk.PatchRoleIDPSyncMappingRequest + if !httpapi.Read(ctx, rw, r, &req) { + return + } + + var settings idpsync.RoleSyncSettings + //nolint:gocritic // Requires system context to update runtime config + sysCtx := dbauthz.AsSystemRestricted(ctx) + err := database.ReadModifyUpdate(api.Database, func(tx database.Store) error { + existing, err := api.IDPSync.RoleSyncSettings(sysCtx, org.ID, tx) + if err != nil { + return err + } + aReq.Old = *existing + + newMapping := applyIDPSyncMappingDiff(existing.Mapping, req.Add, req.Remove) + settings = idpsync.RoleSyncSettings{ + Field: existing.Field, + Mapping: newMapping, + } + + err = api.IDPSync.UpdateRoleSyncSettings(sysCtx, org.ID, tx, settings) + if err != nil { + return err + } + + return nil + }) + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + + aReq.New = settings + httpapi.Write(ctx, rw, http.StatusOK, codersdk.RoleSyncSettings{ + Field: settings.Field, + Mapping: settings.Mapping, + }) +} + // @Summary Get organization IdP Sync settings // @ID get-organization-idp-sync-settings // @Security CoderSessionToken @@ -292,7 +575,7 @@ func (api *API) patchOrganizationIDPSyncSettings(rw http.ResponseWriter, r *http } aReq.Old = *existing - err = api.IDPSync.UpdateOrganizationSettings(sysCtx, api.Database, idpsync.OrganizationSyncSettings{ + err = api.IDPSync.UpdateOrganizationSyncSettings(sysCtx, api.Database, idpsync.OrganizationSyncSettings{ Field: req.Field, // We do not check if the mappings point to actual organizations. Mapping: req.Mapping, @@ -317,6 +600,139 @@ func (api *API) patchOrganizationIDPSyncSettings(rw http.ResponseWriter, r *http }) } +// @Summary Update organization IdP Sync config +// @ID update-organization-idp-sync-config +// @Security CoderSessionToken +// @Produce json +// @Accept json +// @Tags Enterprise +// @Success 200 {object} codersdk.OrganizationSyncSettings +// @Param request body codersdk.PatchOrganizationIDPSyncConfigRequest true "New config values" +// @Router /settings/idpsync/organization/config [patch] +func (api *API) patchOrganizationIDPSyncConfig(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + auditor := *api.AGPL.Auditor.Load() + aReq, commitAudit := audit.InitRequest[idpsync.OrganizationSyncSettings](rw, &audit.RequestParams{ + Audit: auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionWrite, + }) + defer commitAudit() + + if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceIdpsyncSettings) { + httpapi.Forbidden(rw) + return + } + + var req codersdk.PatchOrganizationIDPSyncConfigRequest + if !httpapi.Read(ctx, rw, r, &req) { + return + } + + var settings idpsync.OrganizationSyncSettings + //nolint:gocritic // Requires system context to update runtime config + sysCtx := dbauthz.AsSystemRestricted(ctx) + err := database.ReadModifyUpdate(api.Database, func(tx database.Store) error { + existing, err := api.IDPSync.OrganizationSyncSettings(sysCtx, tx) + if err != nil { + return err + } + aReq.Old = *existing + + settings = idpsync.OrganizationSyncSettings{ + Field: req.Field, + AssignDefault: req.AssignDefault, + Mapping: existing.Mapping, + } + + err = api.IDPSync.UpdateOrganizationSyncSettings(sysCtx, tx, settings) + if err != nil { + return err + } + + return nil + }) + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + + aReq.New = settings + httpapi.Write(ctx, rw, http.StatusOK, codersdk.OrganizationSyncSettings{ + Field: settings.Field, + Mapping: settings.Mapping, + AssignDefault: settings.AssignDefault, + }) +} + +// @Summary Update organization IdP Sync mapping +// @ID update-organization-idp-sync-mapping +// @Security CoderSessionToken +// @Produce json +// @Accept json +// @Tags Enterprise +// @Success 200 {object} codersdk.OrganizationSyncSettings +// @Param request body codersdk.PatchOrganizationIDPSyncMappingRequest true "Description of the mappings to add and remove" +// @Router /settings/idpsync/organization/mapping [patch] +func (api *API) patchOrganizationIDPSyncMapping(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + auditor := *api.AGPL.Auditor.Load() + aReq, commitAudit := audit.InitRequest[idpsync.OrganizationSyncSettings](rw, &audit.RequestParams{ + Audit: auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionWrite, + }) + defer commitAudit() + + if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceIdpsyncSettings) { + httpapi.Forbidden(rw) + return + } + + var req codersdk.PatchOrganizationIDPSyncMappingRequest + if !httpapi.Read(ctx, rw, r, &req) { + return + } + + var settings idpsync.OrganizationSyncSettings + //nolint:gocritic // Requires system context to update runtime config + sysCtx := dbauthz.AsSystemRestricted(ctx) + err := database.ReadModifyUpdate(api.Database, func(tx database.Store) error { + existing, err := api.IDPSync.OrganizationSyncSettings(sysCtx, tx) + if err != nil { + return err + } + aReq.Old = *existing + + newMapping := applyIDPSyncMappingDiff(existing.Mapping, req.Add, req.Remove) + settings = idpsync.OrganizationSyncSettings{ + Field: existing.Field, + Mapping: newMapping, + AssignDefault: existing.AssignDefault, + } + + err = api.IDPSync.UpdateOrganizationSyncSettings(sysCtx, tx, settings) + if err != nil { + return err + } + + return nil + }) + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + + aReq.New = settings + httpapi.Write(ctx, rw, http.StatusOK, codersdk.OrganizationSyncSettings{ + Field: settings.Field, + Mapping: settings.Mapping, + AssignDefault: settings.AssignDefault, + }) +} + // @Summary Get the available organization idp sync claim fields // @ID get-the-available-organization-idp-sync-claim-fields // @Security CoderSessionToken @@ -423,3 +839,31 @@ func (api *API) idpSyncClaimFieldValues(orgID uuid.UUID, rw http.ResponseWriter, httpapi.Write(ctx, rw, http.StatusOK, fieldValues) } + +func applyIDPSyncMappingDiff[IDType uuid.UUID | string]( + previous map[string][]IDType, + add, remove []codersdk.IDPSyncMapping[IDType], +) map[string][]IDType { + next := make(map[string][]IDType) + + // Copy existing mapping + for key, ids := range previous { + next[key] = append(next[key], ids...) + } + + // Add unique entries + for _, mapping := range add { + if !slice.Contains(next[mapping.Given], mapping.Gets) { + next[mapping.Given] = append(next[mapping.Given], mapping.Gets) + } + } + + // Remove entries + for _, mapping := range remove { + next[mapping.Given] = slices.DeleteFunc(next[mapping.Given], func(u IDType) bool { + return u == mapping.Gets + }) + } + + return next +} diff --git a/enterprise/coderd/idpsync_internal_test.go b/enterprise/coderd/idpsync_internal_test.go new file mode 100644 index 0000000000000..51db04e74b913 --- /dev/null +++ b/enterprise/coderd/idpsync_internal_test.go @@ -0,0 +1,117 @@ +package coderd + +import ( + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/codersdk" +) + +func TestApplyIDPSyncMappingDiff(t *testing.T) { + t.Parallel() + + t.Run("with UUIDs", func(t *testing.T) { + t.Parallel() + + id := []uuid.UUID{ + uuid.MustParse("00000000-b8bd-46bb-bb6c-6c2b2c0dd2ea"), + uuid.MustParse("01000000-fbe8-464c-9429-fe01a03f3644"), + uuid.MustParse("02000000-0926-407b-9998-39af62e3d0c5"), + uuid.MustParse("03000000-92f6-4bfd-bba6-0f54667b131c"), + } + + mapping := applyIDPSyncMappingDiff(map[string][]uuid.UUID{}, + []codersdk.IDPSyncMapping[uuid.UUID]{ + {Given: "wibble", Gets: id[0]}, + {Given: "wibble", Gets: id[1]}, + {Given: "wobble", Gets: id[0]}, + {Given: "wobble", Gets: id[1]}, + {Given: "wobble", Gets: id[2]}, + {Given: "wobble", Gets: id[3]}, + {Given: "wooble", Gets: id[0]}, + }, + // Remove takes priority over Add, so `3` should not actually be added. + []codersdk.IDPSyncMapping[uuid.UUID]{ + {Given: "wobble", Gets: id[3]}, + }, + ) + + expected := map[string][]uuid.UUID{ + "wibble": {id[0], id[1]}, + "wobble": {id[0], id[1], id[2]}, + "wooble": {id[0]}, + } + + require.Equal(t, expected, mapping) + + mapping = applyIDPSyncMappingDiff(mapping, + []codersdk.IDPSyncMapping[uuid.UUID]{ + {Given: "wibble", Gets: id[2]}, + {Given: "wobble", Gets: id[3]}, + {Given: "wooble", Gets: id[0]}, + }, + []codersdk.IDPSyncMapping[uuid.UUID]{ + {Given: "wibble", Gets: id[0]}, + {Given: "wobble", Gets: id[1]}, + }, + ) + + expected = map[string][]uuid.UUID{ + "wibble": {id[1], id[2]}, + "wobble": {id[0], id[2], id[3]}, + "wooble": {id[0]}, + } + + require.Equal(t, expected, mapping) + }) + + t.Run("with strings", func(t *testing.T) { + t.Parallel() + + mapping := applyIDPSyncMappingDiff(map[string][]string{}, + []codersdk.IDPSyncMapping[string]{ + {Given: "wibble", Gets: "group-00"}, + {Given: "wibble", Gets: "group-01"}, + {Given: "wobble", Gets: "group-00"}, + {Given: "wobble", Gets: "group-01"}, + {Given: "wobble", Gets: "group-02"}, + {Given: "wobble", Gets: "group-03"}, + {Given: "wooble", Gets: "group-00"}, + }, + // Remove takes priority over Add, so `3` should not actually be added. + []codersdk.IDPSyncMapping[string]{ + {Given: "wobble", Gets: "group-03"}, + }, + ) + + expected := map[string][]string{ + "wibble": {"group-00", "group-01"}, + "wobble": {"group-00", "group-01", "group-02"}, + "wooble": {"group-00"}, + } + + require.Equal(t, expected, mapping) + + mapping = applyIDPSyncMappingDiff(mapping, + []codersdk.IDPSyncMapping[string]{ + {Given: "wibble", Gets: "group-02"}, + {Given: "wobble", Gets: "group-03"}, + {Given: "wooble", Gets: "group-00"}, + }, + []codersdk.IDPSyncMapping[string]{ + {Given: "wibble", Gets: "group-00"}, + {Given: "wobble", Gets: "group-01"}, + }, + ) + + expected = map[string][]string{ + "wibble": {"group-01", "group-02"}, + "wobble": {"group-00", "group-02", "group-03"}, + "wooble": {"group-00"}, + } + + require.Equal(t, expected, mapping) + }) +} diff --git a/enterprise/coderd/idpsync_test.go b/enterprise/coderd/idpsync_test.go index 41a8db2dd0792..d34701c3f6936 100644 --- a/enterprise/coderd/idpsync_test.go +++ b/enterprise/coderd/idpsync_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/coder/coder/v2/coderd/coderdtest" @@ -19,7 +20,7 @@ import ( "github.com/coder/serpent" ) -func TestGetGroupSyncConfig(t *testing.T) { +func TestGetGroupSyncSettings(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { @@ -82,7 +83,7 @@ func TestGetGroupSyncConfig(t *testing.T) { }) } -func TestPostGroupSyncConfig(t *testing.T) { +func TestPatchGroupSyncSettings(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { @@ -140,7 +141,172 @@ func TestPostGroupSyncConfig(t *testing.T) { }) } -func TestGetRoleSyncConfig(t *testing.T) { +func TestPatchGroupSyncConfig(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + owner, user := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + orgID := user.OrganizationID + orgAdmin, _ := coderdtest.CreateAnotherUser(t, owner, orgID, rbac.ScopedRoleOrgAdmin(user.OrganizationID)) + + mapping := map[string][]uuid.UUID{"wibble": {uuid.New()}} + + ctx := testutil.Context(t, testutil.WaitShort) + _, err := orgAdmin.PatchGroupIDPSyncSettings(ctx, orgID.String(), codersdk.GroupSyncSettings{ + Field: "wibble", + RegexFilter: regexp.MustCompile("wib{2,}le"), + AutoCreateMissing: false, + Mapping: mapping, + }) + + require.NoError(t, err) + + fetchedSettings, err := orgAdmin.GroupIDPSyncSettings(ctx, orgID.String()) + require.NoError(t, err) + require.Equal(t, "wibble", fetchedSettings.Field) + require.Equal(t, "wib{2,}le", fetchedSettings.RegexFilter.String()) + require.Equal(t, false, fetchedSettings.AutoCreateMissing) + require.Equal(t, mapping, fetchedSettings.Mapping) + + ctx = testutil.Context(t, testutil.WaitShort) + settings, err := orgAdmin.PatchGroupIDPSyncConfig(ctx, orgID.String(), codersdk.PatchGroupIDPSyncConfigRequest{ + Field: "wobble", + RegexFilter: regexp.MustCompile("wob{2,}le"), + AutoCreateMissing: true, + }) + + require.NoError(t, err) + require.Equal(t, "wobble", settings.Field) + require.Equal(t, "wob{2,}le", settings.RegexFilter.String()) + require.Equal(t, true, settings.AutoCreateMissing) + require.Equal(t, mapping, settings.Mapping) + + fetchedSettings, err = orgAdmin.GroupIDPSyncSettings(ctx, orgID.String()) + require.NoError(t, err) + require.Equal(t, "wobble", fetchedSettings.Field) + require.Equal(t, "wob{2,}le", fetchedSettings.RegexFilter.String()) + require.Equal(t, true, fetchedSettings.AutoCreateMissing) + require.Equal(t, mapping, fetchedSettings.Mapping) + }) + + t.Run("NotAuthorized", func(t *testing.T) { + t.Parallel() + + owner, user := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + member, _ := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID) + + ctx := testutil.Context(t, testutil.WaitShort) + _, err := member.PatchGroupIDPSyncConfig(ctx, user.OrganizationID.String(), codersdk.PatchGroupIDPSyncConfigRequest{}) + var apiError *codersdk.Error + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusForbidden, apiError.StatusCode()) + }) +} + +func TestPatchGroupSyncMapping(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + owner, user := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + orgID := user.OrganizationID + orgAdmin, _ := coderdtest.CreateAnotherUser(t, owner, orgID, rbac.ScopedRoleOrgAdmin(user.OrganizationID)) + // These IDs are easier to visually diff if the test fails than truly random + // ones. + orgs := []uuid.UUID{ + uuid.MustParse("00000000-b8bd-46bb-bb6c-6c2b2c0dd2ea"), + uuid.MustParse("01000000-fbe8-464c-9429-fe01a03f3644"), + uuid.MustParse("02000000-0926-407b-9998-39af62e3d0c5"), + } + + ctx := testutil.Context(t, testutil.WaitShort) + _, err := orgAdmin.PatchGroupIDPSyncSettings(ctx, orgID.String(), codersdk.GroupSyncSettings{ + Field: "wibble", + RegexFilter: regexp.MustCompile("wib{2,}le"), + AutoCreateMissing: true, + Mapping: map[string][]uuid.UUID{"wobble": {orgs[0]}}, + }) + require.NoError(t, err) + + ctx = testutil.Context(t, testutil.WaitShort) + settings, err := orgAdmin.PatchGroupIDPSyncMapping(ctx, orgID.String(), codersdk.PatchGroupIDPSyncMappingRequest{ + Add: []codersdk.IDPSyncMapping[uuid.UUID]{ + {Given: "wibble", Gets: orgs[0]}, + {Given: "wobble", Gets: orgs[1]}, + {Given: "wobble", Gets: orgs[2]}, + }, + // Remove takes priority over Add, so "3" should not actually be added to wooble. + Remove: []codersdk.IDPSyncMapping[uuid.UUID]{ + {Given: "wobble", Gets: orgs[1]}, + }, + }) + + expected := map[string][]uuid.UUID{ + "wibble": {orgs[0]}, + "wobble": {orgs[0], orgs[2]}, + } + + require.NoError(t, err) + require.Equal(t, expected, settings.Mapping) + + fetchedSettings, err := orgAdmin.GroupIDPSyncSettings(ctx, orgID.String()) + require.NoError(t, err) + require.Equal(t, "wibble", fetchedSettings.Field) + require.Equal(t, "wib{2,}le", fetchedSettings.RegexFilter.String()) + require.Equal(t, true, fetchedSettings.AutoCreateMissing) + require.Equal(t, expected, fetchedSettings.Mapping) + }) + + t.Run("NotAuthorized", func(t *testing.T) { + t.Parallel() + + owner, user := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + member, _ := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID) + + ctx := testutil.Context(t, testutil.WaitShort) + _, err := member.PatchGroupIDPSyncMapping(ctx, user.OrganizationID.String(), codersdk.PatchGroupIDPSyncMappingRequest{}) + var apiError *codersdk.Error + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusForbidden, apiError.StatusCode()) + }) +} + +func TestGetRoleSyncSettings(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { @@ -174,7 +340,7 @@ func TestGetRoleSyncConfig(t *testing.T) { }) } -func TestPostRoleSyncConfig(t *testing.T) { +func TestPatchRoleSyncSettings(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { @@ -231,3 +397,381 @@ func TestPostRoleSyncConfig(t *testing.T) { require.Equal(t, http.StatusForbidden, apiError.StatusCode()) }) } + +func TestPatchRoleSyncConfig(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + owner, user := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + orgID := user.OrganizationID + orgAdmin, _ := coderdtest.CreateAnotherUser(t, owner, orgID, rbac.ScopedRoleOrgAdmin(user.OrganizationID)) + + mapping := map[string][]string{"wibble": {"group-01"}} + + ctx := testutil.Context(t, testutil.WaitShort) + _, err := orgAdmin.PatchRoleIDPSyncSettings(ctx, orgID.String(), codersdk.RoleSyncSettings{ + Field: "wibble", + Mapping: mapping, + }) + + require.NoError(t, err) + + fetchedSettings, err := orgAdmin.RoleIDPSyncSettings(ctx, orgID.String()) + require.NoError(t, err) + require.Equal(t, "wibble", fetchedSettings.Field) + require.Equal(t, mapping, fetchedSettings.Mapping) + + ctx = testutil.Context(t, testutil.WaitShort) + settings, err := orgAdmin.PatchRoleIDPSyncConfig(ctx, orgID.String(), codersdk.PatchRoleIDPSyncConfigRequest{ + Field: "wobble", + }) + + require.NoError(t, err) + require.Equal(t, "wobble", settings.Field) + require.Equal(t, mapping, settings.Mapping) + + fetchedSettings, err = orgAdmin.RoleIDPSyncSettings(ctx, orgID.String()) + require.NoError(t, err) + require.Equal(t, "wobble", fetchedSettings.Field) + require.Equal(t, mapping, fetchedSettings.Mapping) + }) + + t.Run("NotAuthorized", func(t *testing.T) { + t.Parallel() + + owner, user := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + member, _ := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID) + + ctx := testutil.Context(t, testutil.WaitShort) + _, err := member.PatchGroupIDPSyncConfig(ctx, user.OrganizationID.String(), codersdk.PatchGroupIDPSyncConfigRequest{}) + var apiError *codersdk.Error + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusForbidden, apiError.StatusCode()) + }) +} + +func TestPatchRoleSyncMapping(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + owner, user := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + orgID := user.OrganizationID + orgAdmin, _ := coderdtest.CreateAnotherUser(t, owner, orgID, rbac.ScopedRoleOrgAdmin(user.OrganizationID)) + + ctx := testutil.Context(t, testutil.WaitShort) + _, err := orgAdmin.PatchRoleIDPSyncSettings(ctx, orgID.String(), codersdk.RoleSyncSettings{ + Field: "wibble", + Mapping: map[string][]string{"wobble": {"group-00"}}, + }) + require.NoError(t, err) + + ctx = testutil.Context(t, testutil.WaitShort) + settings, err := orgAdmin.PatchRoleIDPSyncMapping(ctx, orgID.String(), codersdk.PatchRoleIDPSyncMappingRequest{ + Add: []codersdk.IDPSyncMapping[string]{ + {Given: "wibble", Gets: "group-00"}, + {Given: "wobble", Gets: "group-01"}, + {Given: "wobble", Gets: "group-02"}, + }, + // Remove takes priority over Add, so "3" should not actually be added to wooble. + Remove: []codersdk.IDPSyncMapping[string]{ + {Given: "wobble", Gets: "group-01"}, + }, + }) + + expected := map[string][]string{ + "wibble": {"group-00"}, + "wobble": {"group-00", "group-02"}, + } + + require.NoError(t, err) + require.Equal(t, expected, settings.Mapping) + + fetchedSettings, err := orgAdmin.RoleIDPSyncSettings(ctx, orgID.String()) + require.NoError(t, err) + require.Equal(t, "wibble", fetchedSettings.Field) + require.Equal(t, expected, fetchedSettings.Mapping) + }) + + t.Run("NotAuthorized", func(t *testing.T) { + t.Parallel() + + owner, user := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + member, _ := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID) + + ctx := testutil.Context(t, testutil.WaitShort) + _, err := member.PatchGroupIDPSyncMapping(ctx, user.OrganizationID.String(), codersdk.PatchGroupIDPSyncMappingRequest{}) + var apiError *codersdk.Error + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusForbidden, apiError.StatusCode()) + }) +} + +func TestGetOrganizationSyncSettings(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + owner, _, _, user := coderdenttest.NewWithAPI(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + expected := map[string][]uuid.UUID{"foo": {user.OrganizationID}} + + ctx := testutil.Context(t, testutil.WaitShort) + settings, err := owner.PatchOrganizationIDPSyncSettings(ctx, codersdk.OrganizationSyncSettings{ + Field: "august", + Mapping: expected, + }) + + require.NoError(t, err) + require.Equal(t, "august", settings.Field) + require.Equal(t, expected, settings.Mapping) + + settings, err = owner.OrganizationIDPSyncSettings(ctx) + require.NoError(t, err) + require.Equal(t, "august", settings.Field) + require.Equal(t, expected, settings.Mapping) + }) +} + +func TestPatchOrganizationSyncSettings(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + owner, _ := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + ctx := testutil.Context(t, testutil.WaitShort) + //nolint:gocritic // Only owners can change Organization IdP sync settings + settings, err := owner.PatchOrganizationIDPSyncSettings(ctx, codersdk.OrganizationSyncSettings{ + Field: "august", + }) + require.NoError(t, err) + require.Equal(t, "august", settings.Field) + + fetchedSettings, err := owner.OrganizationIDPSyncSettings(ctx) + require.NoError(t, err) + require.Equal(t, "august", fetchedSettings.Field) + }) + + t.Run("NotAuthorized", func(t *testing.T) { + t.Parallel() + + owner, user := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + member, _ := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID) + + ctx := testutil.Context(t, testutil.WaitShort) + _, err := member.PatchRoleIDPSyncSettings(ctx, user.OrganizationID.String(), codersdk.RoleSyncSettings{ + Field: "august", + }) + var apiError *codersdk.Error + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusForbidden, apiError.StatusCode()) + + _, err = member.RoleIDPSyncSettings(ctx, user.OrganizationID.String()) + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusForbidden, apiError.StatusCode()) + }) +} + +func TestPatchOrganizationSyncConfig(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + owner, user := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + mapping := map[string][]uuid.UUID{"wibble": {user.OrganizationID}} + + ctx := testutil.Context(t, testutil.WaitShort) + //nolint:gocritic // Only owners can change Organization IdP sync settings + _, err := owner.PatchOrganizationIDPSyncSettings(ctx, codersdk.OrganizationSyncSettings{ + Field: "wibble", + AssignDefault: true, + Mapping: mapping, + }) + + require.NoError(t, err) + + fetchedSettings, err := owner.OrganizationIDPSyncSettings(ctx) + require.NoError(t, err) + require.Equal(t, "wibble", fetchedSettings.Field) + require.Equal(t, true, fetchedSettings.AssignDefault) + require.Equal(t, mapping, fetchedSettings.Mapping) + + ctx = testutil.Context(t, testutil.WaitShort) + settings, err := owner.PatchOrganizationIDPSyncConfig(ctx, codersdk.PatchOrganizationIDPSyncConfigRequest{ + Field: "wobble", + }) + + require.NoError(t, err) + require.Equal(t, "wobble", settings.Field) + require.Equal(t, false, settings.AssignDefault) + require.Equal(t, mapping, settings.Mapping) + + fetchedSettings, err = owner.OrganizationIDPSyncSettings(ctx) + require.NoError(t, err) + require.Equal(t, "wobble", fetchedSettings.Field) + require.Equal(t, false, fetchedSettings.AssignDefault) + require.Equal(t, mapping, fetchedSettings.Mapping) + }) + + t.Run("NotAuthorized", func(t *testing.T) { + t.Parallel() + + owner, user := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + member, _ := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID) + + ctx := testutil.Context(t, testutil.WaitShort) + _, err := member.PatchOrganizationIDPSyncConfig(ctx, codersdk.PatchOrganizationIDPSyncConfigRequest{}) + var apiError *codersdk.Error + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusForbidden, apiError.StatusCode()) + }) +} + +func TestPatchOrganizationSyncMapping(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + owner, _ := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + // These IDs are easier to visually diff if the test fails than truly random + // ones. + orgs := []uuid.UUID{ + uuid.MustParse("00000000-b8bd-46bb-bb6c-6c2b2c0dd2ea"), + uuid.MustParse("01000000-fbe8-464c-9429-fe01a03f3644"), + uuid.MustParse("02000000-0926-407b-9998-39af62e3d0c5"), + } + + ctx := testutil.Context(t, testutil.WaitShort) + //nolint:gocritic // Only owners can change Organization IdP sync settings + settings, err := owner.PatchOrganizationIDPSyncMapping(ctx, codersdk.PatchOrganizationIDPSyncMappingRequest{ + Add: []codersdk.IDPSyncMapping[uuid.UUID]{ + {Given: "wibble", Gets: orgs[0]}, + {Given: "wobble", Gets: orgs[0]}, + {Given: "wobble", Gets: orgs[1]}, + {Given: "wobble", Gets: orgs[2]}, + }, + Remove: []codersdk.IDPSyncMapping[uuid.UUID]{ + {Given: "wobble", Gets: orgs[1]}, + }, + }) + + expected := map[string][]uuid.UUID{ + "wibble": {orgs[0]}, + "wobble": {orgs[0], orgs[2]}, + } + + require.NoError(t, err) + require.Equal(t, expected, settings.Mapping) + + fetchedSettings, err := owner.OrganizationIDPSyncSettings(ctx) + require.NoError(t, err) + require.Equal(t, expected, fetchedSettings.Mapping) + }) + + t.Run("NotAuthorized", func(t *testing.T) { + t.Parallel() + + owner, user := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + member, _ := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID) + + ctx := testutil.Context(t, testutil.WaitShort) + _, err := member.PatchOrganizationIDPSyncMapping(ctx, codersdk.PatchOrganizationIDPSyncMappingRequest{}) + var apiError *codersdk.Error + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusForbidden, apiError.StatusCode()) + }) +} diff --git a/enterprise/coderd/organizations.go b/enterprise/coderd/organizations.go index a7ec4050ee654..6cf91ec5b856a 100644 --- a/enterprise/coderd/organizations.go +++ b/enterprise/coderd/organizations.go @@ -150,7 +150,16 @@ func (api *API) deleteOrganization(rw http.ResponseWriter, r *http.Request) { return } - err := api.Database.DeleteOrganization(ctx, organization.ID) + err := api.Database.InTx(func(tx database.Store) error { + err := tx.UpdateOrganizationDeletedByID(ctx, database.UpdateOrganizationDeletedByIDParams{ + ID: organization.ID, + UpdatedAt: dbtime.Now(), + }) + if err != nil { + return xerrors.Errorf("delete organization: %w", err) + } + return nil + }, nil) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error deleting organization.", @@ -204,7 +213,10 @@ func (api *API) postOrganizations(rw http.ResponseWriter, r *http.Request) { return } - _, err := api.Database.GetOrganizationByName(ctx, req.Name) + _, err := api.Database.GetOrganizationByName(ctx, database.GetOrganizationByNameParams{ + Name: req.Name, + Deleted: false, + }) if err == nil { httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{ Message: "Organization already exists with that name.", diff --git a/enterprise/coderd/provisionerdaemons_test.go b/enterprise/coderd/provisionerdaemons_test.go index ff3ce625707ee..0cd812b45c5f1 100644 --- a/enterprise/coderd/provisionerdaemons_test.go +++ b/enterprise/coderd/provisionerdaemons_test.go @@ -782,10 +782,14 @@ func TestGetProvisionerDaemons(t *testing.T) { pkDaemons, err := orgAdmin.ListProvisionerKeyDaemons(ctx, org.ID) require.NoError(t, err) - require.Len(t, pkDaemons, 1) + require.Len(t, pkDaemons, 2) require.Len(t, pkDaemons[0].Daemons, 1) assert.Equal(t, keys[0].ID, pkDaemons[0].Key.ID) assert.Equal(t, keys[0].Name, pkDaemons[0].Key.Name) + // user-auth provisioners + require.Len(t, pkDaemons[1].Daemons, 0) + assert.Equal(t, codersdk.ProvisionerKeyUUIDUserAuth, pkDaemons[1].Key.ID) + assert.Equal(t, codersdk.ProvisionerKeyNameUserAuth, pkDaemons[1].Key.Name) assert.Equal(t, daemonName, pkDaemons[0].Daemons[0].Name) assert.Equal(t, buildinfo.Version(), pkDaemons[0].Daemons[0].Version) @@ -949,7 +953,7 @@ func TestGetProvisionerDaemons(t *testing.T) { org := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{ IncludeProvisionerDaemon: false, }) - orgAdmin, _ := coderdtest.CreateAnotherUser(t, client, org.ID, rbac.ScopedRoleOrgMember(org.ID)) + orgTemplateAdmin, _ := coderdtest.CreateAnotherUser(t, client, org.ID, rbac.ScopedRoleOrgTemplateAdmin(org.ID)) daemonCreatedAt := time.Now() @@ -982,11 +986,13 @@ func TestGetProvisionerDaemons(t *testing.T) { require.NoError(t, err, "should be able to create provisioner daemon") daemonAsCreated := db2sdk.ProvisionerDaemon(pd) - allDaemons, err := orgAdmin.OrganizationProvisionerDaemons(ctx, org.ID, nil) + allDaemons, err := orgTemplateAdmin.OrganizationProvisionerDaemons(ctx, org.ID, nil) require.NoError(t, err) require.Len(t, allDaemons, 1) - daemonsAsFound, err := orgAdmin.OrganizationProvisionerDaemons(ctx, org.ID, tt.tagsToFilterBy) + daemonsAsFound, err := orgTemplateAdmin.OrganizationProvisionerDaemons(ctx, org.ID, &codersdk.OrganizationProvisionerDaemonsOptions{ + Tags: tt.tagsToFilterBy, + }) if tt.expectToGetDaemon { require.NoError(t, err) require.Len(t, daemonsAsFound, 1) diff --git a/enterprise/coderd/provisionerkeys.go b/enterprise/coderd/provisionerkeys.go index 279b9c567e353..d615819ec3510 100644 --- a/enterprise/coderd/provisionerkeys.go +++ b/enterprise/coderd/provisionerkeys.go @@ -137,6 +137,20 @@ func (api *API) provisionerKeyDaemons(rw http.ResponseWriter, r *http.Request) { } sdkKeys := convertProvisionerKeys(pks) + // For the default organization, we insert three rows for the special + // provisioner key types (built-in, user-auth, and psk). We _don't_ insert + // those into the database for any other org, but we still need to include the + // user-auth key in this list, so we just insert it manually. + if !slices.ContainsFunc(sdkKeys, func(key codersdk.ProvisionerKey) bool { + return key.ID == codersdk.ProvisionerKeyUUIDUserAuth + }) { + sdkKeys = append(sdkKeys, codersdk.ProvisionerKey{ + ID: codersdk.ProvisionerKeyUUIDUserAuth, + Name: codersdk.ProvisionerKeyNameUserAuth, + Tags: map[string]string{}, + }) + } + daemons, err := api.Database.GetProvisionerDaemonsByOrganization(ctx, database.GetProvisionerDaemonsByOrganizationParams{OrganizationID: organization.ID}) if err != nil { httpapi.InternalServerError(rw, err) diff --git a/enterprise/coderd/roles.go b/enterprise/coderd/roles.go index 227be3e4ce39e..30432af76c7eb 100644 --- a/enterprise/coderd/roles.go +++ b/enterprise/coderd/roles.go @@ -127,8 +127,7 @@ func (api *API) putOrgRoles(rw http.ResponseWriter, r *http.Request) { }, }, ExcludeOrgRoles: false, - // Linter requires all fields to be set. This field is not actually required. - OrganizationID: organization.ID, + OrganizationID: organization.ID, }) // If it is a 404 (not found) error, ignore it. if err != nil && !httpapi.Is404Error(err) { @@ -147,9 +146,13 @@ func (api *API) putOrgRoles(rw http.ResponseWriter, r *http.Request) { UUID: organization.ID, Valid: true, }, - SitePermissions: db2sdk.List(req.SitePermissions, sdkPermissionToDB), - OrgPermissions: db2sdk.List(req.OrganizationPermissions, sdkPermissionToDB), - UserPermissions: db2sdk.List(req.UserPermissions, sdkPermissionToDB), + // Invalid permissions are filtered out. If this is changed + // to throw an error, then the story of a previously valid role + // now being invalid has to be addressed. Coder can change permissions, + // objects, and actions at any time. + SitePermissions: db2sdk.List(filterInvalidPermissions(req.SitePermissions), sdkPermissionToDB), + OrgPermissions: db2sdk.List(filterInvalidPermissions(req.OrganizationPermissions), sdkPermissionToDB), + UserPermissions: db2sdk.List(filterInvalidPermissions(req.UserPermissions), sdkPermissionToDB), }) if httpapi.Is404Error(err) { httpapi.ResourceNotFound(rw) @@ -247,6 +250,23 @@ func (api *API) deleteOrgRole(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusNoContent, nil) } +func filterInvalidPermissions(permissions []codersdk.Permission) []codersdk.Permission { + // Filter out any invalid permissions + var validPermissions []codersdk.Permission + for _, permission := range permissions { + err := rbac.Permission{ + Negate: permission.Negate, + ResourceType: string(permission.ResourceType), + Action: policy.Action(permission.Action), + }.Valid() + if err != nil { + continue + } + validPermissions = append(validPermissions, permission) + } + return validPermissions +} + func sdkPermissionToDB(p codersdk.Permission) database.CustomRolePermission { return database.CustomRolePermission{ Negate: p.Negate, diff --git a/enterprise/coderd/roles_test.go b/enterprise/coderd/roles_test.go index 8bbf9218058e7..57b66a368248c 100644 --- a/enterprise/coderd/roles_test.go +++ b/enterprise/coderd/roles_test.go @@ -441,10 +441,11 @@ func TestListRoles(t *testing.T) { return member.ListOrganizationRoles(ctx, owner.OrganizationID) }, ExpectedRoles: convertRoles(map[rbac.RoleIdentifier]bool{ - {Name: codersdk.RoleOrganizationAdmin, OrganizationID: owner.OrganizationID}: false, - {Name: codersdk.RoleOrganizationAuditor, OrganizationID: owner.OrganizationID}: false, - {Name: codersdk.RoleOrganizationTemplateAdmin, OrganizationID: owner.OrganizationID}: false, - {Name: codersdk.RoleOrganizationUserAdmin, OrganizationID: owner.OrganizationID}: false, + {Name: codersdk.RoleOrganizationAdmin, OrganizationID: owner.OrganizationID}: false, + {Name: codersdk.RoleOrganizationAuditor, OrganizationID: owner.OrganizationID}: false, + {Name: codersdk.RoleOrganizationTemplateAdmin, OrganizationID: owner.OrganizationID}: false, + {Name: codersdk.RoleOrganizationUserAdmin, OrganizationID: owner.OrganizationID}: false, + {Name: codersdk.RoleOrganizationWorkspaceCreationBan, OrganizationID: owner.OrganizationID}: false, }), }, { @@ -473,10 +474,11 @@ func TestListRoles(t *testing.T) { return orgAdmin.ListOrganizationRoles(ctx, owner.OrganizationID) }, ExpectedRoles: convertRoles(map[rbac.RoleIdentifier]bool{ - {Name: codersdk.RoleOrganizationAdmin, OrganizationID: owner.OrganizationID}: true, - {Name: codersdk.RoleOrganizationAuditor, OrganizationID: owner.OrganizationID}: true, - {Name: codersdk.RoleOrganizationTemplateAdmin, OrganizationID: owner.OrganizationID}: true, - {Name: codersdk.RoleOrganizationUserAdmin, OrganizationID: owner.OrganizationID}: true, + {Name: codersdk.RoleOrganizationAdmin, OrganizationID: owner.OrganizationID}: true, + {Name: codersdk.RoleOrganizationAuditor, OrganizationID: owner.OrganizationID}: true, + {Name: codersdk.RoleOrganizationTemplateAdmin, OrganizationID: owner.OrganizationID}: true, + {Name: codersdk.RoleOrganizationUserAdmin, OrganizationID: owner.OrganizationID}: true, + {Name: codersdk.RoleOrganizationWorkspaceCreationBan, OrganizationID: owner.OrganizationID}: true, }), }, { @@ -505,10 +507,11 @@ func TestListRoles(t *testing.T) { return client.ListOrganizationRoles(ctx, owner.OrganizationID) }, ExpectedRoles: convertRoles(map[rbac.RoleIdentifier]bool{ - {Name: codersdk.RoleOrganizationAdmin, OrganizationID: owner.OrganizationID}: true, - {Name: codersdk.RoleOrganizationAuditor, OrganizationID: owner.OrganizationID}: true, - {Name: codersdk.RoleOrganizationTemplateAdmin, OrganizationID: owner.OrganizationID}: true, - {Name: codersdk.RoleOrganizationUserAdmin, OrganizationID: owner.OrganizationID}: true, + {Name: codersdk.RoleOrganizationAdmin, OrganizationID: owner.OrganizationID}: true, + {Name: codersdk.RoleOrganizationAuditor, OrganizationID: owner.OrganizationID}: true, + {Name: codersdk.RoleOrganizationTemplateAdmin, OrganizationID: owner.OrganizationID}: true, + {Name: codersdk.RoleOrganizationUserAdmin, OrganizationID: owner.OrganizationID}: true, + {Name: codersdk.RoleOrganizationWorkspaceCreationBan, OrganizationID: owner.OrganizationID}: true, }), }, } diff --git a/enterprise/coderd/scim_test.go b/enterprise/coderd/scim_test.go index a8d5c67ed4c0d..5396180b4a0d0 100644 --- a/enterprise/coderd/scim_test.go +++ b/enterprise/coderd/scim_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/golang-jwt/jwt/v4" + "github.com/google/uuid" "github.com/imulab/go-scim/pkg/v2/handlerutil" "github.com/imulab/go-scim/pkg/v2/spec" "github.com/stretchr/testify/assert" @@ -568,6 +569,7 @@ func TestScim(t *testing.T) { //nolint:bodyclose scimUserClient, _ := fake.Login(t, client, jwt.MapClaims{ "email": sUser.Emails[0].Value, + "sub": uuid.NewString(), }) scimUser, err = scimUserClient.User(ctx, codersdk.Me) require.NoError(t, err) @@ -836,6 +838,7 @@ func TestScim(t *testing.T) { //nolint:bodyclose scimUserClient, _ := fake.Login(t, client, jwt.MapClaims{ "email": sUser.Emails[0].Value, + "sub": uuid.NewString(), }) scimUser, err = scimUserClient.User(ctx, codersdk.Me) require.NoError(t, err) diff --git a/enterprise/coderd/templates_test.go b/enterprise/coderd/templates_test.go index 22314f45bb3c7..a40ed7b64a6db 100644 --- a/enterprise/coderd/templates_test.go +++ b/enterprise/coderd/templates_test.go @@ -161,11 +161,11 @@ func TestTemplates(t *testing.T) { Name: "some", Type: "example", Agents: []*proto.Agent{{ - Id: "something", + Id: "something", + Name: "test", Auth: &proto.Agent_Token{ Token: uuid.NewString(), }, - Name: "test", }}, }, { Name: "another", @@ -2018,7 +2018,7 @@ func TestMultipleOrganizationTemplates(t *testing.T) { t.Logf("Second organization: %s", second.ID.String()) t.Logf("Third organization: %s", third.ID.String()) - t.Logf("Creating template version in second organization") + t.Log("Creating template version in second organization") start := time.Now() version := coderdtest.CreateTemplateVersion(t, templateAdmin, second.ID, nil) diff --git a/enterprise/coderd/userauth_test.go b/enterprise/coderd/userauth_test.go index d3e997608f316..267e1168f84cf 100644 --- a/enterprise/coderd/userauth_test.go +++ b/enterprise/coderd/userauth_test.go @@ -50,6 +50,7 @@ func TestUserOIDC(t *testing.T) { claims := jwt.MapClaims{ "email": "alice@coder.com", + "sub": uuid.NewString(), } // Login a new client that signs up @@ -82,6 +83,7 @@ func TestUserOIDC(t *testing.T) { claims := jwt.MapClaims{ "email": "alice@coder.com", + "sub": uuid.NewString(), } // Login a new client that signs up @@ -152,9 +154,11 @@ func TestUserOIDC(t *testing.T) { require.NoError(t, err) require.Equal(t, expectedSettings.Field, settings.Field) + sub := uuid.NewString() claims := jwt.MapClaims{ "email": "alice@coder.com", "organization": []string{"first", "second"}, + "sub": sub, } // Then: a new user logs in with claims "second" and "third", they @@ -169,7 +173,7 @@ func TestUserOIDC(t *testing.T) { fields, err := runner.AdminClient.GetAvailableIDPSyncFields(ctx) require.NoError(t, err) require.ElementsMatch(t, []string{ - "aud", "exp", "iss", // Always included from jwt + "sub", "aud", "exp", "iss", // Always included from jwt "email", "organization", }, fields) @@ -204,6 +208,7 @@ func TestUserOIDC(t *testing.T) { runner.Login(t, jwt.MapClaims{ "email": "alice@coder.com", "organization": []string{"second"}, + "sub": sub, }) runner.AssertOrganizations(t, "alice", true, []uuid.UUID{orgTwo.ID}) }) @@ -238,10 +243,12 @@ func TestUserOIDC(t *testing.T) { }) fourth := dbgen.Organization(t, runner.API.Database, database.Organization{}) + sub := uuid.NewString() ctx := testutil.Context(t, testutil.WaitMedium) claims := jwt.MapClaims{ "email": "alice@coder.com", "organization": []string{"second", "third"}, + "sub": sub, } // Then: a new user logs in with claims "second" and "third", they @@ -265,6 +272,7 @@ func TestUserOIDC(t *testing.T) { runner.Login(t, jwt.MapClaims{ "email": "alice@coder.com", "organization": []string{"third"}, + "sub": sub, }) runner.AssertOrganizations(t, "alice", false, []uuid.UUID{third}) }) @@ -289,6 +297,7 @@ func TestUserOIDC(t *testing.T) { claims := jwt.MapClaims{ "email": "alice@coder.com", + "sub": uuid.NewString(), } // Login a new client that signs up client, resp := runner.Login(t, claims) @@ -328,6 +337,7 @@ func TestUserOIDC(t *testing.T) { // This is sent as a **string** intentionally instead // of an array. "roles": oidcRoleName, + "sub": uuid.NewString(), }) require.Equal(t, http.StatusOK, resp.StatusCode) runner.AssertRoles(t, "alice", []string{rbac.RoleTemplateAdmin().String()}) @@ -398,9 +408,11 @@ func TestUserOIDC(t *testing.T) { }) // User starts with the owner role + sub := uuid.NewString() _, resp := runner.Login(t, jwt.MapClaims{ "email": "alice@coder.com", "roles": []string{"random", oidcRoleName, rbac.RoleOwner().String()}, + "sub": sub, }) require.Equal(t, http.StatusOK, resp.StatusCode) runner.AssertRoles(t, "alice", []string{rbac.RoleTemplateAdmin().String(), rbac.RoleUserAdmin().String(), rbac.RoleOwner().String()}) @@ -409,6 +421,7 @@ func TestUserOIDC(t *testing.T) { _, resp = runner.Login(t, jwt.MapClaims{ "email": "alice@coder.com", "roles": []string{"random"}, + "sub": sub, }) require.Equal(t, http.StatusOK, resp.StatusCode) @@ -429,9 +442,11 @@ func TestUserOIDC(t *testing.T) { }, }) + sub := uuid.NewString() _, resp := runner.Login(t, jwt.MapClaims{ "email": "alice@coder.com", "roles": []string{}, + "sub": sub, }) require.Equal(t, http.StatusOK, resp.StatusCode) // Try to manually update user roles, even though controlled by oidc @@ -476,6 +491,7 @@ func TestUserOIDC(t *testing.T) { _, resp := runner.Login(t, jwt.MapClaims{ "email": "alice@coder.com", groupClaim: []string{groupName}, + "sub": uuid.New(), }) require.Equal(t, http.StatusOK, resp.StatusCode) runner.AssertGroups(t, "alice", []string{groupName}) @@ -510,6 +526,7 @@ func TestUserOIDC(t *testing.T) { _, resp := runner.Login(t, jwt.MapClaims{ "email": "alice@coder.com", groupClaim: []string{oidcGroupName}, + "sub": uuid.New(), }) require.Equal(t, http.StatusOK, resp.StatusCode) runner.AssertGroups(t, "alice", []string{coderGroupName}) @@ -546,6 +563,7 @@ func TestUserOIDC(t *testing.T) { client, resp := runner.Login(t, jwt.MapClaims{ "email": "alice@coder.com", groupClaim: []string{groupName}, + "sub": uuid.New(), }) require.Equal(t, http.StatusOK, resp.StatusCode) runner.AssertGroups(t, "alice", []string{groupName}) @@ -579,9 +597,11 @@ func TestUserOIDC(t *testing.T) { require.NoError(t, err) require.Len(t, group.Members, 0) + sub := uuid.NewString() _, resp := runner.Login(t, jwt.MapClaims{ "email": "alice@coder.com", groupClaim: []string{groupName}, + "sub": sub, }) require.Equal(t, http.StatusOK, resp.StatusCode) runner.AssertGroups(t, "alice", []string{groupName}) @@ -589,6 +609,7 @@ func TestUserOIDC(t *testing.T) { // Refresh without the group claim _, resp = runner.Login(t, jwt.MapClaims{ "email": "alice@coder.com", + "sub": sub, }) require.Equal(t, http.StatusOK, resp.StatusCode) runner.AssertGroups(t, "alice", []string{}) @@ -612,6 +633,7 @@ func TestUserOIDC(t *testing.T) { _, resp := runner.Login(t, jwt.MapClaims{ "email": "alice@coder.com", groupClaim: []string{"not-exists"}, + "sub": uuid.New(), }) require.Equal(t, http.StatusOK, resp.StatusCode) runner.AssertGroups(t, "alice", []string{}) @@ -637,6 +659,7 @@ func TestUserOIDC(t *testing.T) { _, resp := runner.Login(t, jwt.MapClaims{ "email": "alice@coder.com", groupClaim: []string{groupName}, + "sub": uuid.New(), }) require.Equal(t, http.StatusOK, resp.StatusCode) runner.AssertGroups(t, "alice", []string{groupName}) @@ -665,6 +688,7 @@ func TestUserOIDC(t *testing.T) { // This is sent as a **string** intentionally instead // of an array. groupClaim: groupName, + "sub": uuid.New(), }) require.Equal(t, http.StatusOK, resp.StatusCode) runner.AssertGroups(t, "alice", []string{groupName}) @@ -686,9 +710,11 @@ func TestUserOIDC(t *testing.T) { }) // Test forbidden + sub := uuid.NewString() _, resp := runner.AttemptLogin(t, jwt.MapClaims{ "email": "alice@coder.com", groupClaim: []string{"not-allowed"}, + "sub": sub, }) require.Equal(t, http.StatusForbidden, resp.StatusCode) @@ -696,6 +722,7 @@ func TestUserOIDC(t *testing.T) { client, _ := runner.Login(t, jwt.MapClaims{ "email": "alice@coder.com", groupClaim: []string{allowedGroup}, + "sub": sub, }) ctx := testutil.Context(t, testutil.WaitShort) @@ -719,6 +746,7 @@ func TestUserOIDC(t *testing.T) { claims := jwt.MapClaims{ "email": "alice@coder.com", + "sub": uuid.NewString(), } // Login a new client that signs up client, resp := runner.Login(t, claims) @@ -747,6 +775,7 @@ func TestUserOIDC(t *testing.T) { claims := jwt.MapClaims{ "email": "alice@coder.com", + "sub": uuid.NewString(), } // Login a new client that signs up client, resp := runner.Login(t, claims) @@ -921,6 +950,7 @@ func TestGroupSync(t *testing.T) { require.NoError(t, err, "user must be oidc type") // Log in the new user + tc.claims["sub"] = uuid.NewString() tc.claims["email"] = user.Email _, resp := runner.Login(t, tc.claims) require.Equal(t, http.StatusOK, resp.StatusCode) diff --git a/enterprise/tailnet/pgcoord_test.go b/enterprise/tailnet/pgcoord_test.go index e17f1c61e28a2..b8f2c4718357c 100644 --- a/enterprise/tailnet/pgcoord_test.go +++ b/enterprise/tailnet/pgcoord_test.go @@ -462,33 +462,33 @@ func TestPGCoordinatorDual_Mainline(t *testing.T) { defer client22.Close(ctx) t.Logf("client22=%s", client22.ID) - t.Logf("client11 -> Node 11") + t.Log("client11 -> Node 11") client11.UpdateDERP(11) agent1.AssertEventuallyHasDERP(client11.ID, 11) - t.Logf("client21 -> Node 21") + t.Log("client21 -> Node 21") client21.UpdateDERP(21) agent1.AssertEventuallyHasDERP(client21.ID, 21) - t.Logf("client22 -> Node 22") + t.Log("client22 -> Node 22") client22.UpdateDERP(22) agent2.AssertEventuallyHasDERP(client22.ID, 22) - t.Logf("agent2 -> Node 2") + t.Log("agent2 -> Node 2") agent2.UpdateDERP(2) client22.AssertEventuallyHasDERP(agent2.ID, 2) client12.AssertEventuallyHasDERP(agent2.ID, 2) - t.Logf("client12 -> Node 12") + t.Log("client12 -> Node 12") client12.UpdateDERP(12) agent2.AssertEventuallyHasDERP(client12.ID, 12) - t.Logf("agent1 -> Node 1") + t.Log("agent1 -> Node 1") agent1.UpdateDERP(1) client21.AssertEventuallyHasDERP(agent1.ID, 1) client11.AssertEventuallyHasDERP(agent1.ID, 1) - t.Logf("close coord2") + t.Log("close coord2") err = coord2.Close() require.NoError(t, err) diff --git a/examples/templates/kubernetes/main.tf b/examples/templates/kubernetes/main.tf index 0ba6ba33b7aad..e1fdb12cbefda 100644 --- a/examples/templates/kubernetes/main.tf +++ b/examples/templates/kubernetes/main.tf @@ -278,8 +278,9 @@ resource "kubernetes_deployment" "main" { } spec { security_context { - run_as_user = 1000 - fs_group = 1000 + run_as_user = 1000 + fs_group = 1000 + run_as_non_root = true } container { diff --git a/flake.nix b/flake.nix index f4654ccb378b1..e5ce3d4a790af 100644 --- a/flake.nix +++ b/flake.nix @@ -71,70 +71,97 @@ vendorHash = null; }; + # Packages required to build the frontend + frontendPackages = + with pkgs; + [ + cairo + pango + pixman + libpng + libjpeg + giflib + librsvg + python312Packages.setuptools # Needed for node-gyp + ] + ++ (lib.optionals stdenv.targetPlatform.isDarwin [ + darwin.apple_sdk.frameworks.Foundation + xcbuild + ]); + # The minimal set of packages to build Coder. - devShellPackages = with pkgs; [ - # google-chrome is not available on aarch64 linux - (lib.optionalDrvAttr (!stdenv.isLinux || !stdenv.isAarch64) google-chrome) - # strace is not available on OSX - (lib.optionalDrvAttr (!pkgs.stdenv.isDarwin) strace) - bat - cairo - curl - delve - dive - drpc.defaultPackage.${system} - formatter - fzf - gcc - gdk - getopt - gh - git - (lib.optionalDrvAttr stdenv.isLinux glibcLocales) - gnumake - gnused - go_1_22 - go-migrate - (pinnedPkgs.golangci-lint) - gopls - gotestsum - jq - kubectl - kubectx - kubernetes-helm - lazygit - less - mockgen - moreutils - neovim - nfpm - nix-prefetch-git - nodejs - openssh - openssl - pango - pixman - pkg-config - playwright-driver.browsers - pnpm - postgresql_16 - proto_gen_go_1_30 - protobuf_23 - ripgrep - shellcheck - (pinnedPkgs.shfmt) - sqlc - terraform - typos - # Needed for many LD system libs! - (lib.optional stdenv.isLinux util-linux) - vim - wget - yq-go - zip - zsh - zstd - ]; + devShellPackages = + with pkgs; + [ + # google-chrome is not available on aarch64 linux + (lib.optionalDrvAttr (!stdenv.isLinux || !stdenv.isAarch64) google-chrome) + # strace is not available on OSX + (lib.optionalDrvAttr (!pkgs.stdenv.isDarwin) strace) + bat + cairo + curl + delve + dive + drpc.defaultPackage.${system} + formatter + fzf + gawk + gcc13 + gdk + getopt + gh + git + (lib.optionalDrvAttr stdenv.isLinux glibcLocales) + gnumake + gnused + gnugrep + gnutar + go_1_22 + go-migrate + (pinnedPkgs.golangci-lint) + gopls + gotestsum + jq + kubectl + kubectx + kubernetes-helm + lazygit + less + mockgen + moreutils + neovim + nfpm + nix-prefetch-git + nodejs + openssh + openssl + pango + pixman + pkg-config + playwright-driver.browsers + pnpm + postgresql_16 + proto_gen_go_1_30 + protobuf_23 + ripgrep + shellcheck + (pinnedPkgs.shfmt) + sqlc + terraform + typos + which + # Needed for many LD system libs! + (lib.optional stdenv.isLinux util-linux) + vim + wget + yq-go + zip + zsh + zstd + ] + ++ frontendPackages; + + docker = pkgs.callPackage ./nix/docker.nix { }; # buildSite packages the site directory. buildSite = pnpm2nix.packages.${system}.mkPnpmPackage { @@ -142,22 +169,7 @@ src = ./site/.; # Required for the `canvas` package! - extraBuildInputs = - with pkgs; - [ - cairo - pango - pixman - libpng - libjpeg - giflib - librsvg - python312Packages.setuptools - ] - ++ (lib.optionals stdenv.targetPlatform.isDarwin [ - darwin.apple_sdk.frameworks.Foundation - xcbuild - ]); + extraBuildInputs = frontendPackages; installInPlace = true; distDir = "out"; }; @@ -172,7 +184,7 @@ name = "coder-${osArch}"; # Updated with ./scripts/update-flake.sh`. # This should be updated whenever go.mod changes! - vendorHash = "sha256-hJBNmHz9ZJLS/QTu8w8y1w/Yi45aSoaSeZ//ysllp6c="; + vendorHash = "sha256-QjqF+QZ5JKMnqkpNh6ZjrJU2QcSqiT4Dip1KoicwLYc="; proxyVendor = true; src = ./.; nativeBuildInputs = with pkgs; [ @@ -210,14 +222,16 @@ devShells = { default = pkgs.mkShell { buildInputs = devShellPackages; - shellHook = '' - export PLAYWRIGHT_BROWSERS_PATH=${pkgs.playwright-driver.browsers} - export PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS=true - ''; + + PLAYWRIGHT_BROWSERS_PATH = pkgs.playwright-driver.browsers; + PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS = true; LOCALE_ARCHIVE = with pkgs; lib.optionalDrvAttr stdenv.isLinux "${glibcLocales}/lib/locale/locale-archive"; + + NODE_OPTIONS = "--max-old-space-size=8192"; + GOPRIVATE = "coder.com,cdr.dev,go.coder.com,github.com/cdr,github.com/coder"; }; }; @@ -237,12 +251,39 @@ aarch64-windows = buildFat "windows_arm64.exe"; } // (pkgs.lib.optionalAttrs pkgs.stdenv.isLinux { - dev_image = pkgs.dockerTools.buildNixShellImage { + dev_image = docker.buildNixShellImage rec { name = "codercom/oss-dogfood-nix"; tag = "latest-${system}"; + # (ThomasK33): Workaround for images with too many layers (>64 layers) causing sysbox + # to have issues on dogfood envs. + maxLayers = 32; + + uname = "coder"; + homeDirectory = "/home/${uname}"; + drv = devShells.default.overrideAttrs (oldAttrs: { - buildInputs = oldAttrs.buildInputs ++ [ pkgs.nix ]; + buildInputs = + (with pkgs; [ + coreutils + nix + curl.bin # Ensure the actual curl binary is included in the PATH + glibc.bin # Ensure the glibc binaries are included in the PATH + jq.bin + binutils # ld and strings + filebrowser # Ensure that we're not redownloading filebrowser on each launch + systemd.out + service-wrapper + docker_26 + shadow.out + su + ncurses.out # clear + unzip + zip + gzip + procps # free + ]) + ++ oldAttrs.buildInputs; }); }; }); diff --git a/go.mod b/go.mod index a1f8b6b7a7ac8..4b38c65265f4d 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/coder/coder/v2 -go 1.22.8 +go 1.22.9 // Required until a v3 of chroma is created to lazily initialize all XML files. // None of our dependencies seem to use the registries anyways, so this @@ -36,7 +36,7 @@ replace github.com/tcnksm/go-httpstat => github.com/coder/go-httpstat v0.0.0-202 // There are a few minor changes we make to Tailscale that we're slowly upstreaming. Compare here: // https://github.com/tailscale/tailscale/compare/main...coder:tailscale:main -replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20250129014916-8086c871eae6 +replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20250227024825-c9983534152a // This is replaced to include // 1. a fix for a data race: c.f. https://github.com/tailscale/wireguard-go/pull/25 @@ -71,9 +71,10 @@ require ( github.com/adrg/xdg v0.5.0 github.com/ammario/tlru v0.4.0 github.com/andybalholm/brotli v1.1.1 + github.com/aquasecurity/trivy-iac v0.8.0 github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 github.com/awalterschulze/gographviz v2.0.3+incompatible - github.com/aws/smithy-go v1.22.1 + github.com/aws/smithy-go v1.22.2 github.com/bgentry/speakeasy v0.2.0 github.com/bramvdbogaerde/go-scp v1.5.0 github.com/briandowns/spinner v1.18.1 @@ -88,12 +89,12 @@ require ( github.com/chromedp/chromedp v0.11.0 github.com/cli/safeexec v1.0.1 github.com/coder/flog v1.1.0 - github.com/coder/guts v1.0.0 + github.com/coder/guts v1.0.1 github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 github.com/coder/quartz v0.1.2 github.com/coder/retry v1.5.1 github.com/coder/serpent v0.10.0 - github.com/coder/terraform-provider-coder v1.0.4 + github.com/coder/terraform-provider-coder/v2 v2.1.3 github.com/coder/websocket v1.8.12 github.com/coder/wgtunnel v0.1.13-0.20240522110300-ade90dfb2da0 github.com/coreos/go-oidc/v3 v3.12.0 @@ -101,6 +102,7 @@ require ( github.com/creack/pty v1.1.21 github.com/dave/dst v0.27.2 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc + github.com/dblohm7/wingoes v0.0.0-20240820181039-f2b84150679e github.com/elastic/go-sysinfo v1.15.0 github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 github.com/emersion/go-smtp v0.21.2 @@ -115,22 +117,22 @@ require ( github.com/go-chi/cors v1.2.1 github.com/go-chi/httprate v0.14.1 github.com/go-chi/render v1.0.1 - github.com/go-jose/go-jose/v4 v4.0.2 + github.com/go-jose/go-jose/v4 v4.0.5 github.com/go-logr/logr v1.4.2 - github.com/go-playground/validator/v10 v10.24.0 + github.com/go-playground/validator/v10 v10.25.0 github.com/gofrs/flock v0.12.0 - github.com/gohugoio/hugo v0.141.0 + github.com/gohugoio/hugo v0.143.0 github.com/golang-jwt/jwt/v4 v4.5.1 github.com/golang-migrate/migrate/v4 v4.18.1 github.com/gomarkdown/markdown v0.0.0-20240930133441-72d49d9543d8 - github.com/google/go-cmp v0.6.0 + github.com/google/go-cmp v0.7.0 github.com/google/go-github/v43 v43.0.1-0.20220414155304-00e42332e405 github.com/google/go-github/v61 v61.0.0 github.com/google/uuid v1.6.0 github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-reap v0.0.0-20170704170343-bf58d8a43e7b github.com/hashicorp/go-version v1.7.0 - github.com/hashicorp/hc-install v0.9.0 + github.com/hashicorp/hc-install v0.9.1 github.com/hashicorp/terraform-config-inspect v0.0.0-20211115214459-90acf1ca460f github.com/hashicorp/terraform-json v0.24.0 github.com/hashicorp/yamux v0.1.2 @@ -141,23 +143,23 @@ require ( github.com/justinas/nosurf v1.1.1 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f - github.com/klauspost/compress v1.17.11 + github.com/klauspost/compress v1.18.0 github.com/lib/pq v1.10.9 github.com/mattn/go-isatty v0.0.20 github.com/mitchellh/go-wordwrap v1.0.1 github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c - github.com/moby/moby v27.5.0+incompatible + github.com/moby/moby v28.0.0+incompatible github.com/mocktools/go-smtp-mock/v2 v2.4.0 - github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a + github.com/muesli/termenv v0.16.0 github.com/natefinch/atomic v1.0.1 - github.com/open-policy-agent/opa v1.0.0 + github.com/open-policy-agent/opa v1.1.0 github.com/ory/dockertest/v3 v3.11.0 github.com/pion/udp v0.1.4 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e github.com/pkg/sftp v1.13.7 - github.com/prometheus-community/pro-bing v0.5.0 - github.com/prometheus/client_golang v1.20.5 + github.com/prometheus-community/pro-bing v0.6.0 + github.com/prometheus/client_golang v1.21.0 github.com/prometheus/client_model v0.6.1 github.com/prometheus/common v0.62.0 github.com/quasilyte/go-ruleguard/dsl v0.3.21 @@ -172,34 +174,35 @@ require ( github.com/tidwall/gjson v1.18.0 github.com/u-root/u-root v0.14.0 github.com/unrolled/secure v1.17.0 - github.com/valyala/fasthttp v1.58.0 + github.com/valyala/fasthttp v1.59.0 github.com/wagslane/go-password-validator v0.3.0 + github.com/zclconf/go-cty-yaml v1.1.0 go.mozilla.org/pkcs7 v0.9.0 go.nhat.io/otelsql v0.15.0 - go.opentelemetry.io/otel v1.33.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 - go.opentelemetry.io/otel/sdk v1.33.0 - go.opentelemetry.io/otel/trace v1.33.0 + go.opentelemetry.io/otel v1.34.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 + go.opentelemetry.io/otel/sdk v1.34.0 + go.opentelemetry.io/otel/trace v1.34.0 go.uber.org/atomic v1.11.0 go.uber.org/goleak v1.3.1-0.20240429205332-517bace7cc29 go.uber.org/mock v0.5.0 go4.org/netipx v0.0.0-20230728180743-ad4cb58a6516 - golang.org/x/crypto v0.32.0 + golang.org/x/crypto v0.33.0 golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa - golang.org/x/mod v0.22.0 - golang.org/x/net v0.34.0 - golang.org/x/oauth2 v0.25.0 - golang.org/x/sync v0.10.0 - golang.org/x/sys v0.29.1-0.20250107080300-1c14dcadc3ab - golang.org/x/term v0.28.0 - golang.org/x/text v0.21.0 // indirect - golang.org/x/tools v0.29.0 + golang.org/x/mod v0.23.0 + golang.org/x/net v0.35.0 + golang.org/x/oauth2 v0.26.0 + golang.org/x/sync v0.11.0 + golang.org/x/sys v0.30.0 + golang.org/x/term v0.29.0 + golang.org/x/text v0.22.0 // indirect + golang.org/x/tools v0.30.0 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da - google.golang.org/api v0.217.0 - google.golang.org/grpc v1.69.4 - google.golang.org/protobuf v1.36.2 - gopkg.in/DataDog/dd-trace-go.v1 v1.70.3 + google.golang.org/api v0.221.0 + google.golang.org/grpc v1.70.0 + google.golang.org/protobuf v1.36.5 + gopkg.in/DataDog/dd-trace-go.v1 v1.72.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v3 v3.0.1 gvisor.dev/gvisor v0.0.0-20240509041132-65b30f7869dc @@ -209,7 +212,7 @@ require ( ) require ( - cloud.google.com/go/auth v0.14.0 // indirect + cloud.google.com/go/auth v0.14.1 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect cloud.google.com/go/logging v1.12.0 // indirect cloud.google.com/go/longrunning v0.6.2 // indirect @@ -218,27 +221,35 @@ require ( github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/DataDog/appsec-internal-go v1.9.0 // indirect github.com/DataDog/datadog-agent/pkg/obfuscate v0.58.0 // indirect + github.com/DataDog/datadog-agent/pkg/proto v0.58.0 // indirect github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.58.0 // indirect + github.com/DataDog/datadog-agent/pkg/trace v0.58.0 // indirect + github.com/DataDog/datadog-agent/pkg/util/log v0.58.0 // indirect + github.com/DataDog/datadog-agent/pkg/util/scrubber v0.58.0 // indirect github.com/DataDog/datadog-go/v5 v5.5.0 // indirect github.com/DataDog/go-libddwaf/v3 v3.5.1 // indirect + github.com/DataDog/go-runtime-metrics-internal v0.0.4-0.20241206090539-a14610dc22b6 // indirect + github.com/DataDog/go-sqllexer v0.0.14 // indirect github.com/DataDog/go-tuf v1.1.0-0.5.2 // indirect github.com/DataDog/gostackparse v0.7.0 // indirect + github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes v0.20.0 // indirect github.com/DataDog/sketches-go v1.4.5 // indirect github.com/KyleBanks/depth v1.2.1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/OneOfOne/xxhash v1.2.8 // indirect - github.com/ProtonMail/go-crypto v1.1.0-alpha.2 // indirect + github.com/ProtonMail/go-crypto v1.1.3 // indirect github.com/agext/levenshtein v1.2.3 // indirect github.com/agnivade/levenshtein v1.2.0 // indirect github.com/akutz/memconn v0.1.0 // indirect github.com/alecthomas/chroma/v2 v2.15.0 // indirect github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect + github.com/apparentlymart/go-cidr v1.1.0 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c // indirect github.com/atotto/clipboard v0.1.4 // indirect - github.com/aws/aws-sdk-go-v2 v1.33.0 + github.com/aws/aws-sdk-go-v2 v1.36.0 github.com/aws/aws-sdk-go-v2/config v1.29.1 github.com/aws/aws-sdk-go-v2/credentials v1.17.54 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24 // indirect @@ -257,12 +268,14 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bep/godartsass/v2 v2.3.2 // indirect github.com/bep/golibsass v1.2.0 // indirect + github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect github.com/charmbracelet/x/ansi v0.4.5 // indirect github.com/charmbracelet/x/term v0.2.0 // indirect github.com/chromedp/sysutil v1.0.0 // indirect + github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 // indirect github.com/clbanning/mxj/v2 v2.7.0 // indirect github.com/cloudflare/circl v1.3.7 // indirect - github.com/containerd/continuity v0.4.3 // indirect + github.com/containerd/continuity v0.4.4 // indirect github.com/coreos/go-iptables v0.6.0 // indirect github.com/dlclark/regexp2 v1.11.4 // indirect github.com/docker/cli v27.1.1+incompatible // indirect @@ -289,7 +302,7 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect - github.com/go-test/deep v1.0.8 // indirect + github.com/go-test/deep v1.1.0 // indirect github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 // indirect github.com/go-viper/mapstructure/v2 v2.0.0 // indirect github.com/gobwas/glob v0.2.3 // indirect @@ -298,10 +311,10 @@ require ( github.com/gobwas/ws v1.4.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/gohugoio/hashstructure v0.3.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.2 // indirect - github.com/google/flatbuffers v23.1.21+incompatible // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/nftables v0.2.0 // indirect github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b // indirect @@ -311,7 +324,7 @@ require ( github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/mux v1.8.1 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect @@ -325,9 +338,9 @@ require ( github.com/hashicorp/hcl v1.0.1-vault-5 // indirect github.com/hashicorp/hcl/v2 v2.23.0 github.com/hashicorp/logutils v1.0.0 // indirect - github.com/hashicorp/terraform-plugin-go v0.25.0 // indirect + github.com/hashicorp/terraform-plugin-go v0.26.0 // indirect github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect - github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.0 // indirect + github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.0 // indirect github.com/hdevalence/ed25519consensus v0.1.0 // indirect github.com/illarion/gonotify v1.0.1 // indirect github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 // indirect @@ -335,11 +348,13 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 // indirect github.com/jsimonetti/rtnetlink v1.3.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect github.com/kr/fs v0.1.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-localereader v0.0.1 // indirect @@ -357,6 +372,8 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/term v0.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect @@ -375,6 +392,7 @@ require ( github.com/pion/transport/v3 v3.0.7 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect github.com/riandyrn/otelchi v0.5.1 // indirect @@ -383,6 +401,8 @@ require ( github.com/ryanuber/go-glob v1.0.0 // indirect github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b // indirect github.com/secure-systems-lab/go-securesystemslib v0.7.0 // indirect + github.com/shirou/gopsutil/v3 v3.24.4 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spf13/cast v1.7.1 // indirect @@ -392,14 +412,17 @@ require ( github.com/tailscale/golang-x-crypto v0.0.0-20230713185742-f0b76a10a08e // indirect github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 + github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect github.com/tailscale/wireguard-go v0.0.0-20231121184858-cc193a0b3272 - github.com/tchap/go-patricia/v2 v2.3.1 // indirect + github.com/tchap/go-patricia/v2 v2.3.2 // indirect github.com/tcnksm/go-httpstat v0.2.0 // indirect github.com/tdewolff/parse/v2 v2.7.15 // indirect github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tinylib/msgp v1.2.1 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a // indirect github.com/vishvananda/netlink v1.2.1-beta.2 // indirect github.com/vishvananda/netns v0.0.4 // indirect @@ -414,61 +437,33 @@ require ( github.com/yashtewari/glob-intersection v0.2.0 // indirect github.com/yuin/goldmark v1.7.8 // indirect github.com/yuin/goldmark-emoji v1.0.4 // indirect - github.com/zclconf/go-cty v1.16.0 + github.com/yusufpapurcu/wmi v1.2.4 // indirect + github.com/zclconf/go-cty v1.16.2 github.com/zeebo/errs v1.3.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/collector/component v0.104.0 // indirect + go.opentelemetry.io/collector/config/configtelemetry v0.104.0 // indirect + go.opentelemetry.io/collector/pdata v1.11.0 // indirect + go.opentelemetry.io/collector/pdata/pprofile v0.104.0 // indirect + go.opentelemetry.io/collector/semconv v0.104.0 // indirect go.opentelemetry.io/contrib v1.19.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect - go.opentelemetry.io/otel/metric v1.33.0 // indirect - go.opentelemetry.io/proto/otlp v1.4.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/proto/otlp v1.5.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect - golang.org/x/time v0.9.0 // indirect - golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect + golang.org/x/time v0.10.0 // indirect + golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 // indirect golang.zx2c4.com/wireguard/windows v0.5.3 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect howett.net/plist v1.0.0 // indirect kernel.org/pub/linux/libs/security/libcap/psx v1.2.73 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) - -require ( - github.com/aquasecurity/trivy-iac v0.8.0 - github.com/zclconf/go-cty-yaml v1.1.0 -) - -require ( - github.com/DataDog/datadog-agent/pkg/proto v0.58.0 // indirect - github.com/DataDog/datadog-agent/pkg/trace v0.58.0 // indirect - github.com/DataDog/datadog-agent/pkg/util/log v0.58.0 // indirect - github.com/DataDog/datadog-agent/pkg/util/scrubber v0.58.0 // indirect - github.com/DataDog/go-runtime-metrics-internal v0.0.0-20241106155157-194426bbbd59 // indirect - github.com/DataDog/go-sqllexer v0.0.14 // indirect - github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes v0.20.0 // indirect - github.com/apparentlymart/go-cidr v1.1.0 // indirect - github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect - github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect - github.com/shirou/gopsutil/v3 v3.24.4 // indirect - github.com/shoenig/go-m1cpu v0.1.6 // indirect - github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect - github.com/tklauser/go-sysconf v0.3.12 // indirect - github.com/tklauser/numcpus v0.6.1 // indirect - github.com/yusufpapurcu/wmi v1.2.4 // indirect - go.opentelemetry.io/collector/component v0.104.0 // indirect - go.opentelemetry.io/collector/config/configtelemetry v0.104.0 // indirect - go.opentelemetry.io/collector/pdata v1.11.0 // indirect - go.opentelemetry.io/collector/pdata/pprofile v0.104.0 // indirect - go.opentelemetry.io/collector/semconv v0.104.0 // indirect - go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect -) diff --git a/go.sum b/go.sum index 1737a0ea1e0b1..6496dfc84118d 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ cdr.dev/slog v1.6.2-0.20241112041820-0ec81e6e67bb h1:4MKA8lBQLnCqj2myJCb5Lzoa65y0tABO4gHrxuMdsCQ= cdr.dev/slog v1.6.2-0.20241112041820-0ec81e6e67bb/go.mod h1:NaoTA7KwopCrnaSb0JXTC0PTp/O/Y83Lndnq0OEV3ZQ= -cloud.google.com/go/auth v0.14.0 h1:A5C4dKV/Spdvxcl0ggWwWEzzP7AZMJSEIgrkngwhGYM= -cloud.google.com/go/auth v0.14.0/go.mod h1:CYsoRL1PdiDuqeQpZE0bP2pnPrGqFcOkI0nldEQis+A= +cloud.google.com/go/auth v0.14.1 h1:AwoJbzUdxA/whv1qj3TLKwh3XX5sikny2fc40wUl+h0= +cloud.google.com/go/auth v0.14.1/go.mod h1:4JHUxlGXisL0AW8kXPtUF6ztuOksyfUQNFjfsOCXkPM= cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= @@ -40,8 +40,8 @@ github.com/DataDog/datadog-go/v5 v5.5.0 h1:G5KHeB8pWBNXT4Jtw0zAkhdxEAWSpWH00geHI github.com/DataDog/datadog-go/v5 v5.5.0/go.mod h1:K9kcYBlxkcPP8tvvjZZKs/m1edNAUFzBbdpTUKfCsuw= github.com/DataDog/go-libddwaf/v3 v3.5.1 h1:GWA4ln4DlLxiXm+X7HA/oj0ZLcdCwOS81KQitegRTyY= github.com/DataDog/go-libddwaf/v3 v3.5.1/go.mod h1:n98d9nZ1gzenRSk53wz8l6d34ikxS+hs62A31Fqmyi4= -github.com/DataDog/go-runtime-metrics-internal v0.0.0-20241106155157-194426bbbd59 h1:s4hgS6gqbXIakEMMujYiHCVVsB3R3oZtqEzPBMnFU2w= -github.com/DataDog/go-runtime-metrics-internal v0.0.0-20241106155157-194426bbbd59/go.mod h1:quaQJ+wPN41xEC458FCpTwyROZm3MzmTZ8q8XOXQiPs= +github.com/DataDog/go-runtime-metrics-internal v0.0.4-0.20241206090539-a14610dc22b6 h1:bpitH5JbjBhfcTG+H2RkkiUXpYa8xSuIPnyNtTaSPog= +github.com/DataDog/go-runtime-metrics-internal v0.0.4-0.20241206090539-a14610dc22b6/go.mod h1:quaQJ+wPN41xEC458FCpTwyROZm3MzmTZ8q8XOXQiPs= github.com/DataDog/go-sqllexer v0.0.14 h1:xUQh2tLr/95LGxDzLmttLgTo/1gzFeOyuwrQa/Iig4Q= github.com/DataDog/go-sqllexer v0.0.14/go.mod h1:KwkYhpFEVIq+BfobkTC1vfqm4gTi65skV/DpDBXtexc= github.com/DataDog/go-tuf v1.1.0-0.5.2 h1:4CagiIekonLSfL8GMHRHcHudo1fQnxELS9g4tiAupQ4= @@ -64,8 +64,8 @@ github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEV github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= -github.com/ProtonMail/go-crypto v1.1.0-alpha.2 h1:bkyFVUP+ROOARdgCiJzNQo2V2kiB97LyUpzH9P6Hrlg= -github.com/ProtonMail/go-crypto v1.1.0-alpha.2/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk= +github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/adrg/xdg v0.5.0 h1:dDaZvhMXatArP1NPHhnfaQUqWBLBsmx1h1HXQdMoFCY= @@ -106,8 +106,8 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/awalterschulze/gographviz v2.0.3+incompatible h1:9sVEXJBJLwGX7EQVhLm2elIKCm7P2YHFC8v6096G09E= github.com/awalterschulze/gographviz v2.0.3+incompatible/go.mod h1:GEV5wmg4YquNw7v1kkyoX9etIk8yVmXj+AkDHuuETHs= -github.com/aws/aws-sdk-go-v2 v1.33.0 h1:Evgm4DI9imD81V0WwD+TN4DCwjUMdc94TrduMLbgZJs= -github.com/aws/aws-sdk-go-v2 v1.33.0/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= +github.com/aws/aws-sdk-go-v2 v1.36.0 h1:b1wM5CcE65Ujwn565qcwgtOTT1aT4ADOHHgglKjG7fk= +github.com/aws/aws-sdk-go-v2 v1.36.0/go.mod h1:5PMILGVKiW32oDzjj6RU52yrNrDPUHcbZQYr1sM7qmM= github.com/aws/aws-sdk-go-v2/config v1.29.1 h1:JZhGawAyZ/EuJeBtbQYnaoftczcb2drR2Iq36Wgz4sQ= github.com/aws/aws-sdk-go-v2/config v1.29.1/go.mod h1:7bR2YD5euaxBhzt2y/oDkt3uNRb6tjFp98GlTFueRwk= github.com/aws/aws-sdk-go-v2/credentials v1.17.54 h1:4UmqeOqJPvdvASZWrKlhzpRahAulBfyTJQUaYy4+hEI= @@ -134,8 +134,8 @@ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.10 h1:l+dgv/64iVlQ3WsBbnn+JSb github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.10/go.mod h1:Fzsj6lZEb8AkTE5S68OhcbBqeWPsR8RnGuKPr8Todl8= github.com/aws/aws-sdk-go-v2/service/sts v1.33.9 h1:BRVDbewN6VZcwr+FBOszDKvYeXY1kJ+GGMCcpghlw0U= github.com/aws/aws-sdk-go-v2/service/sts v1.33.9/go.mod h1:f6vjfZER1M17Fokn0IzssOTMT2N8ZSq+7jnNF0tArvw= -github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= -github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= +github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= @@ -183,8 +183,6 @@ github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5 h1:BjkPE3785EwP github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5/go.mod h1:jtAfVaU/2cu1+wdSRPWE2c1N2qeAA3K4RH9pYgqwets= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -220,14 +218,12 @@ github.com/coder/bubbletea v1.2.2-0.20241212190825-007a1cdb2c41 h1:SBN/DA63+ZHwu github.com/coder/bubbletea v1.2.2-0.20241212190825-007a1cdb2c41/go.mod h1:I9ULxr64UaOSUv7hcb3nX4kowodJCVS7vt7VVJk/kW4= github.com/coder/flog v1.1.0 h1:kbAes1ai8fIS5OeV+QAnKBQE22ty1jRF/mcAwHpLBa4= github.com/coder/flog v1.1.0/go.mod h1:UQlQvrkJBvnRGo69Le8E24Tcl5SJleAAR7gYEHzAmdQ= -github.com/coder/glog v1.0.1-0.20220322161911-7365fe7f2cd1 h1:UqBrPWSYvRI2s5RtOul20JukUEpu4ip9u7biBL+ntgk= -github.com/coder/glog v1.0.1-0.20220322161911-7365fe7f2cd1/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/coder/go-httpstat v0.0.0-20230801153223-321c88088322 h1:m0lPZjlQ7vdVpRBPKfYIFlmgevoTkBxB10wv6l2gOaU= github.com/coder/go-httpstat v0.0.0-20230801153223-321c88088322/go.mod h1:rOLFDDVKVFiDqZFXoteXc97YXx7kFi9kYqR+2ETPkLQ= github.com/coder/go-scim/pkg/v2 v2.0.0-20230221055123-1d63c1222136 h1:0RgB61LcNs24WOxc3PBvygSNTQurm0PYPujJjLLOzs0= github.com/coder/go-scim/pkg/v2 v2.0.0-20230221055123-1d63c1222136/go.mod h1:VkD1P761nykiq75dz+4iFqIQIZka189tx1BQLOp0Skc= -github.com/coder/guts v1.0.0 h1:Ba6TBOeED+96Dv8IdISjbGhCzHKicqSc4SEYVV+4zeE= -github.com/coder/guts v1.0.0/go.mod h1:SfmxjDaSfPjzKJ9mGU4sA/1OHU+u66uRfhFF+y4BARQ= +github.com/coder/guts v1.0.1 h1:tU9pW+1jftCSX1eBxnNHiouQBSBJIej3I+kqfjIyeJU= +github.com/coder/guts v1.0.1/go.mod h1:z8LHbF6vwDOXQOReDvay7Rpwp/jHwCZiZwjd6wfLcJg= github.com/coder/pq v1.10.5-0.20240813183442-0c420cb5a048 h1:3jzYUlGH7ZELIH4XggXhnTnP05FCYiAFeQpoN+gNR5I= github.com/coder/pq v1.10.5-0.20240813183442-0c420cb5a048/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs= @@ -240,20 +236,20 @@ github.com/coder/serpent v0.10.0 h1:ofVk9FJXSek+SmL3yVE3GoArP83M+1tX+H7S4t8BSuM= github.com/coder/serpent v0.10.0/go.mod h1:cZFW6/fP+kE9nd/oRkEHJpG6sXCtQ+AX7WMMEHv0Y3Q= github.com/coder/ssh v0.0.0-20231128192721-70855dedb788 h1:YoUSJ19E8AtuUFVYBpXuOD6a/zVP3rcxezNsoDseTUw= github.com/coder/ssh v0.0.0-20231128192721-70855dedb788/go.mod h1:aGQbuCLyhRLMzZF067xc84Lh7JDs1FKwCmF1Crl9dxQ= -github.com/coder/tailscale v1.1.1-0.20250129014916-8086c871eae6 h1:prDIwUcsSEKbs1Rc5FfdvtSfz2XGpW3FnJtWR+Mc7MY= -github.com/coder/tailscale v1.1.1-0.20250129014916-8086c871eae6/go.mod h1:1ggFFdHTRjPRu9Yc1yA7nVHBYB50w9Ce7VIXNqcW6Ko= +github.com/coder/tailscale v1.1.1-0.20250227024825-c9983534152a h1:18TQ03KlYrkW8hOohTQaDnlmkY1H9pDPGbZwOnUUmm8= +github.com/coder/tailscale v1.1.1-0.20250227024825-c9983534152a/go.mod h1:1ggFFdHTRjPRu9Yc1yA7nVHBYB50w9Ce7VIXNqcW6Ko= github.com/coder/terraform-config-inspect v0.0.0-20250107175719-6d06d90c630e h1:JNLPDi2P73laR1oAclY6jWzAbucf70ASAvf5mh2cME0= github.com/coder/terraform-config-inspect v0.0.0-20250107175719-6d06d90c630e/go.mod h1:Gz/z9Hbn+4KSp8A2FBtNszfLSdT2Tn/uAKGuVqqWmDI= -github.com/coder/terraform-provider-coder v1.0.4 h1:MJldCvykIQzzqBVUDjCJpPyqvKelAAHrtJKfIIx4Qxo= -github.com/coder/terraform-provider-coder v1.0.4/go.mod h1:dQ1e/IccUxnmh/1bXTA3PopSoBkHMyWT6EkdBw8Lx6Y= +github.com/coder/terraform-provider-coder/v2 v2.1.3 h1:zB7ObGsiOGBHcJUUMmcSauEPlTWRIYmMYieF05LxHSc= +github.com/coder/terraform-provider-coder/v2 v2.1.3/go.mod h1:RHGyb+ghiy8UpDAMJM8duRFuzd+1VqA3AtkRLh2P3Ug= github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/coder/wgtunnel v0.1.13-0.20240522110300-ade90dfb2da0 h1:C2/eCr+r0a5Auuw3YOiSyLNHkdMtyCZHPFBx7syN4rk= github.com/coder/wgtunnel v0.1.13-0.20240522110300-ade90dfb2da0/go.mod h1:qANbdpqyAGlo2bg+4gQKPj24H1ZWa3bQU2Q5/bV5B3Y= github.com/coder/wireguard-go v0.0.0-20240522052547-769cdd7f7818 h1:bNhUTaKl3q0bFn78bBRq7iIwo72kNTvUD9Ll5TTzDDk= github.com/coder/wireguard-go v0.0.0-20240522052547-769cdd7f7818/go.mod h1:fAlLM6hUgnf4Sagxn2Uy5Us0PBgOYWz+63HwHUVGEbw= -github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= -github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= +github.com/containerd/continuity v0.4.4 h1:/fNVfTJ7wIl/YPMHjf+5H32uFhl63JucB34PlCpMKII= +github.com/containerd/continuity v0.4.4/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/coreos/go-iptables v0.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo4jk= github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo= @@ -271,10 +267,12 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgraph-io/badger/v3 v3.2103.5 h1:ylPa6qzbjYRQMU6jokoj4wzcaweHylt//CH0AKt0akg= -github.com/dgraph-io/badger/v3 v3.2103.5/go.mod h1:4MPiseMeDQ3FNCYwRbbcBOGJLf5jsE0PPFzRiKjtcdw= -github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= -github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= +github.com/dblohm7/wingoes v0.0.0-20240820181039-f2b84150679e h1:L+XrFvD0vBIBm+Wf9sFN6aU395t7JROoai0qXZraA4U= +github.com/dblohm7/wingoes v0.0.0-20240820181039-f2b84150679e/go.mod h1:SUxUaAK/0UG5lYyZR1L1nC4AaYYvSSYTWQSH3FPcxKU= +github.com/dgraph-io/badger/v4 v4.5.1 h1:7DCIXrQjo1LKmM96YD+hLVJ2EEsyyoWxJfpdd56HLps= +github.com/dgraph-io/badger/v4 v4.5.1/go.mod h1:qn3Be0j3TfV4kPbVoK0arXCD1/nr1ftth6sbL5jxdoA= +github.com/dgraph-io/ristretto/v2 v2.1.0 h1:59LjpOJLNDULHh8MC4UaegN52lC4JnO2dITsie/Pa8I= +github.com/dgraph-io/ristretto/v2 v2.1.0/go.mod h1:uejeqfYXpUomfse0+lO+13ATz4TypQYLJZzBSAemuB4= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= @@ -367,8 +365,8 @@ github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8= github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= -github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= -github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= +github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= +github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -400,14 +398,14 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg= -github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= +github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8= +github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= -github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= +github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE= github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10= github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc= @@ -434,8 +432,8 @@ github.com/gohugoio/hashstructure v0.3.0 h1:orHavfqnBv0ffQmobOp41Y9HKEMcjrR/8EFA github.com/gohugoio/hashstructure v0.3.0/go.mod h1:8ohPTAfQLTs2WdzB6k9etmQYclDUeNsIHGPAFejbsEA= github.com/gohugoio/httpcache v0.7.0 h1:ukPnn04Rgvx48JIinZvZetBfHaWE7I01JR2Q2RrQ3Vs= github.com/gohugoio/httpcache v0.7.0/go.mod h1:fMlPrdY/vVJhAriLZnrF5QpN3BNAcoBClgAyQd+lGFI= -github.com/gohugoio/hugo v0.141.0 h1:oTrHI/HTDXFoK4Nmx73xOa1qbx2YUaN0LG+8IV5QNF8= -github.com/gohugoio/hugo v0.141.0/go.mod h1:G0uwM5aRUXN4cbnqrDQx9Dlgmf/ukUpPADajL8FbL9M= +github.com/gohugoio/hugo v0.143.0 h1:acmpu/j47LHQcVQJ1YIIGKe+dH7cGmxarMq/aeGY3AM= +github.com/gohugoio/hugo v0.143.0/go.mod h1:G0uwM5aRUXN4cbnqrDQx9Dlgmf/ukUpPADajL8FbL9M= github.com/gohugoio/hugo-goldmark-extensions/extras v0.2.0 h1:MNdY6hYCTQEekY0oAfsxWZU1CDt6iH+tMLgyMJQh/sg= github.com/gohugoio/hugo-goldmark-extensions/extras v0.2.0/go.mod h1:oBdBVuiZ0fv9xd8xflUgt53QxW5jOCb1S+xntcN4SKo= github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.0 h1:7PY5PIJ2mck7v6R52yCFvvYHvsPMEbulgRviw3I9lP4= @@ -456,21 +454,20 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20240930133441-72d49d9543d8 h1:4txT5G2kqVAKMjzidIabL/8KqjIK71yj30YOeuxLn10= github.com/gomarkdown/markdown v0.0.0-20240930133441-72d49d9543d8/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/flatbuffers v23.1.21+incompatible h1:bUqzx/MXCDxuS0hRJL2EfjyZL3uQrPbMocUa8zGqsTA= -github.com/google/flatbuffers v23.1.21+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v24.12.23+incompatible h1:ubBKR94NR4pXUCY/MUsRVzd9umNW7ht7EG9hHfS9FX8= +github.com/google/flatbuffers v24.12.23+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github/v43 v43.0.1-0.20220414155304-00e42332e405 h1:DdHws/YnnPrSywrjNYu2lEHqYHWp/LnEx56w59esd54= github.com/google/go-github/v43 v43.0.1-0.20220414155304-00e42332e405/go.mod h1:4RgUDSnsxP19d65zJWqvqJ/poJxBCvmna50eXmIvoR8= github.com/google/go-github/v61 v61.0.0 h1:VwQCBwhyE9JclCI+22/7mLB1PuU9eowCXKY5pNlu1go= @@ -499,8 +496,8 @@ github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= github.com/hairyhenderson/go-codeowners v0.7.0 h1:s0W4wF8bdsBEjTWzwzSlsatSthWtTAF2xLgo4a4RwAo= github.com/hairyhenderson/go-codeowners v0.7.0/go.mod h1:wUlNgQ3QjqC4z8DnM5nnCYVq/icpqXJyJOukKx5U8/Q= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -539,26 +536,26 @@ github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09 github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/hashicorp/hc-install v0.9.0 h1:2dIk8LcvANwtv3QZLckxcjyF5w8KVtiMxu6G6eLhghE= -github.com/hashicorp/hc-install v0.9.0/go.mod h1:+6vOP+mf3tuGgMApVYtmsnDoKWMDcFXeTxCACYZ8SFg= +github.com/hashicorp/hc-install v0.9.1 h1:gkqTfE3vVbafGQo6VZXcy2v5yoz2bE0+nhZXruCuODQ= +github.com/hashicorp/hc-install v0.9.1/go.mod h1:pWWvN/IrfeBK4XPeXXYkL6EjMufHkCK5DvwxeLKuBf0= github.com/hashicorp/hcl v1.0.1-vault-5 h1:kI3hhbbyzr4dldA8UdTb7ZlVVlI2DACdCfz31RPDgJM= github.com/hashicorp/hcl v1.0.1-vault-5/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos= github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/terraform-exec v0.21.0 h1:uNkLAe95ey5Uux6KJdua6+cv8asgILFVWkd/RG0D2XQ= -github.com/hashicorp/terraform-exec v0.21.0/go.mod h1:1PPeMYou+KDUSSeRE9szMZ/oHf4fYUmB923Wzbq1ICg= +github.com/hashicorp/terraform-exec v0.22.0 h1:G5+4Sz6jYZfRYUCg6eQgDsqTzkNXV+fP8l+uRmZHj64= +github.com/hashicorp/terraform-exec v0.22.0/go.mod h1:bjVbsncaeh8jVdhttWYZuBGj21FcYw6Ia/XfHcNO7lQ= github.com/hashicorp/terraform-json v0.24.0 h1:rUiyF+x1kYawXeRth6fKFm/MdfBS6+lW4NbeATsYz8Q= github.com/hashicorp/terraform-json v0.24.0/go.mod h1:Nfj5ubo9xbu9uiAoZVBsNOjvNKB66Oyrvtit74kC7ow= -github.com/hashicorp/terraform-plugin-go v0.25.0 h1:oi13cx7xXA6QciMcpcFi/rwA974rdTxjqEhXJjbAyks= -github.com/hashicorp/terraform-plugin-go v0.25.0/go.mod h1:+SYagMYadJP86Kvn+TGeV+ofr/R3g4/If0O5sO96MVw= +github.com/hashicorp/terraform-plugin-go v0.26.0 h1:cuIzCv4qwigug3OS7iKhpGAbZTiypAfFQmw8aE65O2M= +github.com/hashicorp/terraform-plugin-go v0.26.0/go.mod h1:+CXjuLDiFgqR+GcrM5a2E2Kal5t5q2jb0E3D57tTdNY= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.0 h1:wyKCCtn6pBBL46c1uIIBNUOWlNfYXfXpVo16iDyLp8Y= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.0/go.mod h1:B0Al8NyYVr8Mp/KLwssKXG1RqnTk7FySqSn4fRuLNgw= -github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= -github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.0 h1:7/iejAPyCRBhqAg3jOx+4UcAhY0A+Sg8B+0+d/GxSfM= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.0/go.mod h1:TiQwXAjFrgBf5tg5rvBRz8/ubPULpU0HjSaVi5UoJf8= +github.com/hashicorp/terraform-registry-address v0.2.4 h1:JXu/zHB2Ymg/TGVCRu10XqNa4Sh2bWcqCNyKWjnCPJA= +github.com/hashicorp/terraform-registry-address v0.2.4/go.mod h1:tUNYTVyCtU4OIGXXMDp7WNcJ+0W1B4nmstVDgHMjfAU= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= @@ -606,8 +603,8 @@ github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f h1:dKccXx7xA56UNq github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f/go.mod h1:4rEELDSfUAlBSyUjPG0JnaNGjf13JySHFeRdD/3dLP0= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ= github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= @@ -699,8 +696,8 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/moby v27.5.0+incompatible h1:RuYLppjLxMzWmPUQAy/hkJ6pGcXsuVdcmIVFqVPegO8= -github.com/moby/moby v27.5.0+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= +github.com/moby/moby v28.0.0+incompatible h1:D+F1Z56b/DS8J5pUkTG/stemqrvHBQ006hUqJxjV9P0= +github.com/moby/moby v28.0.0+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/mocktools/go-smtp-mock/v2 v2.4.0 h1:u0ky0iyNW/LEMKAFRTsDivHyP8dHYxe/cV3FZC3rRjo= @@ -722,12 +719,14 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/smartcrop v0.3.0 h1:JTlSkmxWg/oQ1TcLDoypuirdE8Y/jzNirQeLkxpA6Oc= github.com/muesli/smartcrop v0.3.0/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78RwcQLUkocpI= -github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg= -github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niklasfasching/go-org v1.7.0 h1:vyMdcMWWTe/XmANk19F4k8XGBYg0GQ/gJGMimOjGMek= github.com/niklasfasching/go-org v1.7.0/go.mod h1:WuVm4d45oePiE0eX25GqTDQIt/qPW1T9DGkRscqLW5o= @@ -737,8 +736,8 @@ github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -github.com/open-policy-agent/opa v1.0.0 h1:fZsEwxg1knpPvUn0YDJuJZBcbVg4G3zKpWa3+CnYK+I= -github.com/open-policy-agent/opa v1.0.0/go.mod h1:+JyoH12I0+zqyC1iX7a2tmoQlipwAEGvOhVJMhmy+rM= +github.com/open-policy-agent/opa v1.1.0 h1:HMz2evdEMTyNqtdLjmu3Vyx06BmhNYAx67Yz3Ll9q2s= +github.com/open-policy-agent/opa v1.1.0/go.mod h1:T1pASQ1/vwfTa+e2fYcfpLCvWgYtqtiUv+IuA/dLPQs= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= @@ -787,10 +786,10 @@ github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndr github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c h1:NRoLoZvkBTKvR5gQLgA3e0hqjkY9u1wm+iOL45VN/qI= github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= -github.com/prometheus-community/pro-bing v0.5.0 h1:Fq+4BUXKIvsPtXUY8K+04ud9dkAuFozqGmRAyNUpffY= -github.com/prometheus-community/pro-bing v0.5.0/go.mod h1:1joR9oXdMEAcAJJvhs+8vNDvTg5thfAZcRFhcUozG2g= -github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= -github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus-community/pro-bing v0.6.0 h1:04SZ/092gONTE1XUFzYFWqgB4mKwcdkqNChLMFedwhg= +github.com/prometheus-community/pro-bing v0.6.0/go.mod h1:jNCOI3D7pmTCeaoF41cNS6uaxeFY/Gmc3ffwbuJVzAQ= +github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA= +github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= @@ -884,8 +883,10 @@ github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQ github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0= github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA= github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc= -github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes= -github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= +github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA= +github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk= +github.com/tchap/go-patricia/v2 v2.3.2 h1:xTHFutuitO2zqKAQ5rCROYgUb7Or/+IC3fts9/Yc7nM= +github.com/tchap/go-patricia/v2 v2.3.2/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= github.com/tdewolff/minify/v2 v2.20.37 h1:Q97cx4STXCh1dlWDlNHZniE8BJ2EBL0+2b0n92BJQhw= github.com/tdewolff/minify/v2 v2.20.37/go.mod h1:L1VYef/jwKw6Wwyk5A+T0mBjjn3mMPgmjjA688RNsxU= github.com/tdewolff/parse/v2 v2.7.15 h1:hysDXtdGZIRF5UZXwpfn3ZWRbm+ru4l53/ajBRGpCTw= @@ -918,8 +919,8 @@ github.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbW github.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE= -github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw= +github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI= +github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU= github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs= github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= @@ -969,8 +970,8 @@ github.com/yuin/goldmark-emoji v1.0.4 h1:vCwMkPZSNefSUnOW2ZKRUjBSD5Ok3W78IXhGxxA github.com/yuin/goldmark-emoji v1.0.4/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/zclconf/go-cty v1.16.0 h1:xPKEhst+BW5D0wxebMZkxgapvOE/dw7bFTlgSc9nD6w= -github.com/zclconf/go-cty v1.16.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/zclconf/go-cty v1.16.2 h1:LAJSwc3v81IRBZyUVQDUdZ7hs3SYs9jv0eZJDWHD/70= +github.com/zclconf/go-cty v1.16.2/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0= @@ -998,35 +999,35 @@ go.opentelemetry.io/collector/semconv v0.104.0/go.mod h1:yMVUCNoQPZVq/IPfrHrnntZ go.opentelemetry.io/contrib v1.0.0/go.mod h1:EH4yDYeNoaTqn/8yCWQmfNB78VHfGX2Jt2bvnvzBlGM= go.opentelemetry.io/contrib v1.19.0 h1:rnYI7OEPMWFeM4QCqWQ3InMJ0arWMR1i0Cx9A5hcjYM= go.opentelemetry.io/contrib v1.19.0/go.mod h1:gIzjwWFoGazJmtCaDgViqOSJPde2mCWzv60o0bWPcZs= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 h1:PS8wXpbyaDJQ2VDHHncMe9Vct0Zn1fEjpsjrLxGJoSc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= go.opentelemetry.io/otel v1.3.0/go.mod h1:PWIKzi6JCp7sM0k9yZ43VX+T345uNbAkDKwHVjb2PTs= -go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= -go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= go.opentelemetry.io/otel/exporters/prometheus v0.49.0 h1:Er5I1g/YhfYv9Affk9nJLfH/+qCCVVg1f2R9AbJfqDQ= go.opentelemetry.io/otel/exporters/prometheus v0.49.0/go.mod h1:KfQ1wpjf3zsHjzP149P4LyAwWRupc6c7t1ZJ9eXpKQM= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.33.0 h1:FiOTYABOX4tdzi8A0+mtzcsTmi6WBOxk66u0f1Mj9Gs= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.33.0/go.mod h1:xyo5rS8DgzV0Jtsht+LCEMwyiDbjpsxBpWETwFRF0/4= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0 h1:W5AWUn/IVe8RFb5pZx1Uh9Laf/4+Qmm4kJL5zPuvR+0= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0/go.mod h1:mzKxJywMNBdEX8TSJais3NnsVZUaJ+bAy6UxPTng2vk= -go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= -go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= go.opentelemetry.io/otel/sdk v1.3.0/go.mod h1:rIo4suHNhQwBIPg9axF8V9CA72Wz2mKF1teNrup8yzs= -go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= -go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= go.opentelemetry.io/otel/sdk/metric v1.33.0 h1:Gs5VK9/WUJhNXZgn8MR6ITatvAmKeIuCtNbsP3JkNqU= go.opentelemetry.io/otel/sdk/metric v1.33.0/go.mod h1:dL5ykHZmm1B1nVRk9dDjChwDmt81MjVp3gLkQRwKf/Q= go.opentelemetry.io/otel/trace v1.3.0/go.mod h1:c/VDhno8888bvQYmbYLqe41/Ldmr/KKunbvWM4/fEjk= -go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= -go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= -go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg= -go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= @@ -1050,8 +1051,8 @@ golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI= golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/image v0.22.0 h1:UtK5yLUzilVrkjMAZAZ34DXGpASN8i8pj8g+O+yd10g= @@ -1061,8 +1062,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= -golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -1074,10 +1075,10 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= -golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= -golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= +golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1085,8 +1086,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1126,8 +1127,8 @@ golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.29.1-0.20250107080300-1c14dcadc3ab h1:BMkEEWYOjkvOX7+YKOGbp6jCyQ5pR2j0Ah47p1Vdsx4= -golang.org/x/sys v0.29.1-0.20250107080300-1c14dcadc3ab/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1135,8 +1136,8 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= -golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= -golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -1147,10 +1148,10 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= +golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -1158,8 +1159,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= -golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1172,26 +1173,26 @@ golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvY golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80= golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI= -google.golang.org/api v0.217.0 h1:GYrUtD289o4zl1AhiTZL0jvQGa2RDLyC+kX1N/lfGOU= -google.golang.org/api v0.217.0/go.mod h1:qMc2E8cBAbQlRypBTBWHklNJlaZZJBwDv81B1Iu8oSI= +google.golang.org/api v0.221.0 h1:qzaJfLhDsbMeFee8zBRdt/Nc+xmOuafD/dbdgGfutOU= +google.golang.org/api v0.221.0/go.mod h1:7sOU2+TL4TxUTdbi0gWgAIg7tH5qBXxoyhtL+9x3biQ= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc= -google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= -google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 h1:3UsHvIr4Wc2aW4brOaSCmcxh9ksica6fHEr8P1XhkYw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= -google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= -google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 h1:2duwAxN2+k0xLNpjnHTXoMUgnv6VPSp5fiqTuwSxjmI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= +google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= +google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU= -google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -gopkg.in/DataDog/dd-trace-go.v1 v1.70.3 h1:lXHrxMpQZjxNdA8mGRfgMtwF/O6qIut5QjL7LICUVJ4= -gopkg.in/DataDog/dd-trace-go.v1 v1.70.3/go.mod h1:CVUgctrrPGeB+OSjgyt56CNH5QxQwW3t11QU8R1LQjQ= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/DataDog/dd-trace-go.v1 v1.72.1 h1:QG2HNpxe9H4WnztDYbdGQJL/5YIiiZ6xY1+wMuQ2c1w= +gopkg.in/DataDog/dd-trace-go.v1 v1.72.1/go.mod h1:XqDhDqsLpThFnJc4z0FvAEItISIAUka+RHwmQ6EfN1U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/helm/coder/README.md b/helm/coder/README.md index 015c2e7039088..172f880c83045 100644 --- a/helm/coder/README.md +++ b/helm/coder/README.md @@ -47,6 +47,10 @@ coder: # This env enables the Prometheus metrics endpoint. - name: CODER_PROMETHEUS_ADDRESS value: "0.0.0.0:2112" + # For production deployments, we recommend configuring your own GitHub + # OAuth2 provider and disabling the default one. + - name: CODER_OAUTH2_GITHUB_DEFAULT_PROVIDER_ENABLE + value: "false" tls: secretNames: - my-tls-secret-name diff --git a/helm/coder/templates/ingress.yaml b/helm/coder/templates/ingress.yaml index 7dd2a1389e233..0ca2726fcd2c1 100644 --- a/helm/coder/templates/ingress.yaml +++ b/helm/coder/templates/ingress.yaml @@ -1,10 +1,10 @@ - {{- if .Values.coder.ingress.enable }} --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: coder + namespace: {{ .Release.Namespace }} labels: {{- include "coder.labels" . | nindent 4 }} annotations: diff --git a/helm/coder/templates/service.yaml b/helm/coder/templates/service.yaml index de81d57c2a306..30c3825d10f5d 100644 --- a/helm/coder/templates/service.yaml +++ b/helm/coder/templates/service.yaml @@ -4,6 +4,7 @@ apiVersion: v1 kind: Service metadata: name: coder + namespace: {{ .Release.Namespace }} labels: {{- include "coder.labels" . | nindent 4 }} annotations: diff --git a/helm/coder/tests/chart_test.go b/helm/coder/tests/chart_test.go index 6c0b181a30550..a00ad7ee28107 100644 --- a/helm/coder/tests/chart_test.go +++ b/helm/coder/tests/chart_test.go @@ -23,6 +23,11 @@ import ( // updateGoldenFiles is a flag that can be set to update golden files. var updateGoldenFiles = flag.Bool("update", false, "Update golden files") +var namespaces = []string{ + "default", + "coder", +} + var testCases = []testCase{ { name: "default_values", @@ -108,10 +113,15 @@ var testCases = []testCase{ name: "svc_loadbalancer", expectedError: "", }, + { + name: "securitycontext", + expectedError: "", + }, } type testCase struct { name string // Name of the test case. This is used to control which values and golden file are used. + namespace string // Namespace is the name of the namespace the resources should be generated within expectedError string // Expected error from running `helm template`. } @@ -120,7 +130,11 @@ func (tc testCase) valuesFilePath() string { } func (tc testCase) goldenFilePath() string { - return filepath.Join("./testdata", tc.name+".golden") + if tc.namespace == "default" { + return filepath.Join("./testdata", tc.name+".golden") + } + + return filepath.Join("./testdata", tc.name+"_"+tc.namespace+".golden") } func TestRenderChart(t *testing.T) { @@ -142,35 +156,41 @@ func TestRenderChart(t *testing.T) { for _, tc := range testCases { tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - // Ensure that the values file exists. - valuesFilePath := tc.valuesFilePath() - if _, err := os.Stat(valuesFilePath); os.IsNotExist(err) { - t.Fatalf("values file %q does not exist", valuesFilePath) - } + for _, ns := range namespaces { + tc := tc + tc.namespace = ns - // Run helm template with the values file. - templateOutput, err := runHelmTemplate(t, helmPath, "..", valuesFilePath) - if tc.expectedError != "" { - require.Error(t, err, "helm template should have failed") - require.Contains(t, templateOutput, tc.expectedError, "helm template output should contain expected error") - } else { - require.NoError(t, err, "helm template should not have failed") - require.NotEmpty(t, templateOutput, "helm template output should not be empty") - goldenFilePath := tc.goldenFilePath() - goldenBytes, err := os.ReadFile(goldenFilePath) - require.NoError(t, err, "failed to read golden file %q", goldenFilePath) - - // Remove carriage returns to make tests pass on Windows. - goldenBytes = bytes.Replace(goldenBytes, []byte("\r"), []byte(""), -1) - expected := string(goldenBytes) - - require.NoError(t, err, "failed to load golden file %q") - require.Equal(t, expected, templateOutput) - } - }) + t.Run(tc.namespace+"/"+tc.name, func(t *testing.T) { + t.Parallel() + + // Ensure that the values file exists. + valuesFilePath := tc.valuesFilePath() + if _, err := os.Stat(valuesFilePath); os.IsNotExist(err) { + t.Fatalf("values file %q does not exist", valuesFilePath) + } + + // Run helm template with the values file. + templateOutput, err := runHelmTemplate(t, helmPath, "..", valuesFilePath, tc.namespace) + if tc.expectedError != "" { + require.Error(t, err, "helm template should have failed") + require.Contains(t, templateOutput, tc.expectedError, "helm template output should contain expected error") + } else { + require.NoError(t, err, "helm template should not have failed") + require.NotEmpty(t, templateOutput, "helm template output should not be empty") + goldenFilePath := tc.goldenFilePath() + goldenBytes, err := os.ReadFile(goldenFilePath) + require.NoError(t, err, "failed to read golden file %q", goldenFilePath) + + // Remove carriage returns to make tests pass on Windows. + goldenBytes = bytes.ReplaceAll(goldenBytes, []byte("\r"), []byte("")) + expected := string(goldenBytes) + + require.NoError(t, err, "failed to load golden file %q") + require.Equal(t, expected, templateOutput) + } + }) + } } } @@ -185,22 +205,28 @@ func TestUpdateGoldenFiles(t *testing.T) { require.NoError(t, err, "failed to build Helm dependencies") for _, tc := range testCases { + tc := tc if tc.expectedError != "" { t.Logf("skipping test case %q with render error", tc.name) continue } - valuesPath := tc.valuesFilePath() - templateOutput, err := runHelmTemplate(t, helmPath, "..", valuesPath) - if err != nil { - t.Logf("error running `helm template -f %q`: %v", valuesPath, err) - t.Logf("output: %s", templateOutput) - } - require.NoError(t, err, "failed to run `helm template -f %q`", valuesPath) + for _, ns := range namespaces { + tc := tc + tc.namespace = ns + + valuesPath := tc.valuesFilePath() + templateOutput, err := runHelmTemplate(t, helmPath, "..", valuesPath, tc.namespace) + if err != nil { + t.Logf("error running `helm template -f %q`: %v", valuesPath, err) + t.Logf("output: %s", templateOutput) + } + require.NoError(t, err, "failed to run `helm template -f %q`", valuesPath) - goldenFilePath := tc.goldenFilePath() - err = os.WriteFile(goldenFilePath, []byte(templateOutput), 0o644) // nolint:gosec - require.NoError(t, err, "failed to write golden file %q", goldenFilePath) + goldenFilePath := tc.goldenFilePath() + err = os.WriteFile(goldenFilePath, []byte(templateOutput), 0o644) // nolint:gosec + require.NoError(t, err, "failed to write golden file %q", goldenFilePath) + } } t.Log("Golden files updated. Please review the changes and commit them.") } @@ -227,13 +253,13 @@ func updateHelmDependencies(t testing.TB, helmPath, chartDir string) error { // runHelmTemplate runs helm template on the given chart with the given values and // returns the raw output. -func runHelmTemplate(t testing.TB, helmPath, chartDir, valuesFilePath string) (string, error) { +func runHelmTemplate(t testing.TB, helmPath, chartDir, valuesFilePath, namespace string) (string, error) { // Ensure that valuesFilePath exists if _, err := os.Stat(valuesFilePath); err != nil { return "", xerrors.Errorf("values file %q does not exist: %w", valuesFilePath, err) } - cmd := exec.Command(helmPath, "template", chartDir, "-f", valuesFilePath, "--namespace", "default") + cmd := exec.Command(helmPath, "template", chartDir, "-f", valuesFilePath, "--namespace", namespace) t.Logf("exec command: %v", cmd.Args) out, err := cmd.CombinedOutput() return string(out), err diff --git a/helm/coder/tests/testdata/auto_access_url_1.golden b/helm/coder/tests/testdata/auto_access_url_1.golden index db2d9500255fc..26773759217ab 100644 --- a/helm/coder/tests/testdata/auto_access_url_1.golden +++ b/helm/coder/tests/testdata/auto_access_url_1.golden @@ -12,12 +12,14 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default --- # Source: coder/templates/rbac.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: coder-workspace-perms + namespace: default rules: - apiGroups: [""] resources: ["pods"] @@ -60,6 +62,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: "coder" + namespace: default subjects: - kind: ServiceAccount name: "coder" @@ -73,6 +76,7 @@ apiVersion: v1 kind: Service metadata: name: coder + namespace: default labels: helm.sh/chart: coder-0.1.0 app.kubernetes.io/name: coder @@ -109,6 +113,7 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default spec: replicas: 1 selector: diff --git a/helm/coder/tests/testdata/auto_access_url_1_coder.golden b/helm/coder/tests/testdata/auto_access_url_1_coder.golden new file mode 100644 index 0000000000000..39acb62538146 --- /dev/null +++ b/helm/coder/tests/testdata/auto_access_url_1_coder.golden @@ -0,0 +1,197 @@ +--- +# Source: coder/templates/coder.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder-workspace-perms + namespace: coder +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "coder" + namespace: coder +subjects: + - kind: ServiceAccount + name: "coder" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coder-workspace-perms +--- +# Source: coder/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: coder + namespace: coder + labels: + helm.sh/chart: coder-0.1.0 + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: "0.1.0" + app.kubernetes.io/managed-by: Helm + annotations: + {} +spec: + type: LoadBalancer + sessionAffinity: None + ports: + - name: "http" + port: 80 + targetPort: "http" + protocol: TCP + nodePort: + externalTrafficPolicy: "Cluster" + selector: + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name +--- +# Source: coder/templates/coder.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: coder + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: app.kubernetes.io/instance + operator: In + values: + - coder + topologyKey: kubernetes.io/hostname + weight: 1 + containers: + - args: + - server + command: + - /opt/coder + env: + - name: CODER_HTTP_ADDRESS + value: 0.0.0.0:8080 + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: KUBE_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: CODER_DERP_SERVER_RELAY_URL + value: http://$(KUBE_POD_IP):8080 + - name: SOME_ENV + value: some value + - name: CODER_ACCESS_URL + value: https://dev.coder.com + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + livenessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + name: coder + ports: + - containerPort: 8080 + name: http + protocol: TCP + readinessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + resources: {} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: coder + terminationGracePeriodSeconds: 60 + volumes: [] diff --git a/helm/coder/tests/testdata/auto_access_url_2.golden b/helm/coder/tests/testdata/auto_access_url_2.golden index 4f9c8c2627c49..7c3c0207eb091 100644 --- a/helm/coder/tests/testdata/auto_access_url_2.golden +++ b/helm/coder/tests/testdata/auto_access_url_2.golden @@ -12,12 +12,14 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default --- # Source: coder/templates/rbac.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: coder-workspace-perms + namespace: default rules: - apiGroups: [""] resources: ["pods"] @@ -60,6 +62,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: "coder" + namespace: default subjects: - kind: ServiceAccount name: "coder" @@ -73,6 +76,7 @@ apiVersion: v1 kind: Service metadata: name: coder + namespace: default labels: helm.sh/chart: coder-0.1.0 app.kubernetes.io/name: coder @@ -109,6 +113,7 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default spec: replicas: 1 selector: diff --git a/helm/coder/tests/testdata/auto_access_url_2_coder.golden b/helm/coder/tests/testdata/auto_access_url_2_coder.golden new file mode 100644 index 0000000000000..ca3265c89088d --- /dev/null +++ b/helm/coder/tests/testdata/auto_access_url_2_coder.golden @@ -0,0 +1,197 @@ +--- +# Source: coder/templates/coder.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder-workspace-perms + namespace: coder +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "coder" + namespace: coder +subjects: + - kind: ServiceAccount + name: "coder" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coder-workspace-perms +--- +# Source: coder/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: coder + namespace: coder + labels: + helm.sh/chart: coder-0.1.0 + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: "0.1.0" + app.kubernetes.io/managed-by: Helm + annotations: + {} +spec: + type: LoadBalancer + sessionAffinity: None + ports: + - name: "http" + port: 80 + targetPort: "http" + protocol: TCP + nodePort: + externalTrafficPolicy: "Cluster" + selector: + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name +--- +# Source: coder/templates/coder.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: coder + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: app.kubernetes.io/instance + operator: In + values: + - coder + topologyKey: kubernetes.io/hostname + weight: 1 + containers: + - args: + - server + command: + - /opt/coder + env: + - name: CODER_HTTP_ADDRESS + value: 0.0.0.0:8080 + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_ACCESS_URL + value: http://coder.coder.svc.cluster.local + - name: KUBE_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: CODER_DERP_SERVER_RELAY_URL + value: http://$(KUBE_POD_IP):8080 + - name: SOME_ENV + value: some value + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + livenessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + name: coder + ports: + - containerPort: 8080 + name: http + protocol: TCP + readinessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + resources: {} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: coder + terminationGracePeriodSeconds: 60 + volumes: [] diff --git a/helm/coder/tests/testdata/auto_access_url_3.golden b/helm/coder/tests/testdata/auto_access_url_3.golden index b848a82862c76..9bd33b54a6d89 100644 --- a/helm/coder/tests/testdata/auto_access_url_3.golden +++ b/helm/coder/tests/testdata/auto_access_url_3.golden @@ -12,12 +12,14 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default --- # Source: coder/templates/rbac.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: coder-workspace-perms + namespace: default rules: - apiGroups: [""] resources: ["pods"] @@ -60,6 +62,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: "coder" + namespace: default subjects: - kind: ServiceAccount name: "coder" @@ -73,6 +76,7 @@ apiVersion: v1 kind: Service metadata: name: coder + namespace: default labels: helm.sh/chart: coder-0.1.0 app.kubernetes.io/name: coder @@ -109,6 +113,7 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default spec: replicas: 1 selector: diff --git a/helm/coder/tests/testdata/auto_access_url_3_coder.golden b/helm/coder/tests/testdata/auto_access_url_3_coder.golden new file mode 100644 index 0000000000000..36fff8666c80c --- /dev/null +++ b/helm/coder/tests/testdata/auto_access_url_3_coder.golden @@ -0,0 +1,195 @@ +--- +# Source: coder/templates/coder.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder-workspace-perms + namespace: coder +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "coder" + namespace: coder +subjects: + - kind: ServiceAccount + name: "coder" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coder-workspace-perms +--- +# Source: coder/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: coder + namespace: coder + labels: + helm.sh/chart: coder-0.1.0 + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: "0.1.0" + app.kubernetes.io/managed-by: Helm + annotations: + {} +spec: + type: LoadBalancer + sessionAffinity: None + ports: + - name: "http" + port: 80 + targetPort: "http" + protocol: TCP + nodePort: + externalTrafficPolicy: "Cluster" + selector: + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name +--- +# Source: coder/templates/coder.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: coder + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: app.kubernetes.io/instance + operator: In + values: + - coder + topologyKey: kubernetes.io/hostname + weight: 1 + containers: + - args: + - server + command: + - /opt/coder + env: + - name: CODER_HTTP_ADDRESS + value: 0.0.0.0:8080 + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: KUBE_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: CODER_DERP_SERVER_RELAY_URL + value: http://$(KUBE_POD_IP):8080 + - name: SOME_ENV + value: some value + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + livenessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + name: coder + ports: + - containerPort: 8080 + name: http + protocol: TCP + readinessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + resources: {} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: coder + terminationGracePeriodSeconds: 60 + volumes: [] diff --git a/helm/coder/tests/testdata/command.golden b/helm/coder/tests/testdata/command.golden index f4ea75558dd51..899ac924ba6bd 100644 --- a/helm/coder/tests/testdata/command.golden +++ b/helm/coder/tests/testdata/command.golden @@ -12,12 +12,14 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default --- # Source: coder/templates/rbac.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: coder-workspace-perms + namespace: default rules: - apiGroups: [""] resources: ["pods"] @@ -60,6 +62,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: "coder" + namespace: default subjects: - kind: ServiceAccount name: "coder" @@ -73,6 +76,7 @@ apiVersion: v1 kind: Service metadata: name: coder + namespace: default labels: helm.sh/chart: coder-0.1.0 app.kubernetes.io/name: coder @@ -109,6 +113,7 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default spec: replicas: 1 selector: diff --git a/helm/coder/tests/testdata/command_args.golden b/helm/coder/tests/testdata/command_args.golden index f90c190a81107..9c907d9494399 100644 --- a/helm/coder/tests/testdata/command_args.golden +++ b/helm/coder/tests/testdata/command_args.golden @@ -12,12 +12,14 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default --- # Source: coder/templates/rbac.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: coder-workspace-perms + namespace: default rules: - apiGroups: [""] resources: ["pods"] @@ -60,6 +62,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: "coder" + namespace: default subjects: - kind: ServiceAccount name: "coder" @@ -73,6 +76,7 @@ apiVersion: v1 kind: Service metadata: name: coder + namespace: default labels: helm.sh/chart: coder-0.1.0 app.kubernetes.io/name: coder @@ -109,6 +113,7 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default spec: replicas: 1 selector: diff --git a/helm/coder/tests/testdata/command_args_coder.golden b/helm/coder/tests/testdata/command_args_coder.golden new file mode 100644 index 0000000000000..c0e5e7d32d5f4 --- /dev/null +++ b/helm/coder/tests/testdata/command_args_coder.golden @@ -0,0 +1,196 @@ +--- +# Source: coder/templates/coder.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder-workspace-perms + namespace: coder +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "coder" + namespace: coder +subjects: + - kind: ServiceAccount + name: "coder" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coder-workspace-perms +--- +# Source: coder/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: coder + namespace: coder + labels: + helm.sh/chart: coder-0.1.0 + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: "0.1.0" + app.kubernetes.io/managed-by: Helm + annotations: + {} +spec: + type: LoadBalancer + sessionAffinity: None + ports: + - name: "http" + port: 80 + targetPort: "http" + protocol: TCP + nodePort: + externalTrafficPolicy: "Cluster" + selector: + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name +--- +# Source: coder/templates/coder.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: coder + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: app.kubernetes.io/instance + operator: In + values: + - coder + topologyKey: kubernetes.io/hostname + weight: 1 + containers: + - args: + - arg1 + - arg2 + command: + - /opt/coder + env: + - name: CODER_HTTP_ADDRESS + value: 0.0.0.0:8080 + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_ACCESS_URL + value: http://coder.coder.svc.cluster.local + - name: KUBE_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: CODER_DERP_SERVER_RELAY_URL + value: http://$(KUBE_POD_IP):8080 + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + livenessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + name: coder + ports: + - containerPort: 8080 + name: http + protocol: TCP + readinessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + resources: {} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: coder + terminationGracePeriodSeconds: 60 + volumes: [] diff --git a/helm/coder/tests/testdata/command_coder.golden b/helm/coder/tests/testdata/command_coder.golden new file mode 100644 index 0000000000000..7b5acf605c98e --- /dev/null +++ b/helm/coder/tests/testdata/command_coder.golden @@ -0,0 +1,195 @@ +--- +# Source: coder/templates/coder.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder-workspace-perms + namespace: coder +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "coder" + namespace: coder +subjects: + - kind: ServiceAccount + name: "coder" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coder-workspace-perms +--- +# Source: coder/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: coder + namespace: coder + labels: + helm.sh/chart: coder-0.1.0 + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: "0.1.0" + app.kubernetes.io/managed-by: Helm + annotations: + {} +spec: + type: LoadBalancer + sessionAffinity: None + ports: + - name: "http" + port: 80 + targetPort: "http" + protocol: TCP + nodePort: + externalTrafficPolicy: "Cluster" + selector: + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name +--- +# Source: coder/templates/coder.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: coder + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: app.kubernetes.io/instance + operator: In + values: + - coder + topologyKey: kubernetes.io/hostname + weight: 1 + containers: + - args: + - server + command: + - /opt/colin + env: + - name: CODER_HTTP_ADDRESS + value: 0.0.0.0:8080 + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_ACCESS_URL + value: http://coder.coder.svc.cluster.local + - name: KUBE_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: CODER_DERP_SERVER_RELAY_URL + value: http://$(KUBE_POD_IP):8080 + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + livenessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + name: coder + ports: + - containerPort: 8080 + name: http + protocol: TCP + readinessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + resources: {} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: coder + terminationGracePeriodSeconds: 60 + volumes: [] diff --git a/helm/coder/tests/testdata/default_values.golden b/helm/coder/tests/testdata/default_values.golden index f1a9b7ebf6153..6510c50a82319 100644 --- a/helm/coder/tests/testdata/default_values.golden +++ b/helm/coder/tests/testdata/default_values.golden @@ -12,12 +12,14 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default --- # Source: coder/templates/rbac.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: coder-workspace-perms + namespace: default rules: - apiGroups: [""] resources: ["pods"] @@ -60,6 +62,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: "coder" + namespace: default subjects: - kind: ServiceAccount name: "coder" @@ -73,6 +76,7 @@ apiVersion: v1 kind: Service metadata: name: coder + namespace: default labels: helm.sh/chart: coder-0.1.0 app.kubernetes.io/name: coder @@ -109,6 +113,7 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default spec: replicas: 1 selector: diff --git a/helm/coder/tests/testdata/default_values_coder.golden b/helm/coder/tests/testdata/default_values_coder.golden new file mode 100644 index 0000000000000..72c3e296007f5 --- /dev/null +++ b/helm/coder/tests/testdata/default_values_coder.golden @@ -0,0 +1,195 @@ +--- +# Source: coder/templates/coder.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder-workspace-perms + namespace: coder +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "coder" + namespace: coder +subjects: + - kind: ServiceAccount + name: "coder" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coder-workspace-perms +--- +# Source: coder/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: coder + namespace: coder + labels: + helm.sh/chart: coder-0.1.0 + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: "0.1.0" + app.kubernetes.io/managed-by: Helm + annotations: + {} +spec: + type: LoadBalancer + sessionAffinity: None + ports: + - name: "http" + port: 80 + targetPort: "http" + protocol: TCP + nodePort: + externalTrafficPolicy: "Cluster" + selector: + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name +--- +# Source: coder/templates/coder.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: coder + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: app.kubernetes.io/instance + operator: In + values: + - coder + topologyKey: kubernetes.io/hostname + weight: 1 + containers: + - args: + - server + command: + - /opt/coder + env: + - name: CODER_HTTP_ADDRESS + value: 0.0.0.0:8080 + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_ACCESS_URL + value: http://coder.coder.svc.cluster.local + - name: KUBE_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: CODER_DERP_SERVER_RELAY_URL + value: http://$(KUBE_POD_IP):8080 + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + livenessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + name: coder + ports: + - containerPort: 8080 + name: http + protocol: TCP + readinessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + resources: {} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: coder + terminationGracePeriodSeconds: 60 + volumes: [] diff --git a/helm/coder/tests/testdata/env_from.golden b/helm/coder/tests/testdata/env_from.golden index 6d8bb6426d12b..9abd0578c74d6 100644 --- a/helm/coder/tests/testdata/env_from.golden +++ b/helm/coder/tests/testdata/env_from.golden @@ -12,12 +12,14 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default --- # Source: coder/templates/rbac.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: coder-workspace-perms + namespace: default rules: - apiGroups: [""] resources: ["pods"] @@ -60,6 +62,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: "coder" + namespace: default subjects: - kind: ServiceAccount name: "coder" @@ -73,6 +76,7 @@ apiVersion: v1 kind: Service metadata: name: coder + namespace: default labels: helm.sh/chart: coder-0.1.0 app.kubernetes.io/name: coder @@ -109,6 +113,7 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default spec: replicas: 1 selector: diff --git a/helm/coder/tests/testdata/env_from_coder.golden b/helm/coder/tests/testdata/env_from_coder.golden new file mode 100644 index 0000000000000..3588860882b8b --- /dev/null +++ b/helm/coder/tests/testdata/env_from_coder.golden @@ -0,0 +1,207 @@ +--- +# Source: coder/templates/coder.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder-workspace-perms + namespace: coder +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "coder" + namespace: coder +subjects: + - kind: ServiceAccount + name: "coder" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coder-workspace-perms +--- +# Source: coder/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: coder + namespace: coder + labels: + helm.sh/chart: coder-0.1.0 + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: "0.1.0" + app.kubernetes.io/managed-by: Helm + annotations: + {} +spec: + type: LoadBalancer + sessionAffinity: None + ports: + - name: "http" + port: 80 + targetPort: "http" + protocol: TCP + nodePort: + externalTrafficPolicy: "Cluster" + selector: + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name +--- +# Source: coder/templates/coder.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: coder + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: app.kubernetes.io/instance + operator: In + values: + - coder + topologyKey: kubernetes.io/hostname + weight: 1 + containers: + - args: + - server + command: + - /opt/coder + env: + - name: CODER_HTTP_ADDRESS + value: 0.0.0.0:8080 + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_ACCESS_URL + value: http://coder.coder.svc.cluster.local + - name: KUBE_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: CODER_DERP_SERVER_RELAY_URL + value: http://$(KUBE_POD_IP):8080 + - name: COOL_ENV + valueFrom: + configMapKeyRef: + key: value + name: cool-env + - name: COOL_ENV2 + value: cool value + envFrom: + - configMapRef: + name: cool-configmap + - secretRef: + name: cool-secret + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + livenessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + name: coder + ports: + - containerPort: 8080 + name: http + protocol: TCP + readinessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + resources: {} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: coder + terminationGracePeriodSeconds: 60 + volumes: [] diff --git a/helm/coder/tests/testdata/extra_templates.golden b/helm/coder/tests/testdata/extra_templates.golden index 53a4f95ebcdcc..a8aab8f7b8ec9 100644 --- a/helm/coder/tests/testdata/extra_templates.golden +++ b/helm/coder/tests/testdata/extra_templates.golden @@ -12,6 +12,7 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default --- # Source: coder/templates/extra-templates.yaml apiVersion: v1 @@ -27,6 +28,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: coder-workspace-perms + namespace: default rules: - apiGroups: [""] resources: ["pods"] @@ -69,6 +71,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: "coder" + namespace: default subjects: - kind: ServiceAccount name: "coder" @@ -82,6 +85,7 @@ apiVersion: v1 kind: Service metadata: name: coder + namespace: default labels: helm.sh/chart: coder-0.1.0 app.kubernetes.io/name: coder @@ -118,6 +122,7 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default spec: replicas: 1 selector: diff --git a/helm/coder/tests/testdata/extra_templates_coder.golden b/helm/coder/tests/testdata/extra_templates_coder.golden new file mode 100644 index 0000000000000..b93eb1d821a87 --- /dev/null +++ b/helm/coder/tests/testdata/extra_templates_coder.golden @@ -0,0 +1,204 @@ +--- +# Source: coder/templates/coder.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +--- +# Source: coder/templates/extra-templates.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: some-config + namespace: coder +data: + key: some-value +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder-workspace-perms + namespace: coder +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "coder" + namespace: coder +subjects: + - kind: ServiceAccount + name: "coder" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coder-workspace-perms +--- +# Source: coder/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: coder + namespace: coder + labels: + helm.sh/chart: coder-0.1.0 + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: "0.1.0" + app.kubernetes.io/managed-by: Helm + annotations: + {} +spec: + type: LoadBalancer + sessionAffinity: None + ports: + - name: "http" + port: 80 + targetPort: "http" + protocol: TCP + nodePort: + externalTrafficPolicy: "Cluster" + selector: + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name +--- +# Source: coder/templates/coder.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: coder + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: app.kubernetes.io/instance + operator: In + values: + - coder + topologyKey: kubernetes.io/hostname + weight: 1 + containers: + - args: + - server + command: + - /opt/coder + env: + - name: CODER_HTTP_ADDRESS + value: 0.0.0.0:8080 + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_ACCESS_URL + value: http://coder.coder.svc.cluster.local + - name: KUBE_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: CODER_DERP_SERVER_RELAY_URL + value: http://$(KUBE_POD_IP):8080 + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + livenessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + name: coder + ports: + - containerPort: 8080 + name: http + protocol: TCP + readinessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + resources: {} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: coder + terminationGracePeriodSeconds: 60 + volumes: [] diff --git a/helm/coder/tests/testdata/labels_annotations.golden b/helm/coder/tests/testdata/labels_annotations.golden index c0f796466f8ec..3636fd3223704 100644 --- a/helm/coder/tests/testdata/labels_annotations.golden +++ b/helm/coder/tests/testdata/labels_annotations.golden @@ -12,12 +12,14 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default --- # Source: coder/templates/rbac.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: coder-workspace-perms + namespace: default rules: - apiGroups: [""] resources: ["pods"] @@ -60,6 +62,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: "coder" + namespace: default subjects: - kind: ServiceAccount name: "coder" @@ -73,6 +76,7 @@ apiVersion: v1 kind: Service metadata: name: coder + namespace: default labels: helm.sh/chart: coder-0.1.0 app.kubernetes.io/name: coder @@ -113,6 +117,7 @@ metadata: com.coder/label/foo: bar helm.sh/chart: coder-0.1.0 name: coder + namespace: default spec: replicas: 1 selector: diff --git a/helm/coder/tests/testdata/labels_annotations_coder.golden b/helm/coder/tests/testdata/labels_annotations_coder.golden new file mode 100644 index 0000000000000..60782e25ed7c0 --- /dev/null +++ b/helm/coder/tests/testdata/labels_annotations_coder.golden @@ -0,0 +1,203 @@ +--- +# Source: coder/templates/coder.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder-workspace-perms + namespace: coder +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "coder" + namespace: coder +subjects: + - kind: ServiceAccount + name: "coder" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coder-workspace-perms +--- +# Source: coder/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: coder + namespace: coder + labels: + helm.sh/chart: coder-0.1.0 + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: "0.1.0" + app.kubernetes.io/managed-by: Helm + annotations: + {} +spec: + type: LoadBalancer + sessionAffinity: None + ports: + - name: "http" + port: 80 + targetPort: "http" + protocol: TCP + nodePort: + externalTrafficPolicy: "Cluster" + selector: + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name +--- +# Source: coder/templates/coder.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + com.coder/annotation/baz: qux + com.coder/annotation/foo: bar + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + com.coder/label/baz: qux + com.coder/label/foo: bar + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: coder + template: + metadata: + annotations: + com.coder/podAnnotation/baz: qux + com.coder/podAnnotation/foo: bar + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + com.coder/podLabel/baz: qux + com.coder/podLabel/foo: bar + helm.sh/chart: coder-0.1.0 + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: app.kubernetes.io/instance + operator: In + values: + - coder + topologyKey: kubernetes.io/hostname + weight: 1 + containers: + - args: + - server + command: + - /opt/coder + env: + - name: CODER_HTTP_ADDRESS + value: 0.0.0.0:8080 + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_ACCESS_URL + value: http://coder.coder.svc.cluster.local + - name: KUBE_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: CODER_DERP_SERVER_RELAY_URL + value: http://$(KUBE_POD_IP):8080 + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + livenessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + name: coder + ports: + - containerPort: 8080 + name: http + protocol: TCP + readinessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + resources: {} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: coder + terminationGracePeriodSeconds: 60 + volumes: [] diff --git a/helm/coder/tests/testdata/prometheus.golden b/helm/coder/tests/testdata/prometheus.golden index c199a20410842..b86bca59b0cc9 100644 --- a/helm/coder/tests/testdata/prometheus.golden +++ b/helm/coder/tests/testdata/prometheus.golden @@ -12,12 +12,14 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default --- # Source: coder/templates/rbac.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: coder-workspace-perms + namespace: default rules: - apiGroups: [""] resources: ["pods"] @@ -60,6 +62,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: "coder" + namespace: default subjects: - kind: ServiceAccount name: "coder" @@ -73,6 +76,7 @@ apiVersion: v1 kind: Service metadata: name: coder + namespace: default labels: helm.sh/chart: coder-0.1.0 app.kubernetes.io/name: coder @@ -108,6 +112,7 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default spec: replicas: 1 selector: diff --git a/helm/coder/tests/testdata/prometheus_coder.golden b/helm/coder/tests/testdata/prometheus_coder.golden new file mode 100644 index 0000000000000..74176bbecff45 --- /dev/null +++ b/helm/coder/tests/testdata/prometheus_coder.golden @@ -0,0 +1,199 @@ +--- +# Source: coder/templates/coder.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder-workspace-perms + namespace: coder +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "coder" + namespace: coder +subjects: + - kind: ServiceAccount + name: "coder" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coder-workspace-perms +--- +# Source: coder/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: coder + namespace: coder + labels: + helm.sh/chart: coder-0.1.0 + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: "0.1.0" + app.kubernetes.io/managed-by: Helm + annotations: + {} +spec: + type: NodePort + sessionAffinity: None + ports: + - name: "http" + port: 80 + targetPort: "http" + protocol: TCP + nodePort: + selector: + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name +--- +# Source: coder/templates/coder.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: coder + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: app.kubernetes.io/instance + operator: In + values: + - coder + topologyKey: kubernetes.io/hostname + weight: 1 + containers: + - args: + - server + command: + - /opt/coder + env: + - name: CODER_HTTP_ADDRESS + value: 0.0.0.0:8080 + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_ACCESS_URL + value: http://coder.coder.svc.cluster.local + - name: KUBE_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: CODER_DERP_SERVER_RELAY_URL + value: http://$(KUBE_POD_IP):8080 + - name: CODER_PROMETHEUS_ENABLE + value: "true" + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + livenessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + name: coder + ports: + - containerPort: 8080 + name: http + protocol: TCP + - containerPort: 2112 + name: prometheus-http + protocol: TCP + readinessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + resources: {} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: coder + terminationGracePeriodSeconds: 60 + volumes: [] diff --git a/helm/coder/tests/testdata/provisionerd_psk.golden b/helm/coder/tests/testdata/provisionerd_psk.golden index 45fb6c89fb18d..45a61be4f36ee 100644 --- a/helm/coder/tests/testdata/provisionerd_psk.golden +++ b/helm/coder/tests/testdata/provisionerd_psk.golden @@ -12,12 +12,14 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default --- # Source: coder/templates/rbac.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: coder-workspace-perms + namespace: default rules: - apiGroups: [""] resources: ["pods"] @@ -60,6 +62,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: "coder" + namespace: default subjects: - kind: ServiceAccount name: "coder" @@ -73,6 +76,7 @@ apiVersion: v1 kind: Service metadata: name: coder + namespace: default labels: helm.sh/chart: coder-0.1.0 app.kubernetes.io/name: coder @@ -109,6 +113,7 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default spec: replicas: 1 selector: diff --git a/helm/coder/tests/testdata/provisionerd_psk_coder.golden b/helm/coder/tests/testdata/provisionerd_psk_coder.golden new file mode 100644 index 0000000000000..55af7c3ee239b --- /dev/null +++ b/helm/coder/tests/testdata/provisionerd_psk_coder.golden @@ -0,0 +1,200 @@ +--- +# Source: coder/templates/coder.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder-workspace-perms + namespace: coder +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "coder" + namespace: coder +subjects: + - kind: ServiceAccount + name: "coder" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coder-workspace-perms +--- +# Source: coder/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: coder + namespace: coder + labels: + helm.sh/chart: coder-0.1.0 + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: "0.1.0" + app.kubernetes.io/managed-by: Helm + annotations: + {} +spec: + type: LoadBalancer + sessionAffinity: None + ports: + - name: "http" + port: 80 + targetPort: "http" + protocol: TCP + nodePort: + externalTrafficPolicy: "Cluster" + selector: + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name +--- +# Source: coder/templates/coder.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: coder + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: app.kubernetes.io/instance + operator: In + values: + - coder + topologyKey: kubernetes.io/hostname + weight: 1 + containers: + - args: + - server + command: + - /opt/coder + env: + - name: CODER_HTTP_ADDRESS + value: 0.0.0.0:8080 + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_PROVISIONER_DAEMON_PSK + valueFrom: + secretKeyRef: + key: psk + name: coder-provisionerd-psk + - name: CODER_ACCESS_URL + value: http://coder.coder.svc.cluster.local + - name: KUBE_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: CODER_DERP_SERVER_RELAY_URL + value: http://$(KUBE_POD_IP):8080 + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + livenessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + name: coder + ports: + - containerPort: 8080 + name: http + protocol: TCP + readinessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + resources: {} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: coder + terminationGracePeriodSeconds: 60 + volumes: [] diff --git a/helm/coder/tests/testdata/sa.golden b/helm/coder/tests/testdata/sa.golden index 86825a4621797..33fb3fc5c56c3 100644 --- a/helm/coder/tests/testdata/sa.golden +++ b/helm/coder/tests/testdata/sa.golden @@ -13,12 +13,14 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder-service-account + namespace: default --- # Source: coder/templates/rbac.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: coder-service-account-workspace-perms + namespace: default rules: - apiGroups: [""] resources: ["pods"] @@ -61,6 +63,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: "coder-service-account" + namespace: default subjects: - kind: ServiceAccount name: "coder-service-account" @@ -74,6 +77,7 @@ apiVersion: v1 kind: Service metadata: name: coder + namespace: default labels: helm.sh/chart: coder-0.1.0 app.kubernetes.io/name: coder @@ -110,6 +114,7 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default spec: replicas: 1 selector: diff --git a/helm/coder/tests/testdata/sa_coder.golden b/helm/coder/tests/testdata/sa_coder.golden new file mode 100644 index 0000000000000..c13b66550941b --- /dev/null +++ b/helm/coder/tests/testdata/sa_coder.golden @@ -0,0 +1,196 @@ +--- +# Source: coder/templates/coder.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: + eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/coder-service-account + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder-service-account + namespace: coder +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder-service-account-workspace-perms + namespace: coder +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "coder-service-account" + namespace: coder +subjects: + - kind: ServiceAccount + name: "coder-service-account" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coder-service-account-workspace-perms +--- +# Source: coder/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: coder + namespace: coder + labels: + helm.sh/chart: coder-0.1.0 + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: "0.1.0" + app.kubernetes.io/managed-by: Helm + annotations: + {} +spec: + type: LoadBalancer + sessionAffinity: None + ports: + - name: "http" + port: 80 + targetPort: "http" + protocol: TCP + nodePort: + externalTrafficPolicy: "Cluster" + selector: + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name +--- +# Source: coder/templates/coder.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: coder + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: app.kubernetes.io/instance + operator: In + values: + - coder + topologyKey: kubernetes.io/hostname + weight: 1 + containers: + - args: + - server + command: + - /opt/coder + env: + - name: CODER_HTTP_ADDRESS + value: 0.0.0.0:8080 + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_ACCESS_URL + value: http://coder.coder.svc.cluster.local + - name: KUBE_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: CODER_DERP_SERVER_RELAY_URL + value: http://$(KUBE_POD_IP):8080 + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + livenessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + name: coder + ports: + - containerPort: 8080 + name: http + protocol: TCP + readinessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + resources: {} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: coder-service-account + terminationGracePeriodSeconds: 60 + volumes: [] diff --git a/helm/coder/tests/testdata/sa_disabled.golden b/helm/coder/tests/testdata/sa_disabled.golden index dbdbc0dc8f090..411ad26fdd8a8 100644 --- a/helm/coder/tests/testdata/sa_disabled.golden +++ b/helm/coder/tests/testdata/sa_disabled.golden @@ -4,6 +4,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: coder-workspace-perms + namespace: default rules: - apiGroups: [""] resources: ["pods"] @@ -46,6 +47,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: "coder" + namespace: default subjects: - kind: ServiceAccount name: "coder" @@ -59,6 +61,7 @@ apiVersion: v1 kind: Service metadata: name: coder + namespace: default labels: helm.sh/chart: coder-0.1.0 app.kubernetes.io/name: coder @@ -96,6 +99,7 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default spec: replicas: 1 selector: diff --git a/helm/coder/tests/testdata/sa_disabled_coder.golden b/helm/coder/tests/testdata/sa_disabled_coder.golden new file mode 100644 index 0000000000000..2eebccf8bcaf1 --- /dev/null +++ b/helm/coder/tests/testdata/sa_disabled_coder.golden @@ -0,0 +1,181 @@ +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder-workspace-perms + namespace: coder +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "coder" + namespace: coder +subjects: + - kind: ServiceAccount + name: "coder" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coder-workspace-perms +--- +# Source: coder/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: coder + namespace: coder + labels: + helm.sh/chart: coder-0.1.0 + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: "0.1.0" + app.kubernetes.io/managed-by: Helm + annotations: + {} +spec: + type: LoadBalancer + sessionAffinity: None + ports: + - name: "http" + port: 80 + targetPort: "http" + protocol: TCP + nodePort: + externalTrafficPolicy: "Cluster" + selector: + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name +--- +# Source: coder/templates/coder.yaml +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: coder + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: app.kubernetes.io/instance + operator: In + values: + - coder + topologyKey: kubernetes.io/hostname + weight: 1 + containers: + - args: + - server + command: + - /opt/coder + env: + - name: CODER_HTTP_ADDRESS + value: 0.0.0.0:8080 + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_ACCESS_URL + value: http://coder.coder.svc.cluster.local + - name: KUBE_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: CODER_DERP_SERVER_RELAY_URL + value: http://$(KUBE_POD_IP):8080 + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + livenessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + name: coder + ports: + - containerPort: 8080 + name: http + protocol: TCP + readinessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + resources: {} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: coder + terminationGracePeriodSeconds: 60 + volumes: [] diff --git a/helm/coder/tests/testdata/sa_extra_rules.golden b/helm/coder/tests/testdata/sa_extra_rules.golden index a93252b339060..024b5f8054061 100644 --- a/helm/coder/tests/testdata/sa_extra_rules.golden +++ b/helm/coder/tests/testdata/sa_extra_rules.golden @@ -12,12 +12,14 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default --- # Source: coder/templates/rbac.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: coder-workspace-perms + namespace: default rules: - apiGroups: [""] resources: ["pods"] @@ -74,6 +76,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: "coder" + namespace: default subjects: - kind: ServiceAccount name: "coder" @@ -87,6 +90,7 @@ apiVersion: v1 kind: Service metadata: name: coder + namespace: default labels: helm.sh/chart: coder-0.1.0 app.kubernetes.io/name: coder @@ -123,6 +127,7 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default spec: replicas: 1 selector: diff --git a/helm/coder/tests/testdata/sa_extra_rules_coder.golden b/helm/coder/tests/testdata/sa_extra_rules_coder.golden new file mode 100644 index 0000000000000..a0791d15669da --- /dev/null +++ b/helm/coder/tests/testdata/sa_extra_rules_coder.golden @@ -0,0 +1,209 @@ +--- +# Source: coder/templates/coder.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder-workspace-perms + namespace: coder +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + + - apiGroups: + - "" + resources: + - services + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "coder" + namespace: coder +subjects: + - kind: ServiceAccount + name: "coder" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coder-workspace-perms +--- +# Source: coder/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: coder + namespace: coder + labels: + helm.sh/chart: coder-0.1.0 + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: "0.1.0" + app.kubernetes.io/managed-by: Helm + annotations: + {} +spec: + type: LoadBalancer + sessionAffinity: None + ports: + - name: "http" + port: 80 + targetPort: "http" + protocol: TCP + nodePort: + externalTrafficPolicy: "Cluster" + selector: + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name +--- +# Source: coder/templates/coder.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: coder + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: app.kubernetes.io/instance + operator: In + values: + - coder + topologyKey: kubernetes.io/hostname + weight: 1 + containers: + - args: + - server + command: + - /opt/coder + env: + - name: CODER_HTTP_ADDRESS + value: 0.0.0.0:8080 + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_ACCESS_URL + value: http://coder.coder.svc.cluster.local + - name: KUBE_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: CODER_DERP_SERVER_RELAY_URL + value: http://$(KUBE_POD_IP):8080 + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + livenessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + name: coder + ports: + - containerPort: 8080 + name: http + protocol: TCP + readinessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + resources: {} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: coder + terminationGracePeriodSeconds: 60 + volumes: [] diff --git a/helm/coder/tests/testdata/securitycontext.golden b/helm/coder/tests/testdata/securitycontext.golden new file mode 100644 index 0000000000000..27b928a31eec6 --- /dev/null +++ b/helm/coder/tests/testdata/securitycontext.golden @@ -0,0 +1,198 @@ +--- +# Source: coder/templates/coder.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: default +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder-workspace-perms + namespace: default +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "coder" + namespace: default +subjects: + - kind: ServiceAccount + name: "coder" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coder-workspace-perms +--- +# Source: coder/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: coder + namespace: default + labels: + helm.sh/chart: coder-0.1.0 + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: "0.1.0" + app.kubernetes.io/managed-by: Helm + annotations: + {} +spec: + type: LoadBalancer + sessionAffinity: None + ports: + - name: "http" + port: 80 + targetPort: "http" + protocol: TCP + nodePort: + externalTrafficPolicy: "Cluster" + selector: + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name +--- +# Source: coder/templates/coder.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: coder + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: app.kubernetes.io/instance + operator: In + values: + - coder + topologyKey: kubernetes.io/hostname + weight: 1 + containers: + - args: + - server + command: + - /opt/coder + env: + - name: CODER_HTTP_ADDRESS + value: 0.0.0.0:8080 + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_ACCESS_URL + value: http://coder.default.svc.cluster.local + - name: KUBE_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: CODER_DERP_SERVER_RELAY_URL + value: http://$(KUBE_POD_IP):8080 + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + livenessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + name: coder + ports: + - containerPort: 8080 + name: http + protocol: TCP + readinessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + resources: {} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: coder + terminationGracePeriodSeconds: 60 + volumes: [] diff --git a/helm/coder/tests/testdata/securitycontext.yaml b/helm/coder/tests/testdata/securitycontext.yaml new file mode 100644 index 0000000000000..bcc6594111c97 --- /dev/null +++ b/helm/coder/tests/testdata/securitycontext.yaml @@ -0,0 +1,8 @@ +coder: + image: + tag: latest + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL diff --git a/helm/coder/tests/testdata/securitycontext_coder.golden b/helm/coder/tests/testdata/securitycontext_coder.golden new file mode 100644 index 0000000000000..5ac24c6fcbd20 --- /dev/null +++ b/helm/coder/tests/testdata/securitycontext_coder.golden @@ -0,0 +1,198 @@ +--- +# Source: coder/templates/coder.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder-workspace-perms + namespace: coder +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "coder" + namespace: coder +subjects: + - kind: ServiceAccount + name: "coder" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coder-workspace-perms +--- +# Source: coder/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: coder + namespace: coder + labels: + helm.sh/chart: coder-0.1.0 + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: "0.1.0" + app.kubernetes.io/managed-by: Helm + annotations: + {} +spec: + type: LoadBalancer + sessionAffinity: None + ports: + - name: "http" + port: 80 + targetPort: "http" + protocol: TCP + nodePort: + externalTrafficPolicy: "Cluster" + selector: + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name +--- +# Source: coder/templates/coder.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: coder + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: app.kubernetes.io/instance + operator: In + values: + - coder + topologyKey: kubernetes.io/hostname + weight: 1 + containers: + - args: + - server + command: + - /opt/coder + env: + - name: CODER_HTTP_ADDRESS + value: 0.0.0.0:8080 + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_ACCESS_URL + value: http://coder.coder.svc.cluster.local + - name: KUBE_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: CODER_DERP_SERVER_RELAY_URL + value: http://$(KUBE_POD_IP):8080 + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + livenessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + name: coder + ports: + - containerPort: 8080 + name: http + protocol: TCP + readinessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + resources: {} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: coder + terminationGracePeriodSeconds: 60 + volumes: [] diff --git a/helm/coder/tests/testdata/svc_loadbalancer.golden b/helm/coder/tests/testdata/svc_loadbalancer.golden index bf089e859f8ce..5ed1bffeaa977 100644 --- a/helm/coder/tests/testdata/svc_loadbalancer.golden +++ b/helm/coder/tests/testdata/svc_loadbalancer.golden @@ -12,12 +12,14 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default --- # Source: coder/templates/rbac.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: coder-workspace-perms + namespace: default rules: - apiGroups: [""] resources: ["pods"] @@ -60,6 +62,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: "coder" + namespace: default subjects: - kind: ServiceAccount name: "coder" @@ -73,6 +76,7 @@ apiVersion: v1 kind: Service metadata: name: coder + namespace: default labels: helm.sh/chart: coder-0.1.0 app.kubernetes.io/name: coder @@ -109,6 +113,7 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default spec: replicas: 1 selector: diff --git a/helm/coder/tests/testdata/svc_loadbalancer_class.golden b/helm/coder/tests/testdata/svc_loadbalancer_class.golden index 0bb55dbd4246c..746227c1fe9e5 100644 --- a/helm/coder/tests/testdata/svc_loadbalancer_class.golden +++ b/helm/coder/tests/testdata/svc_loadbalancer_class.golden @@ -12,12 +12,14 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default --- # Source: coder/templates/rbac.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: coder-workspace-perms + namespace: default rules: - apiGroups: [""] resources: ["pods"] @@ -60,6 +62,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: "coder" + namespace: default subjects: - kind: ServiceAccount name: "coder" @@ -73,6 +76,7 @@ apiVersion: v1 kind: Service metadata: name: coder + namespace: default labels: helm.sh/chart: coder-0.1.0 app.kubernetes.io/name: coder @@ -110,6 +114,7 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default spec: replicas: 1 selector: diff --git a/helm/coder/tests/testdata/svc_loadbalancer_class_coder.golden b/helm/coder/tests/testdata/svc_loadbalancer_class_coder.golden new file mode 100644 index 0000000000000..ac35f941dc911 --- /dev/null +++ b/helm/coder/tests/testdata/svc_loadbalancer_class_coder.golden @@ -0,0 +1,196 @@ +--- +# Source: coder/templates/coder.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder-workspace-perms + namespace: coder +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "coder" + namespace: coder +subjects: + - kind: ServiceAccount + name: "coder" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coder-workspace-perms +--- +# Source: coder/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: coder + namespace: coder + labels: + helm.sh/chart: coder-0.1.0 + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: "0.1.0" + app.kubernetes.io/managed-by: Helm + annotations: + {} +spec: + type: LoadBalancer + sessionAffinity: None + ports: + - name: "http" + port: 80 + targetPort: "http" + protocol: TCP + nodePort: + externalTrafficPolicy: "Cluster" + loadBalancerClass: "test" + selector: + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name +--- +# Source: coder/templates/coder.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: coder + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: app.kubernetes.io/instance + operator: In + values: + - coder + topologyKey: kubernetes.io/hostname + weight: 1 + containers: + - args: + - server + command: + - /opt/coder + env: + - name: CODER_HTTP_ADDRESS + value: 0.0.0.0:8080 + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_ACCESS_URL + value: http://coder.coder.svc.cluster.local + - name: KUBE_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: CODER_DERP_SERVER_RELAY_URL + value: http://$(KUBE_POD_IP):8080 + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + livenessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + name: coder + ports: + - containerPort: 8080 + name: http + protocol: TCP + readinessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + resources: {} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: coder + terminationGracePeriodSeconds: 60 + volumes: [] diff --git a/helm/coder/tests/testdata/svc_loadbalancer_coder.golden b/helm/coder/tests/testdata/svc_loadbalancer_coder.golden new file mode 100644 index 0000000000000..0e7ff69fba962 --- /dev/null +++ b/helm/coder/tests/testdata/svc_loadbalancer_coder.golden @@ -0,0 +1,195 @@ +--- +# Source: coder/templates/coder.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder-workspace-perms + namespace: coder +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "coder" + namespace: coder +subjects: + - kind: ServiceAccount + name: "coder" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coder-workspace-perms +--- +# Source: coder/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: coder + namespace: coder + labels: + helm.sh/chart: coder-0.1.0 + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: "0.1.0" + app.kubernetes.io/managed-by: Helm + annotations: + {} +spec: + type: LoadBalancer + sessionAffinity: None + ports: + - name: "http" + port: 80 + targetPort: "http" + protocol: TCP + nodePort: 30080 + externalTrafficPolicy: "Cluster" + selector: + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name +--- +# Source: coder/templates/coder.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: coder + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: app.kubernetes.io/instance + operator: In + values: + - coder + topologyKey: kubernetes.io/hostname + weight: 1 + containers: + - args: + - server + command: + - /opt/coder + env: + - name: CODER_HTTP_ADDRESS + value: 0.0.0.0:8080 + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_ACCESS_URL + value: http://coder.coder.svc.cluster.local + - name: KUBE_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: CODER_DERP_SERVER_RELAY_URL + value: http://$(KUBE_POD_IP):8080 + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + livenessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + name: coder + ports: + - containerPort: 8080 + name: http + protocol: TCP + readinessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + resources: {} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: coder + terminationGracePeriodSeconds: 60 + volumes: [] diff --git a/helm/coder/tests/testdata/svc_nodeport.golden b/helm/coder/tests/testdata/svc_nodeport.golden index 90d63444c7c6c..c687bb43143a3 100644 --- a/helm/coder/tests/testdata/svc_nodeport.golden +++ b/helm/coder/tests/testdata/svc_nodeport.golden @@ -12,12 +12,14 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default --- # Source: coder/templates/rbac.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: coder-workspace-perms + namespace: default rules: - apiGroups: [""] resources: ["pods"] @@ -60,6 +62,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: "coder" + namespace: default subjects: - kind: ServiceAccount name: "coder" @@ -73,6 +76,7 @@ apiVersion: v1 kind: Service metadata: name: coder + namespace: default labels: helm.sh/chart: coder-0.1.0 app.kubernetes.io/name: coder @@ -108,6 +112,7 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default spec: replicas: 1 selector: diff --git a/helm/coder/tests/testdata/svc_nodeport_coder.golden b/helm/coder/tests/testdata/svc_nodeport_coder.golden new file mode 100644 index 0000000000000..685c90b35d4dd --- /dev/null +++ b/helm/coder/tests/testdata/svc_nodeport_coder.golden @@ -0,0 +1,194 @@ +--- +# Source: coder/templates/coder.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder-workspace-perms + namespace: coder +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "coder" + namespace: coder +subjects: + - kind: ServiceAccount + name: "coder" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coder-workspace-perms +--- +# Source: coder/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: coder + namespace: coder + labels: + helm.sh/chart: coder-0.1.0 + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: "0.1.0" + app.kubernetes.io/managed-by: Helm + annotations: + {} +spec: + type: NodePort + sessionAffinity: None + ports: + - name: "http" + port: 80 + targetPort: "http" + protocol: TCP + nodePort: 30080 + selector: + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name +--- +# Source: coder/templates/coder.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: coder + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: app.kubernetes.io/instance + operator: In + values: + - coder + topologyKey: kubernetes.io/hostname + weight: 1 + containers: + - args: + - server + command: + - /opt/coder + env: + - name: CODER_HTTP_ADDRESS + value: 0.0.0.0:8080 + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_ACCESS_URL + value: http://coder.coder.svc.cluster.local + - name: KUBE_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: CODER_DERP_SERVER_RELAY_URL + value: http://$(KUBE_POD_IP):8080 + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + livenessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + name: coder + ports: + - containerPort: 8080 + name: http + protocol: TCP + readinessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + resources: {} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: coder + terminationGracePeriodSeconds: 60 + volumes: [] diff --git a/helm/coder/tests/testdata/tls.golden b/helm/coder/tests/testdata/tls.golden index 17c99538f32a9..bce1cd1c74ce6 100644 --- a/helm/coder/tests/testdata/tls.golden +++ b/helm/coder/tests/testdata/tls.golden @@ -12,12 +12,14 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default --- # Source: coder/templates/rbac.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: coder-workspace-perms + namespace: default rules: - apiGroups: [""] resources: ["pods"] @@ -60,6 +62,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: "coder" + namespace: default subjects: - kind: ServiceAccount name: "coder" @@ -73,6 +76,7 @@ apiVersion: v1 kind: Service metadata: name: coder + namespace: default labels: helm.sh/chart: coder-0.1.0 app.kubernetes.io/name: coder @@ -114,6 +118,7 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default spec: replicas: 1 selector: diff --git a/helm/coder/tests/testdata/tls_coder.golden b/helm/coder/tests/testdata/tls_coder.golden new file mode 100644 index 0000000000000..a9eb138ad1576 --- /dev/null +++ b/helm/coder/tests/testdata/tls_coder.golden @@ -0,0 +1,217 @@ +--- +# Source: coder/templates/coder.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder-workspace-perms + namespace: coder +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "coder" + namespace: coder +subjects: + - kind: ServiceAccount + name: "coder" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coder-workspace-perms +--- +# Source: coder/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: coder + namespace: coder + labels: + helm.sh/chart: coder-0.1.0 + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: "0.1.0" + app.kubernetes.io/managed-by: Helm + annotations: + {} +spec: + type: LoadBalancer + sessionAffinity: None + ports: + - name: "http" + port: 80 + targetPort: "http" + protocol: TCP + nodePort: + - name: "https" + port: 443 + targetPort: "https" + protocol: TCP + nodePort: + externalTrafficPolicy: "Cluster" + selector: + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name +--- +# Source: coder/templates/coder.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: coder + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: app.kubernetes.io/instance + operator: In + values: + - coder + topologyKey: kubernetes.io/hostname + weight: 1 + containers: + - args: + - server + command: + - /opt/coder + env: + - name: CODER_HTTP_ADDRESS + value: 0.0.0.0:8080 + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_ACCESS_URL + value: https://coder.coder.svc.cluster.local + - name: KUBE_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: CODER_DERP_SERVER_RELAY_URL + value: http://$(KUBE_POD_IP):8080 + - name: CODER_TLS_ENABLE + value: "true" + - name: CODER_TLS_ADDRESS + value: 0.0.0.0:8443 + - name: CODER_TLS_CERT_FILE + value: /etc/ssl/certs/coder/coder-tls/tls.crt + - name: CODER_TLS_KEY_FILE + value: /etc/ssl/certs/coder/coder-tls/tls.key + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + livenessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + name: coder + ports: + - containerPort: 8080 + name: http + protocol: TCP + - containerPort: 8443 + name: https + protocol: TCP + readinessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + resources: {} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: + - mountPath: /etc/ssl/certs/coder/coder-tls + name: tls-coder-tls + readOnly: true + restartPolicy: Always + serviceAccountName: coder + terminationGracePeriodSeconds: 60 + volumes: + - name: tls-coder-tls + secret: + secretName: coder-tls diff --git a/helm/coder/tests/testdata/topology.golden b/helm/coder/tests/testdata/topology.golden index f1a5506fb04fc..648db931ab945 100644 --- a/helm/coder/tests/testdata/topology.golden +++ b/helm/coder/tests/testdata/topology.golden @@ -12,12 +12,14 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default --- # Source: coder/templates/rbac.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: coder-workspace-perms + namespace: default rules: - apiGroups: [""] resources: ["pods"] @@ -60,6 +62,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: "coder" + namespace: default subjects: - kind: ServiceAccount name: "coder" @@ -73,6 +76,7 @@ apiVersion: v1 kind: Service metadata: name: coder + namespace: default labels: helm.sh/chart: coder-0.1.0 app.kubernetes.io/name: coder @@ -109,6 +113,7 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default spec: replicas: 1 selector: diff --git a/helm/coder/tests/testdata/topology_coder.golden b/helm/coder/tests/testdata/topology_coder.golden new file mode 100644 index 0000000000000..1950d4d2fafdd --- /dev/null +++ b/helm/coder/tests/testdata/topology_coder.golden @@ -0,0 +1,202 @@ +--- +# Source: coder/templates/coder.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder-workspace-perms + namespace: coder +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "coder" + namespace: coder +subjects: + - kind: ServiceAccount + name: "coder" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coder-workspace-perms +--- +# Source: coder/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: coder + namespace: coder + labels: + helm.sh/chart: coder-0.1.0 + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: "0.1.0" + app.kubernetes.io/managed-by: Helm + annotations: + {} +spec: + type: LoadBalancer + sessionAffinity: None + ports: + - name: "http" + port: 80 + targetPort: "http" + protocol: TCP + nodePort: + externalTrafficPolicy: "Cluster" + selector: + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name +--- +# Source: coder/templates/coder.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: coder + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: app.kubernetes.io/instance + operator: In + values: + - coder + topologyKey: kubernetes.io/hostname + weight: 1 + containers: + - args: + - server + command: + - /opt/coder + env: + - name: CODER_HTTP_ADDRESS + value: 0.0.0.0:8080 + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_ACCESS_URL + value: http://coder.coder.svc.cluster.local + - name: KUBE_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: CODER_DERP_SERVER_RELAY_URL + value: http://$(KUBE_POD_IP):8080 + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + livenessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + name: coder + ports: + - containerPort: 8080 + name: http + protocol: TCP + readinessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + resources: {} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: coder + terminationGracePeriodSeconds: 60 + topologySpreadConstraints: + - labelSelector: + matchLabels: + app.kubernetes.io/instance: coder + maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + volumes: [] diff --git a/helm/coder/tests/testdata/workspace_proxy.golden b/helm/coder/tests/testdata/workspace_proxy.golden index 797bcae2716e9..7d380ac852666 100644 --- a/helm/coder/tests/testdata/workspace_proxy.golden +++ b/helm/coder/tests/testdata/workspace_proxy.golden @@ -12,12 +12,14 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default --- # Source: coder/templates/rbac.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: coder-workspace-perms + namespace: default rules: - apiGroups: [""] resources: ["pods"] @@ -60,6 +62,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: "coder" + namespace: default subjects: - kind: ServiceAccount name: "coder" @@ -73,6 +76,7 @@ apiVersion: v1 kind: Service metadata: name: coder + namespace: default labels: helm.sh/chart: coder-0.1.0 app.kubernetes.io/name: coder @@ -109,6 +113,7 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-0.1.0 name: coder + namespace: default spec: replicas: 1 selector: diff --git a/helm/coder/tests/testdata/workspace_proxy_coder.golden b/helm/coder/tests/testdata/workspace_proxy_coder.golden new file mode 100644 index 0000000000000..9907499027c79 --- /dev/null +++ b/helm/coder/tests/testdata/workspace_proxy_coder.golden @@ -0,0 +1,203 @@ +--- +# Source: coder/templates/coder.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder-workspace-perms + namespace: coder +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +--- +# Source: coder/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "coder" + namespace: coder +subjects: + - kind: ServiceAccount + name: "coder" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coder-workspace-perms +--- +# Source: coder/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: coder + namespace: coder + labels: + helm.sh/chart: coder-0.1.0 + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: "0.1.0" + app.kubernetes.io/managed-by: Helm + annotations: + {} +spec: + type: LoadBalancer + sessionAffinity: None + ports: + - name: "http" + port: 80 + targetPort: "http" + protocol: TCP + nodePort: + externalTrafficPolicy: "Cluster" + selector: + app.kubernetes.io/name: coder + app.kubernetes.io/instance: release-name +--- +# Source: coder/templates/coder.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + name: coder + namespace: coder +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: coder + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder + app.kubernetes.io/part-of: coder + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-0.1.0 + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: app.kubernetes.io/instance + operator: In + values: + - coder + topologyKey: kubernetes.io/hostname + weight: 1 + containers: + - args: + - wsproxy + - server + command: + - /opt/coder + env: + - name: CODER_HTTP_ADDRESS + value: 0.0.0.0:8080 + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_ACCESS_URL + value: http://coder.coder.svc.cluster.local + - name: KUBE_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: CODER_DERP_SERVER_RELAY_URL + value: http://$(KUBE_POD_IP):8080 + - name: CODER_PRIMARY_ACCESS_URL + value: https://dev.coder.com + - name: CODER_PROXY_SESSION_TOKEN + valueFrom: + secretKeyRef: + key: token + name: coder-workspace-proxy-session-token + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + livenessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + name: coder + ports: + - containerPort: 8080 + name: http + protocol: TCP + readinessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTP + resources: {} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: coder + terminationGracePeriodSeconds: 60 + volumes: [] diff --git a/helm/libcoder/templates/_coder.yaml b/helm/libcoder/templates/_coder.yaml index 183d85091f44a..5a0154ae0d420 100644 --- a/helm/libcoder/templates/_coder.yaml +++ b/helm/libcoder/templates/_coder.yaml @@ -3,6 +3,7 @@ apiVersion: apps/v1 kind: Deployment metadata: name: {{ include "coder.name" .}} + namespace: {{ .Release.Namespace }} labels: {{- include "coder.labels" . | nindent 4 }} {{- with .Values.coder.labels }} @@ -80,6 +81,7 @@ apiVersion: v1 kind: ServiceAccount metadata: name: {{ .Values.coder.serviceAccount.name | quote }} + namespace: {{ .Release.Namespace }} annotations: {{ toYaml .Values.coder.serviceAccount.annotations | nindent 4 }} labels: {{- include "coder.labels" . | nindent 4 }} diff --git a/helm/libcoder/templates/_rbac.yaml b/helm/libcoder/templates/_rbac.yaml index 1320c652c8a15..bfd7410e0610d 100644 --- a/helm/libcoder/templates/_rbac.yaml +++ b/helm/libcoder/templates/_rbac.yaml @@ -5,6 +5,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: {{ .Values.coder.serviceAccount.name }}-workspace-perms + namespace: {{ .Release.Namespace }} rules: - apiGroups: [""] resources: ["pods"] @@ -51,6 +52,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: {{ .Values.coder.serviceAccount.name | quote }} + namespace: {{ .Release.Namespace }} subjects: - kind: ServiceAccount name: {{ .Values.coder.serviceAccount.name | quote }} diff --git a/helm/provisioner/tests/chart_test.go b/helm/provisioner/tests/chart_test.go index 136e77f76a4ab..728e63d4b6d2f 100644 --- a/helm/provisioner/tests/chart_test.go +++ b/helm/provisioner/tests/chart_test.go @@ -23,6 +23,11 @@ import ( // updateGoldenFiles is a flag that can be set to update golden files. var updateGoldenFiles = flag.Bool("update", false, "Update golden files") +var namespaces = []string{ + "default", + "coder", +} + var testCases = []testCase{ { name: "default_values", @@ -94,6 +99,7 @@ var testCases = []testCase{ type testCase struct { name string // Name of the test case. This is used to control which values and golden file are used. + namespace string // Namespace is the name of the namespace the resources should be generated within expectedError string // Expected error from running `helm template`. } @@ -102,7 +108,11 @@ func (tc testCase) valuesFilePath() string { } func (tc testCase) goldenFilePath() string { - return filepath.Join("./testdata", tc.name+".golden") + if tc.namespace == "default" { + return filepath.Join("./testdata", tc.name+".golden") + } + + return filepath.Join("./testdata", tc.name+"_"+tc.namespace+".golden") } func TestRenderChart(t *testing.T) { @@ -124,35 +134,40 @@ func TestRenderChart(t *testing.T) { for _, tc := range testCases { tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() + for _, ns := range namespaces { + tc := tc + tc.namespace = ns - // Ensure that the values file exists. - valuesFilePath := tc.valuesFilePath() - if _, err := os.Stat(valuesFilePath); os.IsNotExist(err) { - t.Fatalf("values file %q does not exist", valuesFilePath) - } + t.Run(tc.namespace+"/"+tc.name, func(t *testing.T) { + t.Parallel() - // Run helm template with the values file. - templateOutput, err := runHelmTemplate(t, helmPath, "..", valuesFilePath) - if tc.expectedError != "" { - require.Error(t, err, "helm template should have failed") - require.Contains(t, templateOutput, tc.expectedError, "helm template output should contain expected error") - } else { - require.NoError(t, err, "helm template should not have failed") - require.NotEmpty(t, templateOutput, "helm template output should not be empty") - goldenFilePath := tc.goldenFilePath() - goldenBytes, err := os.ReadFile(goldenFilePath) - require.NoError(t, err, "failed to read golden file %q", goldenFilePath) - - // Remove carriage returns to make tests pass on Windows. - goldenBytes = bytes.Replace(goldenBytes, []byte("\r"), []byte(""), -1) - expected := string(goldenBytes) - - require.NoError(t, err, "failed to load golden file %q") - require.Equal(t, expected, templateOutput) - } - }) + // Ensure that the values file exists. + valuesFilePath := tc.valuesFilePath() + if _, err := os.Stat(valuesFilePath); os.IsNotExist(err) { + t.Fatalf("values file %q does not exist", valuesFilePath) + } + + // Run helm template with the values file. + templateOutput, err := runHelmTemplate(t, helmPath, "..", valuesFilePath, tc.namespace) + if tc.expectedError != "" { + require.Error(t, err, "helm template should have failed") + require.Contains(t, templateOutput, tc.expectedError, "helm template output should contain expected error") + } else { + require.NoError(t, err, "helm template should not have failed") + require.NotEmpty(t, templateOutput, "helm template output should not be empty") + goldenFilePath := tc.goldenFilePath() + goldenBytes, err := os.ReadFile(goldenFilePath) + require.NoError(t, err, "failed to read golden file %q", goldenFilePath) + + // Remove carriage returns to make tests pass on Windows. + goldenBytes = bytes.Replace(goldenBytes, []byte("\r"), []byte(""), -1) + expected := string(goldenBytes) + + require.NoError(t, err, "failed to load golden file %q") + require.Equal(t, expected, templateOutput) + } + }) + } } } @@ -167,22 +182,28 @@ func TestUpdateGoldenFiles(t *testing.T) { require.NoError(t, err, "failed to build Helm dependencies") for _, tc := range testCases { + tc := tc if tc.expectedError != "" { t.Logf("skipping test case %q with render error", tc.name) continue } - valuesPath := tc.valuesFilePath() - templateOutput, err := runHelmTemplate(t, helmPath, "..", valuesPath) - if err != nil { - t.Logf("error running `helm template -f %q`: %v", valuesPath, err) - t.Logf("output: %s", templateOutput) - } - require.NoError(t, err, "failed to run `helm template -f %q`", valuesPath) + for _, ns := range namespaces { + tc := tc + tc.namespace = ns - goldenFilePath := tc.goldenFilePath() - err = os.WriteFile(goldenFilePath, []byte(templateOutput), 0o644) // nolint:gosec - require.NoError(t, err, "failed to write golden file %q", goldenFilePath) + valuesPath := tc.valuesFilePath() + templateOutput, err := runHelmTemplate(t, helmPath, "..", valuesPath, tc.namespace) + if err != nil { + t.Logf("error running `helm template -f %q`: %v", valuesPath, err) + t.Logf("output: %s", templateOutput) + } + require.NoError(t, err, "failed to run `helm template -f %q`", valuesPath) + + goldenFilePath := tc.goldenFilePath() + err = os.WriteFile(goldenFilePath, []byte(templateOutput), 0o644) // nolint:gosec + require.NoError(t, err, "failed to write golden file %q", goldenFilePath) + } } t.Log("Golden files updated. Please review the changes and commit them.") } @@ -209,13 +230,13 @@ func updateHelmDependencies(t testing.TB, helmPath, chartDir string) error { // runHelmTemplate runs helm template on the given chart with the given values and // returns the raw output. -func runHelmTemplate(t testing.TB, helmPath, chartDir, valuesFilePath string) (string, error) { +func runHelmTemplate(t testing.TB, helmPath, chartDir, valuesFilePath, namespace string) (string, error) { // Ensure that valuesFilePath exists if _, err := os.Stat(valuesFilePath); err != nil { return "", xerrors.Errorf("values file %q does not exist: %w", valuesFilePath, err) } - cmd := exec.Command(helmPath, "template", chartDir, "-f", valuesFilePath, "--namespace", "default") + cmd := exec.Command(helmPath, "template", chartDir, "-f", valuesFilePath, "--namespace", namespace) t.Logf("exec command: %v", cmd.Args) out, err := cmd.CombinedOutput() return string(out), err diff --git a/helm/provisioner/tests/testdata/command.golden b/helm/provisioner/tests/testdata/command.golden index 39760332be082..86ee74fdee901 100644 --- a/helm/provisioner/tests/testdata/command.golden +++ b/helm/provisioner/tests/testdata/command.golden @@ -12,12 +12,14 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-provisioner-0.1.0 name: coder-provisioner + namespace: default --- # Source: coder-provisioner/templates/rbac.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: coder-provisioner-workspace-perms + namespace: default rules: - apiGroups: [""] resources: ["pods"] @@ -60,6 +62,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: "coder-provisioner" + namespace: default subjects: - kind: ServiceAccount name: "coder-provisioner" @@ -81,6 +84,7 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-provisioner-0.1.0 name: coder-provisioner + namespace: default spec: replicas: 1 selector: diff --git a/helm/provisioner/tests/testdata/command_args.golden b/helm/provisioner/tests/testdata/command_args.golden index 48162991f61eb..7d51f41b6b9af 100644 --- a/helm/provisioner/tests/testdata/command_args.golden +++ b/helm/provisioner/tests/testdata/command_args.golden @@ -12,12 +12,14 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-provisioner-0.1.0 name: coder-provisioner + namespace: default --- # Source: coder-provisioner/templates/rbac.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: coder-provisioner-workspace-perms + namespace: default rules: - apiGroups: [""] resources: ["pods"] @@ -60,6 +62,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: "coder-provisioner" + namespace: default subjects: - kind: ServiceAccount name: "coder-provisioner" @@ -81,6 +84,7 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-provisioner-0.1.0 name: coder-provisioner + namespace: default spec: replicas: 1 selector: diff --git a/helm/provisioner/tests/testdata/command_args_coder.golden b/helm/provisioner/tests/testdata/command_args_coder.golden new file mode 100644 index 0000000000000..30732650f8c41 --- /dev/null +++ b/helm/provisioner/tests/testdata/command_args_coder.golden @@ -0,0 +1,139 @@ +--- +# Source: coder-provisioner/templates/coder.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-provisioner + app.kubernetes.io/part-of: coder-provisioner + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-provisioner-0.1.0 + name: coder-provisioner + namespace: coder +--- +# Source: coder-provisioner/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder-provisioner-workspace-perms + namespace: coder +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +--- +# Source: coder-provisioner/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "coder-provisioner" + namespace: coder +subjects: + - kind: ServiceAccount + name: "coder-provisioner" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coder-provisioner-workspace-perms +--- +# Source: coder-provisioner/templates/coder.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-provisioner + app.kubernetes.io/part-of: coder-provisioner + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-provisioner-0.1.0 + name: coder-provisioner + namespace: coder +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: coder-provisioner + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-provisioner + app.kubernetes.io/part-of: coder-provisioner + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-provisioner-0.1.0 + spec: + containers: + - args: + - arg1 + - arg2 + command: + - /opt/coder + env: + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_PROVISIONER_DAEMON_PSK + valueFrom: + secretKeyRef: + key: psk + name: coder-provisioner-psk + - name: CODER_URL + value: http://coder.coder.svc.cluster.local + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + name: coder + ports: null + resources: {} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: coder-provisioner + terminationGracePeriodSeconds: 600 + volumes: [] diff --git a/helm/provisioner/tests/testdata/command_coder.golden b/helm/provisioner/tests/testdata/command_coder.golden new file mode 100644 index 0000000000000..c8b96ef938b45 --- /dev/null +++ b/helm/provisioner/tests/testdata/command_coder.golden @@ -0,0 +1,139 @@ +--- +# Source: coder-provisioner/templates/coder.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-provisioner + app.kubernetes.io/part-of: coder-provisioner + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-provisioner-0.1.0 + name: coder-provisioner + namespace: coder +--- +# Source: coder-provisioner/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder-provisioner-workspace-perms + namespace: coder +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +--- +# Source: coder-provisioner/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "coder-provisioner" + namespace: coder +subjects: + - kind: ServiceAccount + name: "coder-provisioner" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coder-provisioner-workspace-perms +--- +# Source: coder-provisioner/templates/coder.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-provisioner + app.kubernetes.io/part-of: coder-provisioner + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-provisioner-0.1.0 + name: coder-provisioner + namespace: coder +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: coder-provisioner + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-provisioner + app.kubernetes.io/part-of: coder-provisioner + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-provisioner-0.1.0 + spec: + containers: + - args: + - provisionerd + - start + command: + - /opt/colin + env: + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_PROVISIONER_DAEMON_PSK + valueFrom: + secretKeyRef: + key: psk + name: coder-provisioner-psk + - name: CODER_URL + value: http://coder.coder.svc.cluster.local + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + name: coder + ports: null + resources: {} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: coder-provisioner + terminationGracePeriodSeconds: 600 + volumes: [] diff --git a/helm/provisioner/tests/testdata/default_values.golden b/helm/provisioner/tests/testdata/default_values.golden index 04197fca37468..b8d24ed93b1b7 100644 --- a/helm/provisioner/tests/testdata/default_values.golden +++ b/helm/provisioner/tests/testdata/default_values.golden @@ -12,12 +12,14 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-provisioner-0.1.0 name: coder-provisioner + namespace: default --- # Source: coder-provisioner/templates/rbac.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: coder-provisioner-workspace-perms + namespace: default rules: - apiGroups: [""] resources: ["pods"] @@ -60,6 +62,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: "coder-provisioner" + namespace: default subjects: - kind: ServiceAccount name: "coder-provisioner" @@ -81,6 +84,7 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-provisioner-0.1.0 name: coder-provisioner + namespace: default spec: replicas: 1 selector: diff --git a/helm/provisioner/tests/testdata/default_values_coder.golden b/helm/provisioner/tests/testdata/default_values_coder.golden new file mode 100644 index 0000000000000..2c9e22777eca8 --- /dev/null +++ b/helm/provisioner/tests/testdata/default_values_coder.golden @@ -0,0 +1,139 @@ +--- +# Source: coder-provisioner/templates/coder.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-provisioner + app.kubernetes.io/part-of: coder-provisioner + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-provisioner-0.1.0 + name: coder-provisioner + namespace: coder +--- +# Source: coder-provisioner/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder-provisioner-workspace-perms + namespace: coder +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +--- +# Source: coder-provisioner/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "coder-provisioner" + namespace: coder +subjects: + - kind: ServiceAccount + name: "coder-provisioner" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coder-provisioner-workspace-perms +--- +# Source: coder-provisioner/templates/coder.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-provisioner + app.kubernetes.io/part-of: coder-provisioner + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-provisioner-0.1.0 + name: coder-provisioner + namespace: coder +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: coder-provisioner + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-provisioner + app.kubernetes.io/part-of: coder-provisioner + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-provisioner-0.1.0 + spec: + containers: + - args: + - provisionerd + - start + command: + - /opt/coder + env: + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_PROVISIONER_DAEMON_PSK + valueFrom: + secretKeyRef: + key: psk + name: coder-provisioner-psk + - name: CODER_URL + value: http://coder.coder.svc.cluster.local + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + name: coder + ports: null + resources: {} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: coder-provisioner + terminationGracePeriodSeconds: 600 + volumes: [] diff --git a/helm/provisioner/tests/testdata/extra_templates.golden b/helm/provisioner/tests/testdata/extra_templates.golden index 73fd654dd7245..6f0ac71a1cf71 100644 --- a/helm/provisioner/tests/testdata/extra_templates.golden +++ b/helm/provisioner/tests/testdata/extra_templates.golden @@ -12,6 +12,7 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-provisioner-0.1.0 name: coder-provisioner + namespace: default --- # Source: coder-provisioner/templates/extra-templates.yaml apiVersion: v1 @@ -27,6 +28,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: coder-provisioner-workspace-perms + namespace: default rules: - apiGroups: [""] resources: ["pods"] @@ -69,6 +71,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: "coder-provisioner" + namespace: default subjects: - kind: ServiceAccount name: "coder-provisioner" @@ -90,6 +93,7 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-provisioner-0.1.0 name: coder-provisioner + namespace: default spec: replicas: 1 selector: diff --git a/helm/provisioner/tests/testdata/extra_templates_coder.golden b/helm/provisioner/tests/testdata/extra_templates_coder.golden new file mode 100644 index 0000000000000..805a314c7643e --- /dev/null +++ b/helm/provisioner/tests/testdata/extra_templates_coder.golden @@ -0,0 +1,148 @@ +--- +# Source: coder-provisioner/templates/coder.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-provisioner + app.kubernetes.io/part-of: coder-provisioner + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-provisioner-0.1.0 + name: coder-provisioner + namespace: coder +--- +# Source: coder-provisioner/templates/extra-templates.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: some-config + namespace: coder +data: + key: some-value +--- +# Source: coder-provisioner/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder-provisioner-workspace-perms + namespace: coder +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +--- +# Source: coder-provisioner/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "coder-provisioner" + namespace: coder +subjects: + - kind: ServiceAccount + name: "coder-provisioner" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coder-provisioner-workspace-perms +--- +# Source: coder-provisioner/templates/coder.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-provisioner + app.kubernetes.io/part-of: coder-provisioner + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-provisioner-0.1.0 + name: coder-provisioner + namespace: coder +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: coder-provisioner + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-provisioner + app.kubernetes.io/part-of: coder-provisioner + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-provisioner-0.1.0 + spec: + containers: + - args: + - provisionerd + - start + command: + - /opt/coder + env: + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_PROVISIONER_DAEMON_PSK + valueFrom: + secretKeyRef: + key: psk + name: coder-provisioner-psk + - name: CODER_URL + value: http://coder.coder.svc.cluster.local + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + name: coder + ports: null + resources: {} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: coder-provisioner + terminationGracePeriodSeconds: 600 + volumes: [] diff --git a/helm/provisioner/tests/testdata/labels_annotations.golden b/helm/provisioner/tests/testdata/labels_annotations.golden index 1c2d49d8c424c..262d9df2ce0fa 100644 --- a/helm/provisioner/tests/testdata/labels_annotations.golden +++ b/helm/provisioner/tests/testdata/labels_annotations.golden @@ -12,12 +12,14 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-provisioner-0.1.0 name: coder-provisioner + namespace: default --- # Source: coder-provisioner/templates/rbac.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: coder-provisioner-workspace-perms + namespace: default rules: - apiGroups: [""] resources: ["pods"] @@ -60,6 +62,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: "coder-provisioner" + namespace: default subjects: - kind: ServiceAccount name: "coder-provisioner" @@ -85,6 +88,7 @@ metadata: com.coder/label/foo: bar helm.sh/chart: coder-provisioner-0.1.0 name: coder-provisioner + namespace: default spec: replicas: 1 selector: diff --git a/helm/provisioner/tests/testdata/labels_annotations_coder.golden b/helm/provisioner/tests/testdata/labels_annotations_coder.golden new file mode 100644 index 0000000000000..23b4a43e1a392 --- /dev/null +++ b/helm/provisioner/tests/testdata/labels_annotations_coder.golden @@ -0,0 +1,147 @@ +--- +# Source: coder-provisioner/templates/coder.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-provisioner + app.kubernetes.io/part-of: coder-provisioner + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-provisioner-0.1.0 + name: coder-provisioner + namespace: coder +--- +# Source: coder-provisioner/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder-provisioner-workspace-perms + namespace: coder +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +--- +# Source: coder-provisioner/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "coder-provisioner" + namespace: coder +subjects: + - kind: ServiceAccount + name: "coder-provisioner" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coder-provisioner-workspace-perms +--- +# Source: coder-provisioner/templates/coder.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + com.coder/annotation/baz: qux + com.coder/annotation/foo: bar + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-provisioner + app.kubernetes.io/part-of: coder-provisioner + app.kubernetes.io/version: 0.1.0 + com.coder/label/baz: qux + com.coder/label/foo: bar + helm.sh/chart: coder-provisioner-0.1.0 + name: coder-provisioner + namespace: coder +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: coder-provisioner + template: + metadata: + annotations: + com.coder/podAnnotation/baz: qux + com.coder/podAnnotation/foo: bar + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-provisioner + app.kubernetes.io/part-of: coder-provisioner + app.kubernetes.io/version: 0.1.0 + com.coder/podLabel/baz: qux + com.coder/podLabel/foo: bar + helm.sh/chart: coder-provisioner-0.1.0 + spec: + containers: + - args: + - provisionerd + - start + command: + - /opt/coder + env: + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_PROVISIONER_DAEMON_PSK + valueFrom: + secretKeyRef: + key: psk + name: coder-provisioner-psk + - name: CODER_URL + value: http://coder.coder.svc.cluster.local + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + name: coder + ports: null + resources: {} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: coder-provisioner + terminationGracePeriodSeconds: 600 + volumes: [] diff --git a/helm/provisioner/tests/testdata/name_override.golden b/helm/provisioner/tests/testdata/name_override.golden index 8f828d73d201a..6f35952422029 100644 --- a/helm/provisioner/tests/testdata/name_override.golden +++ b/helm/provisioner/tests/testdata/name_override.golden @@ -12,6 +12,7 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-provisioner-0.1.0 name: other-coder-provisioner + namespace: default --- # Source: coder-provisioner/templates/extra-templates.yaml apiVersion: v1 @@ -27,6 +28,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: other-coder-provisioner-workspace-perms + namespace: default rules: - apiGroups: [""] resources: ["pods"] @@ -69,6 +71,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: "other-coder-provisioner" + namespace: default subjects: - kind: ServiceAccount name: "other-coder-provisioner" @@ -90,6 +93,7 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-provisioner-0.1.0 name: other-coder-provisioner + namespace: default spec: replicas: 1 selector: diff --git a/helm/provisioner/tests/testdata/name_override_coder.golden b/helm/provisioner/tests/testdata/name_override_coder.golden new file mode 100644 index 0000000000000..c70058bafa4c0 --- /dev/null +++ b/helm/provisioner/tests/testdata/name_override_coder.golden @@ -0,0 +1,148 @@ +--- +# Source: coder-provisioner/templates/coder.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: other-coder-provisioner + app.kubernetes.io/part-of: other-coder-provisioner + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-provisioner-0.1.0 + name: other-coder-provisioner + namespace: coder +--- +# Source: coder-provisioner/templates/extra-templates.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: some-config + namespace: coder +data: + key: some-value +--- +# Source: coder-provisioner/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: other-coder-provisioner-workspace-perms + namespace: coder +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +--- +# Source: coder-provisioner/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "other-coder-provisioner" + namespace: coder +subjects: + - kind: ServiceAccount + name: "other-coder-provisioner" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: other-coder-provisioner-workspace-perms +--- +# Source: coder-provisioner/templates/coder.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: other-coder-provisioner + app.kubernetes.io/part-of: other-coder-provisioner + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-provisioner-0.1.0 + name: other-coder-provisioner + namespace: coder +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: other-coder-provisioner + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: other-coder-provisioner + app.kubernetes.io/part-of: other-coder-provisioner + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-provisioner-0.1.0 + spec: + containers: + - args: + - provisionerd + - start + command: + - /opt/coder + env: + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_PROVISIONER_DAEMON_PSK + valueFrom: + secretKeyRef: + key: psk + name: coder-provisioner-psk + - name: CODER_URL + value: http://coder.coder.svc.cluster.local + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + name: coder + ports: null + resources: {} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: other-coder-provisioner + terminationGracePeriodSeconds: 600 + volumes: [] diff --git a/helm/provisioner/tests/testdata/name_override_existing_sa.golden b/helm/provisioner/tests/testdata/name_override_existing_sa.golden index 8fd4790f6170b..8d2c3da52865b 100644 --- a/helm/provisioner/tests/testdata/name_override_existing_sa.golden +++ b/helm/provisioner/tests/testdata/name_override_existing_sa.golden @@ -13,6 +13,7 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-provisioner-0.1.0 name: other-coder-provisioner + namespace: default spec: replicas: 1 selector: diff --git a/helm/provisioner/tests/testdata/name_override_existing_sa_coder.golden b/helm/provisioner/tests/testdata/name_override_existing_sa_coder.golden new file mode 100644 index 0000000000000..112d117e86ef0 --- /dev/null +++ b/helm/provisioner/tests/testdata/name_override_existing_sa_coder.golden @@ -0,0 +1,68 @@ +--- +# Source: coder-provisioner/templates/coder.yaml +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: other-coder-provisioner + app.kubernetes.io/part-of: other-coder-provisioner + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-provisioner-0.1.0 + name: other-coder-provisioner + namespace: coder +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: other-coder-provisioner + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: other-coder-provisioner + app.kubernetes.io/part-of: other-coder-provisioner + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-provisioner-0.1.0 + spec: + containers: + - args: + - provisionerd + - start + command: + - /opt/coder + env: + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_PROVISIONER_DAEMON_PSK + valueFrom: + secretKeyRef: + key: psk + name: coder-provisioner-psk + - name: CODER_URL + value: http://coder.coder.svc.cluster.local + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + name: coder + ports: null + resources: {} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: existing-coder-provisioner-serviceaccount + terminationGracePeriodSeconds: 600 + volumes: [] diff --git a/helm/provisioner/tests/testdata/provisionerd_key.golden b/helm/provisioner/tests/testdata/provisionerd_key.golden index c4c23ec6da2a3..73421e9240006 100644 --- a/helm/provisioner/tests/testdata/provisionerd_key.golden +++ b/helm/provisioner/tests/testdata/provisionerd_key.golden @@ -12,12 +12,14 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-provisioner-0.1.0 name: coder-provisioner + namespace: default --- # Source: coder-provisioner/templates/rbac.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: coder-provisioner-workspace-perms + namespace: default rules: - apiGroups: [""] resources: ["pods"] @@ -60,6 +62,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: "coder-provisioner" + namespace: default subjects: - kind: ServiceAccount name: "coder-provisioner" @@ -81,6 +84,7 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-provisioner-0.1.0 name: coder-provisioner + namespace: default spec: replicas: 1 selector: diff --git a/helm/provisioner/tests/testdata/provisionerd_key_coder.golden b/helm/provisioner/tests/testdata/provisionerd_key_coder.golden new file mode 100644 index 0000000000000..03e347b284a9e --- /dev/null +++ b/helm/provisioner/tests/testdata/provisionerd_key_coder.golden @@ -0,0 +1,139 @@ +--- +# Source: coder-provisioner/templates/coder.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-provisioner + app.kubernetes.io/part-of: coder-provisioner + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-provisioner-0.1.0 + name: coder-provisioner + namespace: coder +--- +# Source: coder-provisioner/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder-provisioner-workspace-perms + namespace: coder +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +--- +# Source: coder-provisioner/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "coder-provisioner" + namespace: coder +subjects: + - kind: ServiceAccount + name: "coder-provisioner" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coder-provisioner-workspace-perms +--- +# Source: coder-provisioner/templates/coder.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-provisioner + app.kubernetes.io/part-of: coder-provisioner + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-provisioner-0.1.0 + name: coder-provisioner + namespace: coder +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: coder-provisioner + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-provisioner + app.kubernetes.io/part-of: coder-provisioner + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-provisioner-0.1.0 + spec: + containers: + - args: + - provisionerd + - start + command: + - /opt/coder + env: + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_PROVISIONER_DAEMON_KEY + valueFrom: + secretKeyRef: + key: provisionerd-key + name: coder-provisionerd-key + - name: CODER_URL + value: http://coder.coder.svc.cluster.local + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + name: coder + ports: null + resources: {} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: coder-provisioner + terminationGracePeriodSeconds: 600 + volumes: [] diff --git a/helm/provisioner/tests/testdata/provisionerd_key_psk_empty_workaround.golden b/helm/provisioner/tests/testdata/provisionerd_key_psk_empty_workaround.golden index c4c23ec6da2a3..73421e9240006 100644 --- a/helm/provisioner/tests/testdata/provisionerd_key_psk_empty_workaround.golden +++ b/helm/provisioner/tests/testdata/provisionerd_key_psk_empty_workaround.golden @@ -12,12 +12,14 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-provisioner-0.1.0 name: coder-provisioner + namespace: default --- # Source: coder-provisioner/templates/rbac.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: coder-provisioner-workspace-perms + namespace: default rules: - apiGroups: [""] resources: ["pods"] @@ -60,6 +62,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: "coder-provisioner" + namespace: default subjects: - kind: ServiceAccount name: "coder-provisioner" @@ -81,6 +84,7 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-provisioner-0.1.0 name: coder-provisioner + namespace: default spec: replicas: 1 selector: diff --git a/helm/provisioner/tests/testdata/provisionerd_key_psk_empty_workaround_coder.golden b/helm/provisioner/tests/testdata/provisionerd_key_psk_empty_workaround_coder.golden new file mode 100644 index 0000000000000..03e347b284a9e --- /dev/null +++ b/helm/provisioner/tests/testdata/provisionerd_key_psk_empty_workaround_coder.golden @@ -0,0 +1,139 @@ +--- +# Source: coder-provisioner/templates/coder.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-provisioner + app.kubernetes.io/part-of: coder-provisioner + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-provisioner-0.1.0 + name: coder-provisioner + namespace: coder +--- +# Source: coder-provisioner/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder-provisioner-workspace-perms + namespace: coder +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +--- +# Source: coder-provisioner/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "coder-provisioner" + namespace: coder +subjects: + - kind: ServiceAccount + name: "coder-provisioner" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coder-provisioner-workspace-perms +--- +# Source: coder-provisioner/templates/coder.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-provisioner + app.kubernetes.io/part-of: coder-provisioner + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-provisioner-0.1.0 + name: coder-provisioner + namespace: coder +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: coder-provisioner + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-provisioner + app.kubernetes.io/part-of: coder-provisioner + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-provisioner-0.1.0 + spec: + containers: + - args: + - provisionerd + - start + command: + - /opt/coder + env: + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_PROVISIONER_DAEMON_KEY + valueFrom: + secretKeyRef: + key: provisionerd-key + name: coder-provisionerd-key + - name: CODER_URL + value: http://coder.coder.svc.cluster.local + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + name: coder + ports: null + resources: {} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: coder-provisioner + terminationGracePeriodSeconds: 600 + volumes: [] diff --git a/helm/provisioner/tests/testdata/provisionerd_psk.golden b/helm/provisioner/tests/testdata/provisionerd_psk.golden index c1d9421c3c9dd..8b9ea878b56c6 100644 --- a/helm/provisioner/tests/testdata/provisionerd_psk.golden +++ b/helm/provisioner/tests/testdata/provisionerd_psk.golden @@ -12,12 +12,14 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-provisioner-0.1.0 name: coder-provisioner + namespace: default --- # Source: coder-provisioner/templates/rbac.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: coder-provisioner-workspace-perms + namespace: default rules: - apiGroups: [""] resources: ["pods"] @@ -60,6 +62,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: "coder-provisioner" + namespace: default subjects: - kind: ServiceAccount name: "coder-provisioner" @@ -81,6 +84,7 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-provisioner-0.1.0 name: coder-provisioner + namespace: default spec: replicas: 1 selector: diff --git a/helm/provisioner/tests/testdata/provisionerd_psk_coder.golden b/helm/provisioner/tests/testdata/provisionerd_psk_coder.golden new file mode 100644 index 0000000000000..61a8c7a0c1c95 --- /dev/null +++ b/helm/provisioner/tests/testdata/provisionerd_psk_coder.golden @@ -0,0 +1,141 @@ +--- +# Source: coder-provisioner/templates/coder.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-provisioner + app.kubernetes.io/part-of: coder-provisioner + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-provisioner-0.1.0 + name: coder-provisioner + namespace: coder +--- +# Source: coder-provisioner/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder-provisioner-workspace-perms + namespace: coder +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +--- +# Source: coder-provisioner/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "coder-provisioner" + namespace: coder +subjects: + - kind: ServiceAccount + name: "coder-provisioner" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coder-provisioner-workspace-perms +--- +# Source: coder-provisioner/templates/coder.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-provisioner + app.kubernetes.io/part-of: coder-provisioner + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-provisioner-0.1.0 + name: coder-provisioner + namespace: coder +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: coder-provisioner + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-provisioner + app.kubernetes.io/part-of: coder-provisioner + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-provisioner-0.1.0 + spec: + containers: + - args: + - provisionerd + - start + command: + - /opt/coder + env: + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_PROVISIONER_DAEMON_PSK + valueFrom: + secretKeyRef: + key: psk + name: not-the-default-coder-provisioner-psk + - name: CODER_PROVISIONERD_TAGS + value: clusterType=k8s,location=auh + - name: CODER_URL + value: http://coder.coder.svc.cluster.local + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + name: coder + ports: null + resources: {} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: coder-provisioner + terminationGracePeriodSeconds: 600 + volumes: [] diff --git a/helm/provisioner/tests/testdata/sa.golden b/helm/provisioner/tests/testdata/sa.golden index e8f6ee3bd45dd..6f836c593b445 100644 --- a/helm/provisioner/tests/testdata/sa.golden +++ b/helm/provisioner/tests/testdata/sa.golden @@ -13,12 +13,14 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-provisioner-0.1.0 name: coder-service-account + namespace: default --- # Source: coder-provisioner/templates/rbac.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: coder-service-account-workspace-perms + namespace: default rules: - apiGroups: [""] resources: ["pods"] @@ -61,6 +63,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: "coder-service-account" + namespace: default subjects: - kind: ServiceAccount name: "coder-service-account" @@ -82,6 +85,7 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-provisioner-0.1.0 name: coder-provisioner + namespace: default spec: replicas: 1 selector: diff --git a/helm/provisioner/tests/testdata/sa_coder.golden b/helm/provisioner/tests/testdata/sa_coder.golden new file mode 100644 index 0000000000000..97650df0e5e65 --- /dev/null +++ b/helm/provisioner/tests/testdata/sa_coder.golden @@ -0,0 +1,140 @@ +--- +# Source: coder-provisioner/templates/coder.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: + eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/coder-service-account + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-provisioner + app.kubernetes.io/part-of: coder-provisioner + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-provisioner-0.1.0 + name: coder-service-account + namespace: coder +--- +# Source: coder-provisioner/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder-service-account-workspace-perms + namespace: coder +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +--- +# Source: coder-provisioner/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "coder-service-account" + namespace: coder +subjects: + - kind: ServiceAccount + name: "coder-service-account" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coder-service-account-workspace-perms +--- +# Source: coder-provisioner/templates/coder.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-provisioner + app.kubernetes.io/part-of: coder-provisioner + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-provisioner-0.1.0 + name: coder-provisioner + namespace: coder +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: coder-provisioner + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-provisioner + app.kubernetes.io/part-of: coder-provisioner + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-provisioner-0.1.0 + spec: + containers: + - args: + - provisionerd + - start + command: + - /opt/coder + env: + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_PROVISIONER_DAEMON_PSK + valueFrom: + secretKeyRef: + key: psk + name: coder-provisioner-psk + - name: CODER_URL + value: http://coder.coder.svc.cluster.local + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + name: coder + ports: null + resources: {} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: coder-service-account + terminationGracePeriodSeconds: 600 + volumes: [] diff --git a/helm/provisioner/tests/testdata/sa_disabled.golden b/helm/provisioner/tests/testdata/sa_disabled.golden index 583bbe707c502..f403daa33a0df 100644 --- a/helm/provisioner/tests/testdata/sa_disabled.golden +++ b/helm/provisioner/tests/testdata/sa_disabled.golden @@ -13,6 +13,7 @@ metadata: app.kubernetes.io/version: 0.1.0 helm.sh/chart: coder-provisioner-0.1.0 name: coder-provisioner + namespace: default spec: replicas: 1 selector: diff --git a/helm/provisioner/tests/testdata/sa_disabled_coder.golden b/helm/provisioner/tests/testdata/sa_disabled_coder.golden new file mode 100644 index 0000000000000..5429858ca1d56 --- /dev/null +++ b/helm/provisioner/tests/testdata/sa_disabled_coder.golden @@ -0,0 +1,68 @@ +--- +# Source: coder-provisioner/templates/coder.yaml +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-provisioner + app.kubernetes.io/part-of: coder-provisioner + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-provisioner-0.1.0 + name: coder-provisioner + namespace: coder +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/name: coder-provisioner + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: release-name + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-provisioner + app.kubernetes.io/part-of: coder-provisioner + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-provisioner-0.1.0 + spec: + containers: + - args: + - provisionerd + - start + command: + - /opt/coder + env: + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_PROVISIONER_DAEMON_PSK + valueFrom: + secretKeyRef: + key: psk + name: coder-provisioner-psk + - name: CODER_URL + value: http://coder.coder.svc.cluster.local + image: ghcr.io/coder/coder:latest + imagePullPolicy: IfNotPresent + lifecycle: {} + name: coder + ports: null + resources: {} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: null + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: coder-provisioner + terminationGracePeriodSeconds: 600 + volumes: [] diff --git a/install.sh b/install.sh index 734fd3c44f320..931426c54c5db 100755 --- a/install.sh +++ b/install.sh @@ -273,7 +273,7 @@ EOF main() { MAINLINE=1 STABLE=0 - TERRAFORM_VERSION="1.9.8" + TERRAFORM_VERSION="1.10.5" if [ "${TRACE-}" ]; then set -x diff --git a/nix/docker.nix b/nix/docker.nix new file mode 100644 index 0000000000000..84c1a34e79bbe --- /dev/null +++ b/nix/docker.nix @@ -0,0 +1,392 @@ +# (ThomasK33): Inlined the relevant dockerTools functions, so that we can +# set the maxLayers attribute on the attribute set passed +# to the buildNixShellImage function. +# +# I'll create an upstream PR to nixpkgs with those changes, making this +# eventually unnecessary and ripe for removal. +{ + lib, + dockerTools, + devShellTools, + bashInteractive, + fakeNss, + runCommand, + writeShellScriptBin, + writeText, + writeTextFile, + writeTextDir, + cacert, + storeDir ? builtins.storeDir, + pigz, + zstd, + stdenv, + glibc, + sudo, +}: +let + inherit (lib) + optionalString + ; + + inherit (devShellTools) + valueToString + ; + + inherit (dockerTools) + streamLayeredImage + usrBinEnv + caCertificates + ; + + # This provides /bin/sh, pointing to bashInteractive. + # The use of bashInteractive here is intentional to support cases like `docker run -it `, so keep these use cases in mind if making any changes to how this works. + binSh = runCommand "bin-sh" { } '' + mkdir -p $out/bin + ln -s ${bashInteractive}/bin/bash $out/bin/sh + ln -s ${bashInteractive}/bin/bash $out/bin/bash + ''; + + etcNixConf = writeTextDir "etc/nix/nix.conf" '' + experimental-features = nix-command flakes + ''; + + etcReleaseName = writeTextDir "etc/coderniximage-release" '' + 0.0.0 + ''; + + etcPamdSudoFile = writeText "pam-sudo" '' + # Allow root to bypass authentication (optional) + auth sufficient pam_rootok.so + + # For all users, always allow auth + auth sufficient pam_permit.so + + # Do not perform any account management checks + account sufficient pam_permit.so + + # No password management here (only needed if you are changing passwords) + # password requisite pam_unix.so nullok yescrypt + + # Keep session logging if desired + session required pam_unix.so + ''; + + etcPamdSudo = runCommand "etc-pamd-sudo" { } '' + mkdir -p $out/etc/pam.d/ + ln -s ${etcPamdSudoFile} $out/etc/pam.d/sudo + ln -s ${etcPamdSudoFile} $out/etc/pam.d/su + ''; + + compressors = { + none = { + ext = ""; + nativeInputs = [ ]; + compress = "cat"; + decompress = "cat"; + }; + gz = { + ext = ".gz"; + nativeInputs = [ pigz ]; + compress = "pigz -p$NIX_BUILD_CORES -nTR"; + decompress = "pigz -d -p$NIX_BUILD_CORES"; + }; + zstd = { + ext = ".zst"; + nativeInputs = [ zstd ]; + compress = "zstd -T$NIX_BUILD_CORES"; + decompress = "zstd -d -T$NIX_BUILD_CORES"; + }; + }; + compressorForImage = + compressor: imageName: + compressors.${compressor} + or (throw "in docker image ${imageName}: compressor must be one of: [${toString builtins.attrNames compressors}]"); + + streamNixShellImage = + { + drv, + name ? drv.name + "-env", + tag ? null, + uid ? 1000, + gid ? 1000, + homeDirectory ? "/build", + shell ? bashInteractive + "/bin/bash", + command ? null, + run ? null, + maxLayers ? 100, + uname ? "nixbld", + }: + assert lib.assertMsg (!(drv.drvAttrs.__structuredAttrs or false)) + "streamNixShellImage: Does not work with the derivation ${drv.name} because it uses __structuredAttrs"; + assert lib.assertMsg ( + command == null || run == null + ) "streamNixShellImage: Can't specify both command and run"; + let + + # A binary that calls the command to build the derivation + builder = writeShellScriptBin "buildDerivation" '' + exec ${lib.escapeShellArg (valueToString drv.drvAttrs.builder)} ${lib.escapeShellArgs (map valueToString drv.drvAttrs.args)} + ''; + + staticPath = "${dirOf shell}:${ + lib.makeBinPath ( + (lib.flatten [ + builder + drv.buildInputs + ]) + ++ [ "/usr" ] + ) + }"; + + # https://github.com/NixOS/nix/blob/2.8.0/src/nix-build/nix-build.cc#L493-L526 + rcfile = writeText "nix-shell-rc" '' + unset PATH + dontAddDisableDepTrack=1 + # TODO: https://github.com/NixOS/nix/blob/2.8.0/src/nix-build/nix-build.cc#L506 + [ -e $stdenv/setup ] && source $stdenv/setup + PATH=${staticPath}:"$PATH" + SHELL=${lib.escapeShellArg shell} + BASH=${lib.escapeShellArg shell} + set +e + [ -n "$PS1" -a -z "$NIX_SHELL_PRESERVE_PROMPT" ] && PS1='\n\[\033[1;32m\][nix-shell:\w]\$\[\033[0m\] ' + if [ "$(type -t runHook)" = function ]; then + runHook shellHook + fi + unset NIX_ENFORCE_PURITY + shopt -u nullglob + shopt -s execfail + ${optionalString (command != null || run != null) '' + ${optionalString (command != null) command} + ${optionalString (run != null) run} + exit + ''} + ''; + + etcSudoers = writeTextDir "etc/sudoers" '' + root ALL=(ALL) ALL + ${toString uname} ALL=(ALL) NOPASSWD:ALL + ''; + + # Add our Docker init script + dockerInit = writeTextFile { + name = "initd-docker"; + destination = "/etc/init.d/docker"; + executable = true; + + text = '' + #!/usr/bin/env sh + ### BEGIN INIT INFO + # Provides: docker + # Required-Start: $remote_fs $syslog + # Required-Stop: $remote_fs $syslog + # Default-Start: 2 3 4 5 + # Default-Stop: 0 1 6 + # Short-Description: Start and stop Docker daemon + # Description: This script starts and stops the Docker daemon. + ### END INIT INFO + + case "$1" in + start) + echo "Starting dockerd" + SSL_CERT_FILE="${cacert}/etc/ssl/certs/ca-bundle.crt" dockerd --group=${toString gid} & + ;; + stop) + echo "Stopping dockerd" + killall dockerd + ;; + restart) + $0 stop + $0 start + ;; + *) + echo "Usage: $0 {start|stop|restart}" + exit 1 + ;; + esac + exit 0 + ''; + }; + + # https://github.com/NixOS/nix/blob/2.8.0/src/libstore/globals.hh#L464-L465 + sandboxBuildDir = "/build"; + + drvEnv = + devShellTools.unstructuredDerivationInputEnv { inherit (drv) drvAttrs; } + // devShellTools.derivationOutputEnv { + outputList = drv.outputs; + outputMap = drv; + }; + + # Environment variables set in the image + envVars = + { + + # Root certificates for internet access + SSL_CERT_FILE = "${cacert}/etc/ssl/certs/ca-bundle.crt"; + NIX_SSL_CERT_FILE = "${cacert}/etc/ssl/certs/ca-bundle.crt"; + + # https://github.com/NixOS/nix/blob/2.8.0/src/libstore/build/local-derivation-goal.cc#L1027-L1030 + # PATH = "/path-not-set"; + # Allows calling bash and `buildDerivation` as the Cmd + PATH = staticPath; + + # https://github.com/NixOS/nix/blob/2.8.0/src/libstore/build/local-derivation-goal.cc#L1032-L1038 + HOME = homeDirectory; + + # https://github.com/NixOS/nix/blob/2.8.0/src/libstore/build/local-derivation-goal.cc#L1040-L1044 + NIX_STORE = storeDir; + + # https://github.com/NixOS/nix/blob/2.8.0/src/libstore/build/local-derivation-goal.cc#L1046-L1047 + # TODO: Make configurable? + NIX_BUILD_CORES = "1"; + + # Make sure we get the libraries for C and C++ in. + LD_LIBRARY_PATH = lib.makeLibraryPath [ stdenv.cc.cc ]; + } + // drvEnv + // rec { + # https://github.com/NixOS/nix/blob/2.8.0/src/libstore/build/local-derivation-goal.cc#L1008-L1010 + NIX_BUILD_TOP = sandboxBuildDir; + + # https://github.com/NixOS/nix/blob/2.8.0/src/libstore/build/local-derivation-goal.cc#L1012-L1013 + TMPDIR = TMP; + TEMPDIR = TMP; + TMP = "/tmp"; + TEMP = TMP; + + # https://github.com/NixOS/nix/blob/2.8.0/src/libstore/build/local-derivation-goal.cc#L1015-L1019 + PWD = homeDirectory; + + # https://github.com/NixOS/nix/blob/2.8.0/src/libstore/build/local-derivation-goal.cc#L1071-L1074 + # We don't set it here because the output here isn't handled in any special way + # NIX_LOG_FD = "2"; + + # https://github.com/NixOS/nix/blob/2.8.0/src/libstore/build/local-derivation-goal.cc#L1076-L1077 + TERM = "xterm-256color"; + }; + + in + streamLayeredImage { + inherit name tag maxLayers; + contents = [ + binSh + usrBinEnv + caCertificates + etcNixConf + etcSudoers + etcPamdSudo + etcReleaseName + (fakeNss.override { + # Allows programs to look up the build user's home directory + # https://github.com/NixOS/nix/blob/ffe155abd36366a870482625543f9bf924a58281/src/libstore/build/local-derivation-goal.cc#L906-L910 + # Slightly differs however: We use the passed-in homeDirectory instead of sandboxBuildDir. + # We're doing this because it's arguably a bug in Nix that sandboxBuildDir is used here: https://github.com/NixOS/nix/issues/6379 + extraPasswdLines = [ + "${toString uname}:x:${toString uid}:${toString gid}:Build user:${homeDirectory}:${lib.escapeShellArg shell}" + ]; + extraGroupLines = [ + "${toString uname}:!:${toString gid}:" + "docker:!:${toString (builtins.sub gid 1)}:${toString uname}" + ]; + }) + dockerInit + ]; + + fakeRootCommands = '' + # Effectively a single-user installation of Nix, giving the user full + # control over the Nix store. Needed for building the derivation this + # shell is for, but also in case one wants to use Nix inside the + # image + mkdir -p ./nix/{store,var/nix} ./etc/nix + chown -R ${toString uid}:${toString gid} ./nix ./etc/nix + + # Gives the user control over the build directory + mkdir -p .${sandboxBuildDir} + chown -R ${toString uid}:${toString gid} .${sandboxBuildDir} + + mkdir -p .${homeDirectory} + chown -R ${toString uid}:${toString gid} .${homeDirectory} + + mkdir -p ./tmp + chown -R ${toString uid}:${toString gid} ./tmp + + mkdir -p ./etc/skel + chown -R ${toString uid}:${toString gid} ./etc/skel + + # Create traditional /lib or /lib64 as needed. + # For aarch64 (arm64): + if [ -e "${glibc}/lib/ld-linux-aarch64.so.1" ]; then + mkdir -p ./lib + ln -s "${glibc}/lib/ld-linux-aarch64.so.1" ./lib/ld-linux-aarch64.so.1 + fi + + # For x86_64: + if [ -e "${glibc}/lib64/ld-linux-x86-64.so.2" ]; then + mkdir -p ./lib64 + ln -s "${glibc}/lib64/ld-linux-x86-64.so.2" ./lib64/ld-linux-x86-64.so.2 + fi + + # Copy sudo from the Nix store to a "normal" path in the container + mkdir -p ./usr/bin + cp ${sudo}/bin/sudo ./usr/bin/sudo + + # Ensure root owns it & set setuid bit + chown 0:0 ./usr/bin/sudo + chmod 4755 ./usr/bin/sudo + + chown root:root ./etc/pam.d/sudo + chown root:root ./etc/pam.d/su + chown root:root ./etc/sudoers + + # Create /var/run and chown it so docker command + # doesnt encounter permission issues. + mkdir -p ./var/run/ + chown -R ${toString uid}:${toString gid} ./var/run/ + ''; + + # Run this image as the given uid/gid + config.User = "${toString uid}:${toString gid}"; + config.Cmd = + # https://github.com/NixOS/nix/blob/2.8.0/src/nix-build/nix-build.cc#L185-L186 + # https://github.com/NixOS/nix/blob/2.8.0/src/nix-build/nix-build.cc#L534-L536 + if run == null then + [ + shell + "--rcfile" + rcfile + ] + else + [ + shell + rcfile + ]; + config.WorkingDir = homeDirectory; + config.Env = lib.mapAttrsToList (name: value: "${name}=${value}") envVars; + }; +in +{ + inherit streamNixShellImage; + + # This function streams a docker image that behaves like a nix-shell for a derivation + # Docs: doc/build-helpers/images/dockertools.section.md + # Tests: nixos/tests/docker-tools-nix-shell.nix + + # Wrapper around streamNixShellImage to build an image from the result + # Docs: doc/build-helpers/images/dockertools.section.md + # Tests: nixos/tests/docker-tools-nix-shell.nix + buildNixShellImage = + { + drv, + compressor ? "gz", + ... + }@args: + let + stream = streamNixShellImage (builtins.removeAttrs args [ "compressor" ]); + compress = compressorForImage compressor drv.name; + in + runCommand "${drv.name}-env.tar${compress.ext}" { + inherit (stream) imageName; + passthru = { inherit (stream) imageTag; }; + nativeBuildInputs = compress.nativeInputs; + } "${stream} | ${compress.compress} > $out"; +} diff --git a/offlinedocs/package.json b/offlinedocs/package.json index 69c68e77813a4..243c0a1c220e5 100644 --- a/offlinedocs/package.json +++ b/offlinedocs/package.json @@ -30,7 +30,7 @@ "sanitize-html": "2.14.0" }, "devDependencies": { - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/node": "20.17.16", "@types/react": "18.3.12", "@types/react-dom": "18.3.1", diff --git a/offlinedocs/pnpm-lock.yaml b/offlinedocs/pnpm-lock.yaml index a4464cf82427c..5f51f11609def 100644 --- a/offlinedocs/pnpm-lock.yaml +++ b/offlinedocs/pnpm-lock.yaml @@ -55,8 +55,8 @@ importers: version: 2.14.0 devDependencies: '@types/lodash': - specifier: 4.17.14 - version: 4.17.14 + specifier: 4.17.15 + version: 4.17.15 '@types/node': specifier: 20.17.16 version: 20.17.16 @@ -407,8 +407,8 @@ packages: '@types/lodash.mergewith@4.6.9': resolution: {integrity: sha512-fgkoCAOF47K7sxrQ7Mlud2TH023itugZs2bUg8h/KzT+BnZNrR2jAOmaokbLunHNnobXVWOezAeNn/lZqwxkcw==} - '@types/lodash@4.17.14': - resolution: {integrity: sha512-jsxagdikDiDBeIRaPYtArcT8my4tN1og7MtMRquFT3XNA6axxyHDRUemqDz/taRDdOUn0GnGHRCuff4q48sW9A==} + '@types/lodash@4.17.15': + resolution: {integrity: sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==} '@types/mdast@4.0.3': resolution: {integrity: sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg==} @@ -2748,9 +2748,9 @@ snapshots: '@types/lodash.mergewith@4.6.9': dependencies: - '@types/lodash': 4.17.14 + '@types/lodash': 4.17.15 - '@types/lodash@4.17.14': {} + '@types/lodash@4.17.15': {} '@types/mdast@4.0.3': dependencies: diff --git a/provisioner/appslug.go b/provisioner/appslug.go deleted file mode 100644 index a13fa4eb2dc9e..0000000000000 --- a/provisioner/appslug.go +++ /dev/null @@ -1,13 +0,0 @@ -package provisioner - -import "regexp" - -// AppSlugRegex is the regex used to validate the slug of a coder_app -// resource. It must be a valid hostname and cannot contain two consecutive -// hyphens or start/end with a hyphen. -// -// This regex is duplicated in the terraform provider code, so make sure to -// update it there as well. -// -// There are test cases for this regex in appslug_test.go. -var AppSlugRegex = regexp.MustCompile(`^[a-z0-9](-?[a-z0-9])*$`) diff --git a/provisioner/appslug_test.go b/provisioner/appslug_test.go deleted file mode 100644 index f13f220e9c63c..0000000000000 --- a/provisioner/appslug_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package provisioner_test - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/coder/coder/v2/provisioner" -) - -func TestValidAppSlugRegex(t *testing.T) { - t.Parallel() - - t.Run("Valid", func(t *testing.T) { - t.Parallel() - - validStrings := []string{ - "a", - "1", - "a1", - "1a", - "1a1", - "1-1", - "a-a", - "ab-cd", - "ab-cd-ef", - "abc-123", - "a-123", - "abc-1", - "ab-c", - "a-bc", - } - - for _, s := range validStrings { - require.True(t, provisioner.AppSlugRegex.MatchString(s), s) - } - }) - - t.Run("Invalid", func(t *testing.T) { - t.Parallel() - - invalidStrings := []string{ - "", - "-", - "-abc", - "abc-", - "ab--cd", - "a--bc", - "ab--c", - "_", - "ab_cd", - "_abc", - "abc_", - " ", - "abc ", - " abc", - "ab cd", - } - - for _, s := range invalidStrings { - require.False(t, provisioner.AppSlugRegex.MatchString(s), s) - } - }) -} diff --git a/provisioner/regexes.go b/provisioner/regexes.go new file mode 100644 index 0000000000000..fe4db3e9e9e6a --- /dev/null +++ b/provisioner/regexes.go @@ -0,0 +1,31 @@ +package provisioner + +import "regexp" + +var ( + // AgentNameRegex is the regex used to validate the name of a coder_agent + // resource. It must be a valid hostname and cannot contain two consecutive + // hyphens or start/end with a hyphen. Uppercase characters ARE permitted, + // although duplicate agent names with different casing will be rejected. + // + // Previously, underscores were permitted, but this was changed in 2025-02. + // App URLs never supported underscores, and proxy requests to apps on + // agents with underscores in the name always failed. + // + // Due to terraform limitations, this cannot be validated at the provider + // level as resource names cannot be read from the provider API, so this is + // not duplicated in the terraform provider code. + // + // There are test cases for this regex in regexes_test.go. + AgentNameRegex = regexp.MustCompile(`(?i)^[a-z0-9](-?[a-z0-9])*$`) + + // AppSlugRegex is the regex used to validate the slug of a coder_app + // resource. It must be a valid hostname and cannot contain two consecutive + // hyphens or start/end with a hyphen. + // + // This regex is duplicated in the terraform provider code, so make sure to + // update it there as well. + // + // There are test cases for this regex in regexes_test.go. + AppSlugRegex = regexp.MustCompile(`^[a-z0-9](-?[a-z0-9])*$`) +) diff --git a/provisioner/regexes_test.go b/provisioner/regexes_test.go new file mode 100644 index 0000000000000..d8c69f9b67156 --- /dev/null +++ b/provisioner/regexes_test.go @@ -0,0 +1,88 @@ +package provisioner_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/provisioner" +) + +var ( + validStrings = []string{ + "a", + "1", + "a1", + "1a", + "1a1", + "1-1", + "a-a", + "ab-cd", + "ab-cd-ef", + "abc-123", + "a-123", + "abc-1", + "ab-c", + "a-bc", + } + + invalidStrings = []string{ + "", + "-", + "-abc", + "abc-", + "ab--cd", + "a--bc", + "ab--c", + "_", + "ab_cd", + "_abc", + "abc_", + " ", + "abc ", + " abc", + "ab cd", + } + + uppercaseStrings = []string{ + "A", + "A1", + "1A", + } +) + +func TestAgentNameRegex(t *testing.T) { + t.Parallel() + + t.Run("Valid", func(t *testing.T) { + t.Parallel() + for _, s := range append(validStrings, uppercaseStrings...) { + require.True(t, provisioner.AgentNameRegex.MatchString(s), s) + } + }) + + t.Run("Invalid", func(t *testing.T) { + t.Parallel() + for _, s := range invalidStrings { + require.False(t, provisioner.AgentNameRegex.MatchString(s), s) + } + }) +} + +func TestAppSlugRegex(t *testing.T) { + t.Parallel() + + t.Run("Valid", func(t *testing.T) { + t.Parallel() + for _, s := range validStrings { + require.True(t, provisioner.AppSlugRegex.MatchString(s), s) + } + }) + + t.Run("Invalid", func(t *testing.T) { + t.Parallel() + for _, s := range append(invalidStrings, uppercaseStrings...) { + require.False(t, provisioner.AppSlugRegex.MatchString(s), s) + } + }) +} diff --git a/provisioner/terraform/executor.go b/provisioner/terraform/executor.go index 43754446cbd78..7d6c1fa2dfaf0 100644 --- a/provisioner/terraform/executor.go +++ b/provisioner/terraform/executor.go @@ -308,6 +308,7 @@ func (e *executor) plan(ctx, killCtx context.Context, env, vars []string, logr l Resources: state.Resources, ExternalAuthProviders: state.ExternalAuthProviders, Timings: append(e.timings.aggregate(), graphTimings.aggregate()...), + Presets: state.Presets, }, nil } diff --git a/provisioner/terraform/install.go b/provisioner/terraform/install.go index 7f6474d022ba1..9d2c81d296ec8 100644 --- a/provisioner/terraform/install.go +++ b/provisioner/terraform/install.go @@ -2,8 +2,10 @@ package terraform import ( "context" + "fmt" "os" "path/filepath" + "sync/atomic" "time" "github.com/gofrs/flock" @@ -20,17 +22,19 @@ var ( // when Terraform is not available on the system. // NOTE: Keep this in sync with the version in scripts/Dockerfile.base. // NOTE: Keep this in sync with the version in install.sh. - TerraformVersion = version.Must(version.NewVersion("1.9.8")) + TerraformVersion = version.Must(version.NewVersion("1.10.5")) minTerraformVersion = version.Must(version.NewVersion("1.1.0")) - maxTerraformVersion = version.Must(version.NewVersion("1.9.9")) // use .9 to automatically allow patch releases + maxTerraformVersion = version.Must(version.NewVersion("1.10.9")) // use .9 to automatically allow patch releases terraformMinorVersionMismatch = xerrors.New("Terraform binary minor version mismatch.") ) // Install implements a thread-safe, idempotent Terraform Install // operation. -func Install(ctx context.Context, log slog.Logger, dir string, wantVersion *version.Version) (string, error) { +// +//nolint:revive // verbose is a control flag that controls the verbosity of the log output. +func Install(ctx context.Context, log slog.Logger, verbose bool, dir string, wantVersion *version.Version) (string, error) { err := os.MkdirAll(dir, 0o750) if err != nil { return "", err @@ -64,13 +68,37 @@ func Install(ctx context.Context, log slog.Logger, dir string, wantVersion *vers Version: TerraformVersion, } installer.SetLogger(slog.Stdlib(ctx, log, slog.LevelDebug)) - log.Debug( - ctx, - "installing terraform", + + logInstall := log.Debug + if verbose { + logInstall = log.Info + } + + logInstall(ctx, "installing terraform", slog.F("prev_version", hasVersionStr), slog.F("dir", dir), - slog.F("version", TerraformVersion), - ) + slog.F("version", TerraformVersion)) + + prolongedInstall := atomic.Bool{} + prolongedInstallCtx, prolongedInstallCancel := context.WithCancel(ctx) + go func() { + seconds := 15 + select { + case <-time.After(time.Duration(seconds) * time.Second): + prolongedInstall.Store(true) + // We always want to log this at the info level. + log.Info( + prolongedInstallCtx, + fmt.Sprintf("terraform installation is taking longer than %d seconds, still in progress", seconds), + slog.F("prev_version", hasVersionStr), + slog.F("dir", dir), + slog.F("version", TerraformVersion), + ) + case <-prolongedInstallCtx.Done(): + return + } + }() + defer prolongedInstallCancel() path, err := installer.Install(ctx) if err != nil { @@ -83,5 +111,9 @@ func Install(ctx context.Context, log slog.Logger, dir string, wantVersion *vers return "", xerrors.Errorf("%s should be %s", path, binPath) } + if prolongedInstall.Load() { + log.Info(ctx, "terraform installation complete") + } + return path, nil } diff --git a/provisioner/terraform/install_test.go b/provisioner/terraform/install_test.go index 54471bdf6cf61..6a1be707dd146 100644 --- a/provisioner/terraform/install_test.go +++ b/provisioner/terraform/install_test.go @@ -40,7 +40,7 @@ func TestInstall(t *testing.T) { wg.Add(1) go func() { defer wg.Done() - p, err := terraform.Install(ctx, log, dir, version) + p, err := terraform.Install(ctx, log, false, dir, version) assert.NoError(t, err) paths <- p }() diff --git a/provisioner/terraform/internal/timings_test_utils.go b/provisioner/terraform/internal/timings_test_utils.go index 79448532af45c..3fcb60d6ed0fe 100644 --- a/provisioner/terraform/internal/timings_test_utils.go +++ b/provisioner/terraform/internal/timings_test_utils.go @@ -40,7 +40,7 @@ func TimingsAreEqual(t *testing.T, expected []*proto.Timing, actual []*proto.Tim // Shortcut check. if len(expected)+len(actual) == 0 { - t.Logf("both timings are empty") + t.Log("both timings are empty") return true } diff --git a/provisioner/terraform/provision.go b/provisioner/terraform/provision.go index 70a83bb2334b2..bbb91a96cb3dd 100644 --- a/provisioner/terraform/provision.go +++ b/provisioner/terraform/provision.go @@ -16,7 +16,7 @@ import ( "golang.org/x/xerrors" "cdr.dev/slog" - "github.com/coder/terraform-provider-coder/provider" + "github.com/coder/terraform-provider-coder/v2/provider" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/tracing" @@ -78,7 +78,7 @@ func (s *server) Plan( e := s.executor(sess.WorkDirectory, database.ProvisionerJobTimingStagePlan) if err := e.checkMinVersion(ctx); err != nil { - return provisionersdk.PlanErrorf(err.Error()) + return provisionersdk.PlanErrorf("%s", err.Error()) } logTerraformEnvVars(sess) @@ -113,7 +113,6 @@ func (s *server) Plan( initTimings.ingest(createInitTimingsEvent(timingInitStart)) err = e.init(ctx, killCtx, sess) - if err != nil { initTimings.ingest(createInitTimingsEvent(timingInitErrored)) @@ -168,7 +167,7 @@ func (s *server) Plan( request.Metadata.GetWorkspaceTransition() == proto.WorkspaceTransition_DESTROY, ) if err != nil { - return provisionersdk.PlanErrorf(err.Error()) + return provisionersdk.PlanErrorf("%s", err.Error()) } // Prepend init timings since they occur prior to plan timings. @@ -189,7 +188,7 @@ func (s *server) Apply( e := s.executor(sess.WorkDirectory, database.ProvisionerJobTimingStageApply) if err := e.checkMinVersion(ctx); err != nil { - return provisionersdk.ApplyErrorf(err.Error()) + return provisionersdk.ApplyErrorf("%s", err.Error()) } logTerraformEnvVars(sess) @@ -270,7 +269,7 @@ func provisionEnv( env = append(env, provider.ParameterEnvironmentVariable(param.Name)+"="+param.Value) } for _, extAuth := range externalAuth { - env = append(env, provider.GitAuthAccessTokenEnvironmentVariable(extAuth.Id)+"="+extAuth.AccessToken) + env = append(env, gitAuthAccessTokenEnvironmentVariable(extAuth.Id)+"="+extAuth.AccessToken) env = append(env, provider.ExternalAuthAccessTokenEnvironmentVariable(extAuth.Id)+"="+extAuth.AccessToken) } @@ -351,3 +350,12 @@ func tryGettingCoderProviderStacktrace(sess *provisionersdk.Session) string { } return string(stacktraces) } + +// gitAuthAccessTokenEnvironmentVariable is copied from +// github.com/coder/terraform-provider-coder/provider.GitAuthAccessTokenEnvironmentVariable@v1.0.4. +// While removed in v2 of the provider, we keep this to support customers using older templates that +// depend on this environment variable. Once we are certain that no customers are still using v1 of +// the provider, we can remove this function. +func gitAuthAccessTokenEnvironmentVariable(id string) string { + return fmt.Sprintf("CODER_GIT_AUTH_ACCESS_TOKEN_%s", id) +} diff --git a/provisioner/terraform/resources.go b/provisioner/terraform/resources.go index 4a48023b7463e..b3e71d452d51a 100644 --- a/provisioner/terraform/resources.go +++ b/provisioner/terraform/resources.go @@ -12,7 +12,7 @@ import ( "cdr.dev/slog" - "github.com/coder/terraform-provider-coder/provider" + "github.com/coder/terraform-provider-coder/v2/provider" tfaddr "github.com/hashicorp/go-terraform-address" @@ -56,6 +56,23 @@ type agentAttributes struct { Metadata []agentMetadata `mapstructure:"metadata"` DisplayApps []agentDisplayAppsAttributes `mapstructure:"display_apps"` Order int64 `mapstructure:"order"` + ResourcesMonitoring []agentResourcesMonitoring `mapstructure:"resources_monitoring"` +} + +type agentResourcesMonitoring struct { + Memory []agentMemoryResourceMonitor `mapstructure:"memory"` + Volumes []agentVolumeResourceMonitor `mapstructure:"volume"` +} + +type agentMemoryResourceMonitor struct { + Enabled bool `mapstructure:"enabled"` + Threshold int32 `mapstructure:"threshold"` +} + +type agentVolumeResourceMonitor struct { + Path string `mapstructure:"path"` + Enabled bool `mapstructure:"enabled"` + Threshold int32 `mapstructure:"threshold"` } type agentDisplayAppsAttributes struct { @@ -132,6 +149,7 @@ type resourceMetadataItem struct { type State struct { Resources []*proto.Resource Parameters []*proto.RichParameter + Presets []*proto.Preset ExternalAuthProviders []*proto.ExternalAuthProviderResource } @@ -159,7 +177,7 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s // Extra array to preserve the order of rich parameters. tfResourcesRichParameters := make([]*tfjson.StateResource, 0) - + tfResourcesPresets := make([]*tfjson.StateResource, 0) var findTerraformResources func(mod *tfjson.StateModule) findTerraformResources = func(mod *tfjson.StateModule) { for _, module := range mod.ChildModules { @@ -169,6 +187,9 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s if resource.Type == "coder_parameter" { tfResourcesRichParameters = append(tfResourcesRichParameters, resource) } + if resource.Type == "coder_workspace_preset" { + tfResourcesPresets = append(tfResourcesPresets, resource) + } label := convertAddressToLabel(resource.Address) if tfResourcesByLabel[label] == nil { @@ -194,10 +215,25 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s return nil, xerrors.Errorf("decode agent attributes: %w", err) } - if _, ok := agentNames[tfResource.Name]; ok { + // Similar logic is duplicated in terraform/resources.go. + if tfResource.Name == "" { + return nil, xerrors.Errorf("agent name cannot be empty") + } + // In 2025-02 we removed support for underscores in agent names. To + // provide a nicer error message, we check the regex first and check + // for underscores if it fails. + if !provisioner.AgentNameRegex.MatchString(tfResource.Name) { + if strings.Contains(tfResource.Name, "_") { + return nil, xerrors.Errorf("agent name %q contains underscores which are no longer supported, please use hyphens instead (regex: %q)", tfResource.Name, provisioner.AgentNameRegex.String()) + } + return nil, xerrors.Errorf("agent name %q does not match regex %q", tfResource.Name, provisioner.AgentNameRegex.String()) + } + // Agent names must be case-insensitive-unique, to be unambiguous in + // `coder_app`s and CoderVPN DNS names. + if _, ok := agentNames[strings.ToLower(tfResource.Name)]; ok { return nil, xerrors.Errorf("duplicate agent name: %s", tfResource.Name) } - agentNames[tfResource.Name] = struct{}{} + agentNames[strings.ToLower(tfResource.Name)] = struct{}{} // Handling for deprecated attributes. login_before_ready was replaced // by startup_script_behavior, but we still need to support it for @@ -239,6 +275,29 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s } } + resourcesMonitoring := &proto.ResourcesMonitoring{ + Volumes: make([]*proto.VolumeResourceMonitor, 0), + } + + for _, resource := range attrs.ResourcesMonitoring { + for _, memoryResource := range resource.Memory { + resourcesMonitoring.Memory = &proto.MemoryResourceMonitor{ + Enabled: memoryResource.Enabled, + Threshold: memoryResource.Threshold, + } + } + } + + for _, resource := range attrs.ResourcesMonitoring { + for _, volume := range resource.Volumes { + resourcesMonitoring.Volumes = append(resourcesMonitoring.Volumes, &proto.VolumeResourceMonitor{ + Path: volume.Path, + Enabled: volume.Enabled, + Threshold: volume.Threshold, + }) + } + } + agent := &proto.Agent{ Name: tfResource.Name, Id: attrs.ID, @@ -249,6 +308,7 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s ConnectionTimeoutSeconds: attrs.ConnectionTimeoutSeconds, TroubleshootingUrl: attrs.TroubleshootingURL, MotdFile: attrs.MOTDFile, + ResourcesMonitoring: resourcesMonitoring, Metadata: metadata, DisplayApps: displayApps, Order: attrs.Order, @@ -396,6 +456,7 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s if attrs.Slug == "" { attrs.Slug = resource.Name } + // Similar logic is duplicated in terraform/resources.go. if attrs.DisplayName == "" { if attrs.Name != "" { // Name is deprecated but still accepted. @@ -405,8 +466,10 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s } } + // Contrary to agent names above, app slugs were never permitted to + // contain uppercase letters or underscores. if !provisioner.AppSlugRegex.MatchString(attrs.Slug) { - return nil, xerrors.Errorf("invalid app slug %q, please update your coder/coder provider to the latest version and specify the slug property on each coder_app", attrs.Slug) + return nil, xerrors.Errorf("app slug %q does not match regex %q", attrs.Slug, provisioner.AppSlugRegex.String()) } if _, exists := appSlugs[attrs.Slug]; exists { @@ -734,6 +797,78 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s ) } + var duplicatedPresetNames []string + presets := make([]*proto.Preset, 0) + for _, resource := range tfResourcesPresets { + var preset provider.WorkspacePreset + err = mapstructure.Decode(resource.AttributeValues, &preset) + if err != nil { + return nil, xerrors.Errorf("decode preset attributes: %w", err) + } + + var duplicatedPresetParameterNames []string + var nonExistentParameters []string + var presetParameters []*proto.PresetParameter + for name, value := range preset.Parameters { + presetParameter := &proto.PresetParameter{ + Name: name, + Value: value, + } + + formattedName := fmt.Sprintf("%q", name) + if !slice.Contains(duplicatedPresetParameterNames, formattedName) && + slice.ContainsCompare(presetParameters, presetParameter, func(a, b *proto.PresetParameter) bool { + return a.Name == b.Name + }) { + duplicatedPresetParameterNames = append(duplicatedPresetParameterNames, formattedName) + } + if !slice.ContainsCompare(parameters, &proto.RichParameter{Name: name}, func(a, b *proto.RichParameter) bool { + return a.Name == b.Name + }) { + nonExistentParameters = append(nonExistentParameters, name) + } + + presetParameters = append(presetParameters, presetParameter) + } + + if len(duplicatedPresetParameterNames) > 0 { + s := "" + if len(duplicatedPresetParameterNames) == 1 { + s = "s" + } + return nil, xerrors.Errorf( + "coder_workspace_preset parameters must be unique but %s appear%s multiple times", stringutil.JoinWithConjunction(duplicatedPresetParameterNames), s, + ) + } + + if len(nonExistentParameters) > 0 { + logger.Warn( + ctx, + "coder_workspace_preset defines preset values for at least one parameter that is not defined by the template", + slog.F("parameters", stringutil.JoinWithConjunction(nonExistentParameters)), + ) + } + + protoPreset := &proto.Preset{ + Name: preset.Name, + Parameters: presetParameters, + } + if slice.Contains(duplicatedPresetNames, preset.Name) { + duplicatedPresetNames = append(duplicatedPresetNames, preset.Name) + } + presets = append(presets, protoPreset) + } + if len(duplicatedPresetNames) > 0 { + s := "" + if len(duplicatedPresetNames) == 1 { + s = "s" + } + return nil, xerrors.Errorf( + "coder_workspace_preset names must be unique but %s appear%s multiple times", + stringutil.JoinWithConjunction(duplicatedPresetNames), s, + ) + } + // A map is used to ensure we don't have duplicates! externalAuthProvidersMap := map[string]*proto.ExternalAuthProviderResource{} for _, tfResources := range tfResourcesByLabel { @@ -767,6 +902,7 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s return &State{ Resources: resources, Parameters: parameters, + Presets: presets, ExternalAuthProviders: externalAuthProviders, }, nil } diff --git a/provisioner/terraform/resources_test.go b/provisioner/terraform/resources_test.go index a3190458884ef..46ad49d01d476 100644 --- a/provisioner/terraform/resources_test.go +++ b/provisioner/terraform/resources_test.go @@ -11,6 +11,7 @@ import ( "strings" "testing" + "github.com/google/go-cmp/cmp" tfjson "github.com/hashicorp/terraform-json" "github.com/stretchr/testify/require" protobuf "google.golang.org/protobuf/proto" @@ -35,6 +36,7 @@ func TestConvertResources(t *testing.T) { type testCase struct { resources []*proto.Resource parameters []*proto.RichParameter + Presets []*proto.Preset externalAuthProviders []*proto.ExternalAuthProviderResource } @@ -66,6 +68,7 @@ func TestConvertResources(t *testing.T) { Auth: &proto.Agent_Token{}, ConnectionTimeoutSeconds: 120, DisplayApps: &displayApps, + ResourcesMonitoring: &proto.ResourcesMonitoring{}, }}, }}, }, @@ -83,6 +86,7 @@ func TestConvertResources(t *testing.T) { Auth: &proto.Agent_Token{}, ConnectionTimeoutSeconds: 120, DisplayApps: &displayApps, + ResourcesMonitoring: &proto.ResourcesMonitoring{}, }}, }, { Name: "second", @@ -101,6 +105,7 @@ func TestConvertResources(t *testing.T) { Auth: &proto.Agent_InstanceId{}, ConnectionTimeoutSeconds: 120, DisplayApps: &displayApps, + ResourcesMonitoring: &proto.ResourcesMonitoring{}, }}, }}, }, @@ -117,6 +122,7 @@ func TestConvertResources(t *testing.T) { Auth: &proto.Agent_Token{}, ConnectionTimeoutSeconds: 120, DisplayApps: &displayApps, + ResourcesMonitoring: &proto.ResourcesMonitoring{}, }}, ModulePath: "module.module", }}, @@ -134,6 +140,7 @@ func TestConvertResources(t *testing.T) { Auth: &proto.Agent_Token{}, ConnectionTimeoutSeconds: 120, DisplayApps: &displayApps, + ResourcesMonitoring: &proto.ResourcesMonitoring{}, }, { Name: "dev2", OperatingSystem: "darwin", @@ -142,6 +149,7 @@ func TestConvertResources(t *testing.T) { ConnectionTimeoutSeconds: 1, MotdFile: "/etc/motd", DisplayApps: &displayApps, + ResourcesMonitoring: &proto.ResourcesMonitoring{}, Scripts: []*proto.Script{{ Icon: "/emojis/25c0.png", DisplayName: "Shutdown Script", @@ -157,6 +165,7 @@ func TestConvertResources(t *testing.T) { ConnectionTimeoutSeconds: 120, TroubleshootingUrl: "https://coder.com/troubleshoot", DisplayApps: &displayApps, + ResourcesMonitoring: &proto.ResourcesMonitoring{}, }, { Name: "dev4", OperatingSystem: "linux", @@ -164,6 +173,7 @@ func TestConvertResources(t *testing.T) { Auth: &proto.Agent_Token{}, ConnectionTimeoutSeconds: 120, DisplayApps: &displayApps, + ResourcesMonitoring: &proto.ResourcesMonitoring{}, }}, }}, }, @@ -205,6 +215,7 @@ func TestConvertResources(t *testing.T) { Auth: &proto.Agent_Token{}, ConnectionTimeoutSeconds: 120, DisplayApps: &displayApps, + ResourcesMonitoring: &proto.ResourcesMonitoring{}, }}, }}, }, @@ -231,6 +242,7 @@ func TestConvertResources(t *testing.T) { Auth: &proto.Agent_Token{}, ConnectionTimeoutSeconds: 120, DisplayApps: &displayApps, + ResourcesMonitoring: &proto.ResourcesMonitoring{}, }}, }}, }, @@ -265,6 +277,7 @@ func TestConvertResources(t *testing.T) { Auth: &proto.Agent_Token{}, ConnectionTimeoutSeconds: 120, DisplayApps: &displayApps, + ResourcesMonitoring: &proto.ResourcesMonitoring{}, }}, }, { Name: "dev2", @@ -284,6 +297,7 @@ func TestConvertResources(t *testing.T) { Auth: &proto.Agent_Token{}, ConnectionTimeoutSeconds: 120, DisplayApps: &displayApps, + ResourcesMonitoring: &proto.ResourcesMonitoring{}, }}, }}, }, @@ -308,6 +322,7 @@ func TestConvertResources(t *testing.T) { Auth: &proto.Agent_Token{}, ConnectionTimeoutSeconds: 120, DisplayApps: &displayApps, + ResourcesMonitoring: &proto.ResourcesMonitoring{}, }}, }, { Name: "dev2", @@ -325,6 +340,7 @@ func TestConvertResources(t *testing.T) { Auth: &proto.Agent_Token{}, ConnectionTimeoutSeconds: 120, DisplayApps: &displayApps, + ResourcesMonitoring: &proto.ResourcesMonitoring{}, }}, }, { Name: "env1", @@ -337,6 +353,75 @@ func TestConvertResources(t *testing.T) { Type: "coder_env", }}, }, + "multiple-agents-multiple-monitors": { + resources: []*proto.Resource{{ + Name: "dev", + Type: "null_resource", + Agents: []*proto.Agent{ + { + Name: "dev1", + OperatingSystem: "linux", + Architecture: "amd64", + Apps: []*proto.App{ + { + Slug: "app1", + DisplayName: "app1", + // Subdomain defaults to false if unspecified. + Subdomain: false, + OpenIn: proto.AppOpenIn_SLIM_WINDOW, + }, + { + Slug: "app2", + DisplayName: "app2", + Subdomain: true, + Healthcheck: &proto.Healthcheck{ + Url: "http://localhost:13337/healthz", + Interval: 5, + Threshold: 6, + }, + OpenIn: proto.AppOpenIn_SLIM_WINDOW, + }, + }, + Auth: &proto.Agent_Token{}, + ConnectionTimeoutSeconds: 120, + DisplayApps: &displayApps, + ResourcesMonitoring: &proto.ResourcesMonitoring{ + Memory: &proto.MemoryResourceMonitor{ + Enabled: true, + Threshold: 80, + }, + }, + }, + { + Name: "dev2", + OperatingSystem: "linux", + Architecture: "amd64", + Apps: []*proto.App{}, + Auth: &proto.Agent_Token{}, + ConnectionTimeoutSeconds: 120, + DisplayApps: &displayApps, + ResourcesMonitoring: &proto.ResourcesMonitoring{ + Memory: &proto.MemoryResourceMonitor{ + Enabled: true, + Threshold: 99, + }, + Volumes: []*proto.VolumeResourceMonitor{ + { + Path: "/volume2", + Enabled: false, + Threshold: 50, + }, + { + Path: "/volume1", + Enabled: true, + Threshold: 80, + }, + }, + }, + }, + }, + }}, + }, "multiple-agents-multiple-scripts": { resources: []*proto.Resource{{ Name: "dev1", @@ -360,6 +445,7 @@ func TestConvertResources(t *testing.T) { Auth: &proto.Agent_Token{}, ConnectionTimeoutSeconds: 120, DisplayApps: &displayApps, + ResourcesMonitoring: &proto.ResourcesMonitoring{}, }}, }, { Name: "dev2", @@ -378,6 +464,7 @@ func TestConvertResources(t *testing.T) { Auth: &proto.Agent_Token{}, ConnectionTimeoutSeconds: 120, DisplayApps: &displayApps, + ResourcesMonitoring: &proto.ResourcesMonitoring{}, }}, }}, }, @@ -417,6 +504,7 @@ func TestConvertResources(t *testing.T) { }}, ConnectionTimeoutSeconds: 120, DisplayApps: &displayApps, + ResourcesMonitoring: &proto.ResourcesMonitoring{}, }}, }}, }, @@ -468,6 +556,7 @@ func TestConvertResources(t *testing.T) { Auth: &proto.Agent_Token{}, ConnectionTimeoutSeconds: 120, DisplayApps: &displayApps, + ResourcesMonitoring: &proto.ResourcesMonitoring{}, Scripts: []*proto.Script{{ DisplayName: "Startup Script", RunOnStart: true, @@ -490,6 +579,7 @@ func TestConvertResources(t *testing.T) { Auth: &proto.Agent_Token{}, ConnectionTimeoutSeconds: 120, DisplayApps: &displayApps, + ResourcesMonitoring: &proto.ResourcesMonitoring{}, }}, }}, parameters: []*proto.RichParameter{{ @@ -569,6 +659,7 @@ func TestConvertResources(t *testing.T) { Auth: &proto.Agent_Token{}, ConnectionTimeoutSeconds: 120, DisplayApps: &displayApps, + ResourcesMonitoring: &proto.ResourcesMonitoring{}, }}, }}, parameters: []*proto.RichParameter{{ @@ -595,6 +686,7 @@ func TestConvertResources(t *testing.T) { Auth: &proto.Agent_Token{}, ConnectionTimeoutSeconds: 120, DisplayApps: &displayApps, + ResourcesMonitoring: &proto.ResourcesMonitoring{}, }}, }}, parameters: []*proto.RichParameter{{ @@ -637,21 +729,6 @@ func TestConvertResources(t *testing.T) { ValidationMax: nil, }}, }, - "git-auth-providers": { - resources: []*proto.Resource{{ - Name: "dev", - Type: "null_resource", - Agents: []*proto.Agent{{ - Name: "main", - OperatingSystem: "linux", - Architecture: "amd64", - Auth: &proto.Agent_Token{}, - ConnectionTimeoutSeconds: 120, - DisplayApps: &displayApps, - }}, - }}, - externalAuthProviders: []*proto.ExternalAuthProviderResource{{Id: "github"}, {Id: "gitlab"}}, - }, "external-auth-providers": { resources: []*proto.Resource{{ Name: "dev", @@ -663,6 +740,7 @@ func TestConvertResources(t *testing.T) { Auth: &proto.Agent_Token{}, ConnectionTimeoutSeconds: 120, DisplayApps: &displayApps, + ResourcesMonitoring: &proto.ResourcesMonitoring{}, }}, }}, externalAuthProviders: []*proto.ExternalAuthProviderResource{{Id: "github"}, {Id: "gitlab", Optional: true}}, @@ -681,6 +759,7 @@ func TestConvertResources(t *testing.T) { VscodeInsiders: true, WebTerminal: true, }, + ResourcesMonitoring: &proto.ResourcesMonitoring{}, }}, }}, }, @@ -695,6 +774,59 @@ func TestConvertResources(t *testing.T) { Auth: &proto.Agent_Token{}, ConnectionTimeoutSeconds: 120, DisplayApps: &proto.DisplayApps{}, + ResourcesMonitoring: &proto.ResourcesMonitoring{}, + }}, + }}, + }, + "presets": { + resources: []*proto.Resource{{ + Name: "dev", + Type: "null_resource", + Agents: []*proto.Agent{{ + Name: "dev", + OperatingSystem: "windows", + Architecture: "arm64", + Auth: &proto.Agent_Token{}, + ConnectionTimeoutSeconds: 120, + DisplayApps: &displayApps, + ResourcesMonitoring: &proto.ResourcesMonitoring{}, + }}, + }}, + parameters: []*proto.RichParameter{{ + Name: "First parameter from child module", + Type: "string", + Description: "First parameter from child module", + Mutable: true, + DefaultValue: "abcdef", + }, { + Name: "Second parameter from child module", + Type: "string", + Description: "Second parameter from child module", + Mutable: true, + DefaultValue: "ghijkl", + }, { + Name: "First parameter from module", + Type: "string", + Description: "First parameter from module", + Mutable: true, + DefaultValue: "abcdef", + }, { + Name: "Second parameter from module", + Type: "string", + Description: "Second parameter from module", + Mutable: true, + DefaultValue: "ghijkl", + }, { + Name: "Sample", + Type: "string", + Description: "blah blah", + DefaultValue: "ok", + }}, + Presets: []*proto.Preset{{ + Name: "My First Project", + Parameters: []*proto.PresetParameter{{ + Name: "Sample", + Value: "A1B2C3", }}, }}, }, @@ -764,7 +896,9 @@ func TestConvertResources(t *testing.T) { var resourcesMap []map[string]interface{} err = json.Unmarshal(data, &resourcesMap) require.NoError(t, err) - require.Equal(t, expectedNoMetadataMap, resourcesMap) + if diff := cmp.Diff(expectedNoMetadataMap, resourcesMap); diff != "" { + require.Failf(t, "unexpected resources", "diff (-want +got):\n%s", diff) + } expectedParams := expected.parameters if expectedParams == nil { @@ -778,6 +912,8 @@ func TestConvertResources(t *testing.T) { require.Equal(t, expectedNoMetadataMap, resourcesMap) require.ElementsMatch(t, expected.externalAuthProviders, state.ExternalAuthProviders) + + require.ElementsMatch(t, expected.Presets, state.Presets) }) t.Run("Provision", func(t *testing.T) { @@ -819,8 +955,12 @@ func TestConvertResources(t *testing.T) { var resourcesMap []map[string]interface{} err = json.Unmarshal(data, &resourcesMap) require.NoError(t, err) - require.Equal(t, expectedMap, resourcesMap) + if diff := cmp.Diff(expectedMap, resourcesMap); diff != "" { + require.Failf(t, "unexpected resources", "diff (-want +got):\n%s", diff) + } require.ElementsMatch(t, expected.externalAuthProviders, state.ExternalAuthProviders) + + require.ElementsMatch(t, expected.Presets, state.Presets) }) }) } @@ -844,6 +984,7 @@ func TestInvalidTerraformAddress(t *testing.T) { require.Equal(t, state.Resources[0].ModulePath, "invalid terraform address") } +//nolint:tparallel func TestAppSlugValidation(t *testing.T) { t.Parallel() ctx, logger := ctxAndLogger(t) @@ -861,29 +1002,147 @@ func TestAppSlugValidation(t *testing.T) { tfPlanGraph, err := os.ReadFile(filepath.Join(dir, "multiple-apps.tfplan.dot")) require.NoError(t, err) - // Change all slugs to be invalid. + cases := []struct { + slug string + errContains string + }{ + {slug: "$$$ invalid slug $$$", errContains: "does not match regex"}, + {slug: "invalid--slug", errContains: "does not match regex"}, + {slug: "invalid_slug", errContains: "does not match regex"}, + {slug: "Invalid-slug", errContains: "does not match regex"}, + {slug: "valid", errContains: ""}, + } + + //nolint:paralleltest + for i, c := range cases { + c := c + t.Run(fmt.Sprintf("case-%d", i), func(t *testing.T) { + // Change the first app slug to match the current case. + for _, resource := range tfPlan.PlannedValues.RootModule.Resources { + if resource.Type == "coder_app" { + resource.AttributeValues["slug"] = c.slug + break + } + } + + _, err := terraform.ConvertState(ctx, []*tfjson.StateModule{tfPlan.PlannedValues.RootModule}, string(tfPlanGraph), logger) + if c.errContains != "" { + require.ErrorContains(t, err, c.errContains) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestAppSlugDuplicate(t *testing.T) { + t.Parallel() + ctx, logger := ctxAndLogger(t) + + // nolint:dogsled + _, filename, _, _ := runtime.Caller(0) + + dir := filepath.Join(filepath.Dir(filename), "testdata", "multiple-apps") + tfPlanRaw, err := os.ReadFile(filepath.Join(dir, "multiple-apps.tfplan.json")) + require.NoError(t, err) + var tfPlan tfjson.Plan + err = json.Unmarshal(tfPlanRaw, &tfPlan) + require.NoError(t, err) + tfPlanGraph, err := os.ReadFile(filepath.Join(dir, "multiple-apps.tfplan.dot")) + require.NoError(t, err) + for _, resource := range tfPlan.PlannedValues.RootModule.Resources { if resource.Type == "coder_app" { - resource.AttributeValues["slug"] = "$$$ invalid slug $$$" + resource.AttributeValues["slug"] = "dev" } } - state, err := terraform.ConvertState(ctx, []*tfjson.StateModule{tfPlan.PlannedValues.RootModule}, string(tfPlanGraph), logger) - require.Nil(t, state) + _, err = terraform.ConvertState(ctx, []*tfjson.StateModule{tfPlan.PlannedValues.RootModule}, string(tfPlanGraph), logger) require.Error(t, err) - require.ErrorContains(t, err, "invalid app slug") + require.ErrorContains(t, err, "duplicate app slug") +} + +//nolint:tparallel +func TestAgentNameInvalid(t *testing.T) { + t.Parallel() + ctx, logger := ctxAndLogger(t) + + // nolint:dogsled + _, filename, _, _ := runtime.Caller(0) + + dir := filepath.Join(filepath.Dir(filename), "testdata", "multiple-agents") + tfPlanRaw, err := os.ReadFile(filepath.Join(dir, "multiple-agents.tfplan.json")) + require.NoError(t, err) + var tfPlan tfjson.Plan + err = json.Unmarshal(tfPlanRaw, &tfPlan) + require.NoError(t, err) + tfPlanGraph, err := os.ReadFile(filepath.Join(dir, "multiple-agents.tfplan.dot")) + require.NoError(t, err) + + cases := []struct { + name string + errContains string + }{ + {name: "bad--name", errContains: "does not match regex"}, + {name: "bad_name", errContains: "contains underscores"}, // custom error for underscores + {name: "valid-name-123", errContains: ""}, + {name: "valid", errContains: ""}, + {name: "UppercaseValid", errContains: ""}, + } + + //nolint:paralleltest + for i, c := range cases { + c := c + t.Run(fmt.Sprintf("case-%d", i), func(t *testing.T) { + // Change the first agent name to match the current case. + for _, resource := range tfPlan.PlannedValues.RootModule.Resources { + if resource.Type == "coder_agent" { + resource.Name = c.name + break + } + } + + _, err := terraform.ConvertState(ctx, []*tfjson.StateModule{tfPlan.PlannedValues.RootModule}, string(tfPlanGraph), logger) + if c.errContains != "" { + require.ErrorContains(t, err, c.errContains) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestAgentNameDuplicate(t *testing.T) { + t.Parallel() + ctx, logger := ctxAndLogger(t) + + // nolint:dogsled + _, filename, _, _ := runtime.Caller(0) + + dir := filepath.Join(filepath.Dir(filename), "testdata", "multiple-agents") + tfPlanRaw, err := os.ReadFile(filepath.Join(dir, "multiple-agents.tfplan.json")) + require.NoError(t, err) + var tfPlan tfjson.Plan + err = json.Unmarshal(tfPlanRaw, &tfPlan) + require.NoError(t, err) + tfPlanGraph, err := os.ReadFile(filepath.Join(dir, "multiple-agents.tfplan.dot")) + require.NoError(t, err) - // Change all slugs to be identical and valid. for _, resource := range tfPlan.PlannedValues.RootModule.Resources { - if resource.Type == "coder_app" { - resource.AttributeValues["slug"] = "valid" + if resource.Type == "coder_agent" { + switch resource.Name { + case "dev1": + resource.Name = "dev" + case "dev2": + resource.Name = "Dev" + } } } - state, err = terraform.ConvertState(ctx, []*tfjson.StateModule{tfPlan.PlannedValues.RootModule}, string(tfPlanGraph), logger) + state, err := terraform.ConvertState(ctx, []*tfjson.StateModule{tfPlan.PlannedValues.RootModule}, string(tfPlanGraph), logger) require.Nil(t, state) require.Error(t, err) - require.ErrorContains(t, err, "duplicate app slug") + require.ErrorContains(t, err, "duplicate agent name") } func TestMetadataResourceDuplicate(t *testing.T) { diff --git a/provisioner/terraform/serve.go b/provisioner/terraform/serve.go index 7a3b033bf2bba..764b57da84ed3 100644 --- a/provisioner/terraform/serve.go +++ b/provisioner/terraform/serve.go @@ -2,11 +2,13 @@ package terraform import ( "context" + "errors" "path/filepath" "sync" "time" "github.com/cli/safeexec" + "github.com/hashicorp/go-version" semconv "go.opentelemetry.io/otel/semconv/v1.14.0" "go.opentelemetry.io/otel/trace" "golang.org/x/xerrors" @@ -41,10 +43,15 @@ type ServeOptions struct { ExitTimeout time.Duration } -func absoluteBinaryPath(ctx context.Context, logger slog.Logger) (string, error) { +type systemBinaryDetails struct { + absolutePath string + version *version.Version +} + +func systemBinary(ctx context.Context) (*systemBinaryDetails, error) { binaryPath, err := safeexec.LookPath("terraform") if err != nil { - return "", xerrors.Errorf("Terraform binary not found: %w", err) + return nil, xerrors.Errorf("Terraform binary not found: %w", err) } // If the "coder" binary is in the same directory as @@ -54,59 +61,68 @@ func absoluteBinaryPath(ctx context.Context, logger slog.Logger) (string, error) // to execute this properly! absoluteBinary, err := filepath.Abs(binaryPath) if err != nil { - return "", xerrors.Errorf("Terraform binary absolute path not found: %w", err) + return nil, xerrors.Errorf("Terraform binary absolute path not found: %w", err) } // Checking the installed version of Terraform. installedVersion, err := versionFromBinaryPath(ctx, absoluteBinary) if err != nil { - return "", xerrors.Errorf("Terraform binary get version failed: %w", err) + return nil, xerrors.Errorf("Terraform binary get version failed: %w", err) } - logger.Info(ctx, "detected terraform version", - slog.F("installed_version", installedVersion.String()), - slog.F("min_version", minTerraformVersion.String()), - slog.F("max_version", maxTerraformVersion.String())) - - if installedVersion.LessThan(minTerraformVersion) { - logger.Warn(ctx, "installed terraform version too old, will download known good version to cache") - return "", terraformMinorVersionMismatch + details := &systemBinaryDetails{ + absolutePath: absoluteBinary, + version: installedVersion, } - // Warn if the installed version is newer than what we've decided is the max. - // We used to ignore it and download our own version but this makes it easier - // to test out newer versions of Terraform. - if installedVersion.GreaterThanOrEqual(maxTerraformVersion) { - logger.Warn(ctx, "installed terraform version newer than expected, you may experience bugs", - slog.F("installed_version", installedVersion.String()), - slog.F("max_version", maxTerraformVersion.String())) + if installedVersion.LessThan(minTerraformVersion) { + return details, terraformMinorVersionMismatch } - return absoluteBinary, nil + return details, nil } // Serve starts a dRPC server on the provided transport speaking Terraform provisioner. func Serve(ctx context.Context, options *ServeOptions) error { if options.BinaryPath == "" { - absoluteBinary, err := absoluteBinaryPath(ctx, options.Logger) + binaryDetails, err := systemBinary(ctx) if err != nil { // This is an early exit to prevent extra execution in case the context is canceled. // It generally happens in unit tests since this method is asynchronous and // the unit test kills the app before this is complete. - if xerrors.Is(err, context.Canceled) { - return xerrors.Errorf("absolute binary context canceled: %w", err) + if errors.Is(err, context.Canceled) { + return xerrors.Errorf("system binary context canceled: %w", err) } - options.Logger.Warn(ctx, "no usable terraform binary found, downloading to cache dir", - slog.F("terraform_version", TerraformVersion.String()), - slog.F("cache_dir", options.CachePath)) - binPath, err := Install(ctx, options.Logger, options.CachePath, TerraformVersion) + if errors.Is(err, terraformMinorVersionMismatch) { + options.Logger.Warn(ctx, "installed terraform version too old, will download known good version to cache, or use a previously cached version", + slog.F("installed_version", binaryDetails.version.String()), + slog.F("min_version", minTerraformVersion.String())) + } + + binPath, err := Install(ctx, options.Logger, options.ExternalProvisioner, options.CachePath, TerraformVersion) if err != nil { return xerrors.Errorf("install terraform: %w", err) } options.BinaryPath = binPath } else { - options.BinaryPath = absoluteBinary + logVersion := options.Logger.Debug + if options.ExternalProvisioner { + logVersion = options.Logger.Info + } + logVersion(ctx, "detected terraform version", + slog.F("installed_version", binaryDetails.version.String()), + slog.F("min_version", minTerraformVersion.String()), + slog.F("max_version", maxTerraformVersion.String())) + // Warn if the installed version is newer than what we've decided is the max. + // We used to ignore it and download our own version but this makes it easier + // to test out newer versions of Terraform. + if binaryDetails.version.GreaterThanOrEqual(maxTerraformVersion) { + options.Logger.Warn(ctx, "installed terraform version newer than expected, you may experience bugs", + slog.F("installed_version", binaryDetails.version.String()), + slog.F("max_version", maxTerraformVersion.String())) + } + options.BinaryPath = binaryDetails.absolutePath } } if options.Tracer == nil { diff --git a/provisioner/terraform/serve_internal_test.go b/provisioner/terraform/serve_internal_test.go index 165a6e4a0af88..0e4a673cd2c6f 100644 --- a/provisioner/terraform/serve_internal_test.go +++ b/provisioner/terraform/serve_internal_test.go @@ -54,7 +54,6 @@ func Test_absoluteBinaryPath(t *testing.T) { t.Skip("Dummy terraform executable on Windows requires sh which isn't very practical.") } - log := testutil.Logger(t) // Create a temp dir with the binary tempDir := t.TempDir() terraformBinaryOutput := fmt.Sprintf(`#!/bin/sh @@ -85,11 +84,12 @@ func Test_absoluteBinaryPath(t *testing.T) { } ctx := testutil.Context(t, testutil.WaitShort) - actualAbsoluteBinary, actualErr := absoluteBinaryPath(ctx, log) + actualBinaryDetails, actualErr := systemBinary(ctx) - require.Equal(t, expectedAbsoluteBinary, actualAbsoluteBinary) if tt.expectedErr == nil { require.NoError(t, actualErr) + require.Equal(t, expectedAbsoluteBinary, actualBinaryDetails.absolutePath) + require.Equal(t, tt.terraformVersion, actualBinaryDetails.version.String()) } else { require.EqualError(t, actualErr, tt.expectedErr.Error()) } diff --git a/provisioner/terraform/testdata/calling-module/calling-module.tf b/provisioner/terraform/testdata/calling-module/calling-module.tf index 14777169d9994..33fcbb3f1984f 100644 --- a/provisioner/terraform/testdata/calling-module/calling-module.tf +++ b/provisioner/terraform/testdata/calling-module/calling-module.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/calling-module/calling-module.tfplan.json b/provisioner/terraform/testdata/calling-module/calling-module.tfplan.json index 30bc360bb1940..8759627e35398 100644 --- a/provisioner/terraform/testdata/calling-module/calling-module.tfplan.json +++ b/provisioner/terraform/testdata/calling-module/calling-module.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,23 +10,20 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -90,16 +87,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -177,7 +171,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "module.module:null": { "name": "null", @@ -201,7 +195,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 } ], "module_calls": { @@ -260,7 +254,7 @@ ] } ], - "timestamp": "2024-10-28T20:07:49Z", + "timestamp": "2025-02-18T10:58:12Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/calling-module/calling-module.tfstate.json b/provisioner/terraform/testdata/calling-module/calling-module.tfstate.json index 5ead2c6ace0d5..0286c44e0412b 100644 --- a/provisioner/terraform/testdata/calling-module/calling-module.tfstate.json +++ b/provisioner/terraform/testdata/calling-module/calling-module.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -10,7 +10,7 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -26,19 +26,16 @@ } ], "env": null, - "id": "04d66dc4-e25a-4f65-af6f-a9af6b907430", + "id": "6b8c1681-8d24-454f-9674-75aa10a78a66", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "10fbd765-b0cc-4d6f-b5de-e5a036b2cb4b", + "startup_script_behavior": "non-blocking", + "token": "b10f2c9a-2936-4d64-9d3c-3705fa094272", "troubleshooting_url": null }, "sensitive_values": { @@ -69,7 +66,7 @@ "outputs": { "script": "" }, - "random": "7917595776755902204" + "random": "2818431725852233027" }, "sensitive_values": { "inputs": {}, @@ -84,7 +81,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "2669991968036854745", + "id": "2514800225855033412", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tf b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tf index 3f210452dfee0..6ad44a62de986 100644 --- a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tf +++ b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfplan.json b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfplan.json index 38af6827019e7..4f478962e7b97 100644 --- a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfplan.json +++ b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,23 +10,20 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -80,16 +77,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -155,7 +149,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "null": { "name": "null", @@ -178,7 +172,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "null_resource.a", @@ -205,7 +199,7 @@ ] } }, - "timestamp": "2024-10-28T20:07:50Z", + "timestamp": "2025-02-18T10:58:12Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfstate.json b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfstate.json index 0cee8567db250..d51e2ecb81c71 100644 --- a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfstate.json +++ b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -10,7 +10,7 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -26,19 +26,16 @@ } ], "env": null, - "id": "bcf4bae1-0870-48e9-8bb4-af2f652c4d54", + "id": "a4c46a8c-dd2a-4913-8897-e77b24fdd7f1", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "afe98f25-25a2-4892-b921-be04bcd71efc", + "startup_script_behavior": "non-blocking", + "token": "c263f7b6-c0e7-4106-b3fc-aefbe373ee7a", "troubleshooting_url": null }, "sensitive_values": { @@ -57,7 +54,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "6598177855275264799", + "id": "4299141049988455758", "triggers": null }, "sensitive_values": {}, @@ -74,7 +71,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "4663187895457986148", + "id": "8248139888152642631", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tf b/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tf index 8c7b200fca7b0..86585b6a85357 100644 --- a/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tf +++ b/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfplan.json b/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfplan.json index 3fe9f6c41fa9b..57af82397bd20 100644 --- a/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfplan.json +++ b/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,23 +10,20 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -80,16 +77,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -155,7 +149,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "null": { "name": "null", @@ -178,7 +172,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "null_resource.first", @@ -205,7 +199,7 @@ ] } }, - "timestamp": "2024-10-28T20:07:52Z", + "timestamp": "2025-02-18T10:58:12Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfstate.json b/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfstate.json index ffd0690db2263..f1e9760fcdac1 100644 --- a/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfstate.json +++ b/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -10,7 +10,7 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -26,19 +26,16 @@ } ], "env": null, - "id": "d047c7b6-b69e-4029-ab82-67468a0364f7", + "id": "c5972861-13a8-4c3d-9e7b-c32aab3c5105", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "ceff37e3-52b9-4c80-af1b-1f9f99184590", + "startup_script_behavior": "non-blocking", + "token": "9c2883aa-0c0e-470f-a40c-588b47e663be", "troubleshooting_url": null }, "sensitive_values": { @@ -57,7 +54,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "3120105803817695206", + "id": "4167500156989566756", "triggers": null }, "sensitive_values": {}, @@ -73,7 +70,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "2942451035046396496", + "id": "2831408390006359178", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/display-apps-disabled/display-apps-disabled.tf b/provisioner/terraform/testdata/display-apps-disabled/display-apps-disabled.tf index 494e0acafb48f..155b81889540e 100644 --- a/provisioner/terraform/testdata/display-apps-disabled/display-apps-disabled.tf +++ b/provisioner/terraform/testdata/display-apps-disabled/display-apps-disabled.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/display-apps-disabled/display-apps-disabled.tfplan.json b/provisioner/terraform/testdata/display-apps-disabled/display-apps-disabled.tfplan.json index 598d6f1735a84..f715d1e5b36ef 100644 --- a/provisioner/terraform/testdata/display-apps-disabled/display-apps-disabled.tfplan.json +++ b/provisioner/terraform/testdata/display-apps-disabled/display-apps-disabled.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,7 +10,7 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -26,16 +26,13 @@ } ], "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -88,16 +85,13 @@ } ], "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -146,7 +140,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "null": { "name": "null", @@ -188,7 +182,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "null_resource.dev", @@ -204,7 +198,7 @@ ] } }, - "timestamp": "2024-10-28T20:07:55Z", + "timestamp": "2025-02-18T10:58:12Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/display-apps-disabled/display-apps-disabled.tfstate.json b/provisioner/terraform/testdata/display-apps-disabled/display-apps-disabled.tfstate.json index 7e9bdad7a02bb..8127adf08deb5 100644 --- a/provisioner/terraform/testdata/display-apps-disabled/display-apps-disabled.tfstate.json +++ b/provisioner/terraform/testdata/display-apps-disabled/display-apps-disabled.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -10,7 +10,7 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -26,19 +26,16 @@ } ], "env": null, - "id": "6ba13739-4a9c-456f-90cf-feba8f194853", + "id": "f145f4f8-1d6c-4a66-ba80-abbc077dfe1e", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "6e348a4c-ef00-40ab-9732-817fb828045c", + "startup_script_behavior": "non-blocking", + "token": "612a69b3-4b07-4752-b930-ed7dd36dc926", "troubleshooting_url": null }, "sensitive_values": { @@ -57,7 +54,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "3123606937441446452", + "id": "3571714162665255692", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/display-apps/display-apps.tf b/provisioner/terraform/testdata/display-apps/display-apps.tf index a36b68cd3b1cc..3544ab535ad2f 100644 --- a/provisioner/terraform/testdata/display-apps/display-apps.tf +++ b/provisioner/terraform/testdata/display-apps/display-apps.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/display-apps/display-apps.tfplan.json b/provisioner/terraform/testdata/display-apps/display-apps.tfplan.json index 3331a8f282c2b..b4b3e8d72cb07 100644 --- a/provisioner/terraform/testdata/display-apps/display-apps.tfplan.json +++ b/provisioner/terraform/testdata/display-apps/display-apps.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,7 +10,7 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -26,16 +26,13 @@ } ], "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -88,16 +85,13 @@ } ], "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -146,7 +140,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "null": { "name": "null", @@ -188,7 +182,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "null_resource.dev", @@ -204,7 +198,7 @@ ] } }, - "timestamp": "2024-10-28T20:07:54Z", + "timestamp": "2025-02-18T10:58:12Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/display-apps/display-apps.tfstate.json b/provisioner/terraform/testdata/display-apps/display-apps.tfstate.json index 2b04222e751f2..53be3e3041729 100644 --- a/provisioner/terraform/testdata/display-apps/display-apps.tfstate.json +++ b/provisioner/terraform/testdata/display-apps/display-apps.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -10,7 +10,7 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -26,19 +26,16 @@ } ], "env": null, - "id": "b7e8dd7a-34aa-41e2-977e-e38577ab2476", + "id": "df983aa4-ad0a-458a-acd2-1d5c93e4e4d8", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "c6aeeb35-2766-4524-9818-687f7687831d", + "startup_script_behavior": "non-blocking", + "token": "c2ccd3c2-5ac3-46f5-9620-f1d4c633169f", "troubleshooting_url": null }, "sensitive_values": { @@ -57,7 +54,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "2407243137316459395", + "id": "4058093101918806466", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/external-auth-providers/external-auth-providers.tf b/provisioner/terraform/testdata/external-auth-providers/external-auth-providers.tf index 0b68bbe5710fe..5f45a88aacb6a 100644 --- a/provisioner/terraform/testdata/external-auth-providers/external-auth-providers.tf +++ b/provisioner/terraform/testdata/external-auth-providers/external-auth-providers.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/external-auth-providers/external-auth-providers.tfplan.json b/provisioner/terraform/testdata/external-auth-providers/external-auth-providers.tfplan.json index 5ba9e7b6af80f..fbd2636bfb68d 100644 --- a/provisioner/terraform/testdata/external-auth-providers/external-auth-providers.tfplan.json +++ b/provisioner/terraform/testdata/external-auth-providers/external-auth-providers.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,23 +10,20 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -68,16 +65,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -119,7 +113,7 @@ ], "prior_state": { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -160,7 +154,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "null": { "name": "null", @@ -183,7 +177,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "null_resource.dev", @@ -228,7 +222,7 @@ ] } }, - "timestamp": "2024-10-28T20:07:57Z", + "timestamp": "2025-02-18T10:58:12Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/external-auth-providers/external-auth-providers.tfstate.json b/provisioner/terraform/testdata/external-auth-providers/external-auth-providers.tfstate.json index 875d8c9aaf439..e439476cc9b52 100644 --- a/provisioner/terraform/testdata/external-auth-providers/external-auth-providers.tfstate.json +++ b/provisioner/terraform/testdata/external-auth-providers/external-auth-providers.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -38,7 +38,7 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -54,19 +54,16 @@ } ], "env": null, - "id": "ec5d36c9-8690-4246-8ab3-2d85a3eacee6", + "id": "048746d5-8a05-4615-bdf3-5e0ecda12ba0", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "78c55fa2-8e3c-4564-950d-e022c76cf05a", + "startup_script_behavior": "non-blocking", + "token": "d2a64629-1d18-4704-a3b1-eae300a362d1", "troubleshooting_url": null }, "sensitive_values": { @@ -85,7 +82,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "455343782636271645", + "id": "5369997016721085167", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/generate.sh b/provisioner/terraform/testdata/generate.sh index 6cc79568582ee..72b090dc6b749 100755 --- a/provisioner/terraform/testdata/generate.sh +++ b/provisioner/terraform/testdata/generate.sh @@ -3,37 +3,85 @@ set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")" -for d in */; do - pushd "$d" +generate() { + local name="$1" + + echo "=== BEGIN: $name" + terraform init -upgrade && + terraform plan -out terraform.tfplan && + terraform show -json ./terraform.tfplan | jq >"$name".tfplan.json && + terraform graph -type=plan >"$name".tfplan.dot && + rm terraform.tfplan && + terraform apply -auto-approve && + terraform show -json ./terraform.tfstate | jq >"$name".tfstate.json && + rm terraform.tfstate && + terraform graph -type=plan >"$name".tfstate.dot + ret=$? + echo "=== END: $name" + if [[ $ret -ne 0 ]]; then + return $ret + fi +} + +run() { + d="$1" + cd "$d" name=$(basename "$(pwd)") # This needs care to update correctly. if [[ $name == "kubernetes-metadata" ]]; then - popd - continue + echo "== Skipping: $name" + return 0 fi # This directory is used for a different purpose (quick workaround). if [[ $name == "cleanup-stale-plugins" ]]; then - popd - continue + echo "== Skipping: $name" + return 0 fi if [[ $name == "timings-aggregation" ]]; then - popd - continue + echo "== Skipping: $name" + return 0 + fi + + echo "== Generating test data for: $name" + if ! out="$(generate "$name" 2>&1)"; then + echo "$out" + echo "== Error generating test data for: $name" + return 1 fi + echo "== Done generating test data for: $name" + exit 0 +} + +if [[ " $* " == *" --help "* || " $* " == *" -h "* ]]; then + echo "Usage: $0 [module1 module2 ...]" + exit 0 +fi - terraform init -upgrade - terraform plan -out terraform.tfplan - terraform show -json ./terraform.tfplan | jq >"$name".tfplan.json - terraform graph -type=plan >"$name".tfplan.dot - rm terraform.tfplan - terraform apply -auto-approve - terraform show -json ./terraform.tfstate | jq >"$name".tfstate.json - rm terraform.tfstate - terraform graph -type=plan >"$name".tfstate.dot - popd +declare -a jobs=() +if [[ $# -gt 0 ]]; then + for d in "$@"; do + run "$d" & + jobs+=($!) + done +else + for d in */; do + run "$d" & + jobs+=($!) + done +fi + +err=0 +for job in "${jobs[@]}"; do + if ! wait "$job"; then + err=$((err + 1)) + fi done +if [[ $err -ne 0 ]]; then + echo "ERROR: Failed to generate test data for $err modules" + exit 1 +fi terraform version -json | jq -r '.terraform_version' >version.txt diff --git a/provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tf b/provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tf deleted file mode 100644 index 337699a36cccd..0000000000000 --- a/provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tf +++ /dev/null @@ -1,27 +0,0 @@ -terraform { - required_providers { - coder = { - source = "coder/coder" - version = "0.22.0" - } - } -} - -data "coder_git_auth" "github" { - id = "github" -} - -data "coder_git_auth" "gitlab" { - id = "gitlab" -} - -resource "coder_agent" "main" { - os = "linux" - arch = "amd64" -} - -resource "null_resource" "dev" { - depends_on = [ - coder_agent.main - ] -} diff --git a/provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tfplan.json b/provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tfplan.json deleted file mode 100644 index fba34f1cb5f4d..0000000000000 --- a/provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tfplan.json +++ /dev/null @@ -1,230 +0,0 @@ -{ - "format_version": "1.2", - "terraform_version": "1.9.8", - "planned_values": { - "root_module": { - "resources": [ - { - "address": "coder_agent.main", - "mode": "managed", - "type": "coder_agent", - "name": "main", - "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, - "values": { - "arch": "amd64", - "auth": "token", - "connection_timeout": 120, - "dir": null, - "env": null, - "login_before_ready": true, - "metadata": [], - "motd_file": null, - "order": null, - "os": "linux", - "shutdown_script": null, - "shutdown_script_timeout": 300, - "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "troubleshooting_url": null - }, - "sensitive_values": { - "display_apps": [], - "metadata": [], - "token": true - } - }, - { - "address": "null_resource.dev", - "mode": "managed", - "type": "null_resource", - "name": "dev", - "provider_name": "registry.terraform.io/hashicorp/null", - "schema_version": 0, - "values": { - "triggers": null - }, - "sensitive_values": {} - } - ] - } - }, - "resource_changes": [ - { - "address": "coder_agent.main", - "mode": "managed", - "type": "coder_agent", - "name": "main", - "provider_name": "registry.terraform.io/coder/coder", - "change": { - "actions": [ - "create" - ], - "before": null, - "after": { - "arch": "amd64", - "auth": "token", - "connection_timeout": 120, - "dir": null, - "env": null, - "login_before_ready": true, - "metadata": [], - "motd_file": null, - "order": null, - "os": "linux", - "shutdown_script": null, - "shutdown_script_timeout": 300, - "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "troubleshooting_url": null - }, - "after_unknown": { - "display_apps": true, - "id": true, - "init_script": true, - "metadata": [], - "token": true - }, - "before_sensitive": false, - "after_sensitive": { - "display_apps": [], - "metadata": [], - "token": true - } - } - }, - { - "address": "null_resource.dev", - "mode": "managed", - "type": "null_resource", - "name": "dev", - "provider_name": "registry.terraform.io/hashicorp/null", - "change": { - "actions": [ - "create" - ], - "before": null, - "after": { - "triggers": null - }, - "after_unknown": { - "id": true - }, - "before_sensitive": false, - "after_sensitive": {} - } - } - ], - "prior_state": { - "format_version": "1.0", - "terraform_version": "1.9.8", - "values": { - "root_module": { - "resources": [ - { - "address": "data.coder_git_auth.github", - "mode": "data", - "type": "coder_git_auth", - "name": "github", - "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, - "values": { - "access_token": "", - "id": "github" - }, - "sensitive_values": {} - }, - { - "address": "data.coder_git_auth.gitlab", - "mode": "data", - "type": "coder_git_auth", - "name": "gitlab", - "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, - "values": { - "access_token": "", - "id": "gitlab" - }, - "sensitive_values": {} - } - ] - } - } - }, - "configuration": { - "provider_config": { - "coder": { - "name": "coder", - "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" - }, - "null": { - "name": "null", - "full_name": "registry.terraform.io/hashicorp/null" - } - }, - "root_module": { - "resources": [ - { - "address": "coder_agent.main", - "mode": "managed", - "type": "coder_agent", - "name": "main", - "provider_config_key": "coder", - "expressions": { - "arch": { - "constant_value": "amd64" - }, - "os": { - "constant_value": "linux" - } - }, - "schema_version": 0 - }, - { - "address": "null_resource.dev", - "mode": "managed", - "type": "null_resource", - "name": "dev", - "provider_config_key": "null", - "schema_version": 0, - "depends_on": [ - "coder_agent.main" - ] - }, - { - "address": "data.coder_git_auth.github", - "mode": "data", - "type": "coder_git_auth", - "name": "github", - "provider_config_key": "coder", - "expressions": { - "id": { - "constant_value": "github" - } - }, - "schema_version": 0 - }, - { - "address": "data.coder_git_auth.gitlab", - "mode": "data", - "type": "coder_git_auth", - "name": "gitlab", - "provider_config_key": "coder", - "expressions": { - "id": { - "constant_value": "gitlab" - } - }, - "schema_version": 0 - } - ] - } - }, - "timestamp": "2024-10-28T20:07:58Z", - "applyable": true, - "complete": true, - "errored": false -} diff --git a/provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tfstate.json b/provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tfstate.json deleted file mode 100644 index 3cf905c0a2948..0000000000000 --- a/provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tfstate.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "format_version": "1.0", - "terraform_version": "1.9.8", - "values": { - "root_module": { - "resources": [ - { - "address": "data.coder_git_auth.github", - "mode": "data", - "type": "coder_git_auth", - "name": "github", - "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, - "values": { - "access_token": "", - "id": "github" - }, - "sensitive_values": {} - }, - { - "address": "data.coder_git_auth.gitlab", - "mode": "data", - "type": "coder_git_auth", - "name": "gitlab", - "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, - "values": { - "access_token": "", - "id": "gitlab" - }, - "sensitive_values": {} - }, - { - "address": "coder_agent.main", - "mode": "managed", - "type": "coder_agent", - "name": "main", - "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, - "values": { - "arch": "amd64", - "auth": "token", - "connection_timeout": 120, - "dir": null, - "display_apps": [ - { - "port_forwarding_helper": true, - "ssh_helper": true, - "vscode": true, - "vscode_insiders": false, - "web_terminal": true - } - ], - "env": null, - "id": "ffa1f524-0350-4891-868d-93cad369318a", - "init_script": "", - "login_before_ready": true, - "metadata": [], - "motd_file": null, - "order": null, - "os": "linux", - "shutdown_script": null, - "shutdown_script_timeout": 300, - "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "8ba649af-b498-4f20-8055-b6a0b995837e", - "troubleshooting_url": null - }, - "sensitive_values": { - "display_apps": [ - {} - ], - "metadata": [], - "token": true - } - }, - { - "address": "null_resource.dev", - "mode": "managed", - "type": "null_resource", - "name": "dev", - "provider_name": "registry.terraform.io/hashicorp/null", - "schema_version": 0, - "values": { - "id": "7420557451345159984", - "triggers": null - }, - "sensitive_values": {}, - "depends_on": [ - "coder_agent.main" - ] - } - ] - } - } -} diff --git a/provisioner/terraform/testdata/instance-id/instance-id.tf b/provisioner/terraform/testdata/instance-id/instance-id.tf index 1cd4ab828b4f0..84e010a79d6e9 100644 --- a/provisioner/terraform/testdata/instance-id/instance-id.tf +++ b/provisioner/terraform/testdata/instance-id/instance-id.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/instance-id/instance-id.tfplan.json b/provisioner/terraform/testdata/instance-id/instance-id.tfplan.json index 527a2fa05769d..7c929b496d8fd 100644 --- a/provisioner/terraform/testdata/instance-id/instance-id.tfplan.json +++ b/provisioner/terraform/testdata/instance-id/instance-id.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,23 +10,20 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "google-instance-identity", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -80,16 +77,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -156,7 +150,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "null": { "name": "null", @@ -182,7 +176,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_agent_instance.main", @@ -225,7 +219,7 @@ ] } ], - "timestamp": "2024-10-28T20:08:00Z", + "timestamp": "2025-02-18T10:58:12Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/instance-id/instance-id.tfstate.json b/provisioner/terraform/testdata/instance-id/instance-id.tfstate.json index 929d72365502c..7f7cdfa6a5055 100644 --- a/provisioner/terraform/testdata/instance-id/instance-id.tfstate.json +++ b/provisioner/terraform/testdata/instance-id/instance-id.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -10,7 +10,7 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "google-instance-identity", @@ -26,19 +26,16 @@ } ], "env": null, - "id": "0389c8a5-cc5c-485d-959c-8738bada65ff", + "id": "0b84fffb-d2ca-4048-bdab-7b84229bffba", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "097b6128-8d60-4849-969b-03f0b463ac2c", + "startup_script_behavior": "non-blocking", + "token": "05f05235-a62b-4634-841b-da7fe3763e2e", "troubleshooting_url": null }, "sensitive_values": { @@ -57,8 +54,8 @@ "provider_name": "registry.terraform.io/coder/coder", "schema_version": 0, "values": { - "agent_id": "0389c8a5-cc5c-485d-959c-8738bada65ff", - "id": "0ae6bb98-871c-4091-8098-d32f256d8c05", + "agent_id": "0b84fffb-d2ca-4048-bdab-7b84229bffba", + "id": "7d6e9d00-4cf9-4a38-9b4b-1eb6ba98b50c", "instance_id": "example" }, "sensitive_values": {}, @@ -74,7 +71,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "5569763710827889183", + "id": "446414716532401482", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/kubernetes-metadata/kubernetes-metadata.tf b/provisioner/terraform/testdata/kubernetes-metadata/kubernetes-metadata.tf index 2ae1298904fbb..faa08706de380 100644 --- a/provisioner/terraform/testdata/kubernetes-metadata/kubernetes-metadata.tf +++ b/provisioner/terraform/testdata/kubernetes-metadata/kubernetes-metadata.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } kubernetes = { source = "hashicorp/kubernetes" diff --git a/provisioner/terraform/testdata/mapped-apps/mapped-apps.tf b/provisioner/terraform/testdata/mapped-apps/mapped-apps.tf index 1e13495d6ebc7..7664ead2b4962 100644 --- a/provisioner/terraform/testdata/mapped-apps/mapped-apps.tf +++ b/provisioner/terraform/testdata/mapped-apps/mapped-apps.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/mapped-apps/mapped-apps.tfplan.json b/provisioner/terraform/testdata/mapped-apps/mapped-apps.tfplan.json index 2151b4631647a..dfcf3ccc7b52f 100644 --- a/provisioner/terraform/testdata/mapped-apps/mapped-apps.tfplan.json +++ b/provisioner/terraform/testdata/mapped-apps/mapped-apps.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,23 +10,20 @@ "type": "coder_agent", "name": "dev", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -42,16 +39,16 @@ "name": "apps", "index": "app1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "command": null, "display_name": "app1", "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "name": null, + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app1", "subdomain": null, @@ -68,16 +65,16 @@ "name": "apps", "index": "app2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "command": null, "display_name": "app2", "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "name": null, + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app2", "subdomain": null, @@ -120,16 +117,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -164,10 +158,10 @@ "display_name": "app1", "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "name": null, + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app1", "subdomain": null, @@ -201,10 +195,10 @@ "display_name": "app2", "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "name": null, + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app2", "subdomain": null, @@ -248,7 +242,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "null": { "name": "null", @@ -271,7 +265,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_app.apps", @@ -298,7 +292,7 @@ ] } }, - "schema_version": 0, + "schema_version": 1, "for_each_expression": { "references": [ "local.apps_map" @@ -327,7 +321,7 @@ ] } ], - "timestamp": "2024-10-28T20:08:02Z", + "timestamp": "2025-02-18T10:58:12Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/mapped-apps/mapped-apps.tfstate.json b/provisioner/terraform/testdata/mapped-apps/mapped-apps.tfstate.json index 9aaa7b352f518..ae0acf1650825 100644 --- a/provisioner/terraform/testdata/mapped-apps/mapped-apps.tfstate.json +++ b/provisioner/terraform/testdata/mapped-apps/mapped-apps.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -10,7 +10,7 @@ "type": "coder_agent", "name": "dev", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -26,19 +26,16 @@ } ], "env": null, - "id": "b3d3e1d7-1f1f-4abf-8475-2058f73f3437", + "id": "4b66f4b5-d235-4c57-8b50-7db3643f8070", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "56420fd5-57e5-44e0-a264-53395b74505a", + "startup_script_behavior": "non-blocking", + "token": "a39963f7-3429-453f-b23f-961aa3590f06", "troubleshooting_url": null }, "sensitive_values": { @@ -56,18 +53,18 @@ "name": "apps", "index": "app1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { - "agent_id": "b3d3e1d7-1f1f-4abf-8475-2058f73f3437", + "agent_id": "4b66f4b5-d235-4c57-8b50-7db3643f8070", "command": null, "display_name": "app1", "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "id": "e8163eb0-e56e-46e7-8848-8c6c250ce5b9", - "name": null, + "id": "e67b9091-a454-42ce-85ee-df929f716c4f", + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app1", "subdomain": null, @@ -87,18 +84,18 @@ "name": "apps", "index": "app2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { - "agent_id": "b3d3e1d7-1f1f-4abf-8475-2058f73f3437", + "agent_id": "4b66f4b5-d235-4c57-8b50-7db3643f8070", "command": null, "display_name": "app2", "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "id": "0971e625-7a23-4108-9765-78f7ad045b38", - "name": null, + "id": "84db109a-484c-42cc-b428-866458a99964", + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app2", "subdomain": null, @@ -119,7 +116,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "60927265551659604", + "id": "800496923164467286", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/multiple-agents-multiple-apps/multiple-agents-multiple-apps.tf b/provisioner/terraform/testdata/multiple-agents-multiple-apps/multiple-agents-multiple-apps.tf index 02c6ff6c1b67f..8ac412b5b3894 100644 --- a/provisioner/terraform/testdata/multiple-agents-multiple-apps/multiple-agents-multiple-apps.tf +++ b/provisioner/terraform/testdata/multiple-agents-multiple-apps/multiple-agents-multiple-apps.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/multiple-agents-multiple-apps/multiple-agents-multiple-apps.tfplan.json b/provisioner/terraform/testdata/multiple-agents-multiple-apps/multiple-agents-multiple-apps.tfplan.json index d8f5a4763518b..4ba8c29b7fa77 100644 --- a/provisioner/terraform/testdata/multiple-agents-multiple-apps/multiple-agents-multiple-apps.tfplan.json +++ b/provisioner/terraform/testdata/multiple-agents-multiple-apps/multiple-agents-multiple-apps.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,23 +10,20 @@ "type": "coder_agent", "name": "dev1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -41,23 +38,20 @@ "type": "coder_agent", "name": "dev2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -72,16 +66,16 @@ "type": "coder_app", "name": "app1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "command": null, "display_name": null, "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "name": null, + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app1", "subdomain": null, @@ -97,7 +91,7 @@ "type": "coder_app", "name": "app2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "command": null, "display_name": null, @@ -109,10 +103,10 @@ "url": "http://localhost:13337/healthz" } ], + "hidden": false, "icon": null, - "name": null, + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app2", "subdomain": true, @@ -130,16 +124,16 @@ "type": "coder_app", "name": "app3", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "command": null, "display_name": null, "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "name": null, + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app3", "subdomain": false, @@ -194,16 +188,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -238,16 +229,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -281,10 +269,10 @@ "display_name": null, "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "name": null, + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app1", "subdomain": null, @@ -323,10 +311,10 @@ "url": "http://localhost:13337/healthz" } ], + "hidden": false, "icon": null, - "name": null, + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app2", "subdomain": true, @@ -363,10 +351,10 @@ "display_name": null, "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "name": null, + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app3", "subdomain": false, @@ -431,7 +419,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "null": { "name": "null", @@ -454,7 +442,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_agent.dev2", @@ -470,7 +458,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_app.app1", @@ -489,7 +477,7 @@ "constant_value": "app1" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_app.app2", @@ -524,7 +512,7 @@ "constant_value": true } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_app.app3", @@ -546,7 +534,7 @@ "constant_value": false } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "null_resource.dev1", @@ -587,7 +575,7 @@ ] } ], - "timestamp": "2024-10-28T20:08:05Z", + "timestamp": "2025-02-18T10:58:12Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/multiple-agents-multiple-apps/multiple-agents-multiple-apps.tfstate.json b/provisioner/terraform/testdata/multiple-agents-multiple-apps/multiple-agents-multiple-apps.tfstate.json index 4a94e05baa29d..7ffb9866b4c48 100644 --- a/provisioner/terraform/testdata/multiple-agents-multiple-apps/multiple-agents-multiple-apps.tfstate.json +++ b/provisioner/terraform/testdata/multiple-agents-multiple-apps/multiple-agents-multiple-apps.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -10,7 +10,7 @@ "type": "coder_agent", "name": "dev1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -26,19 +26,16 @@ } ], "env": null, - "id": "571523c7-e7a3-420a-b65d-39d15f5f3267", + "id": "9ba3ef14-bb43-4470-b019-129bf16eb0b2", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "c18d762d-062d-43d4-b7c2-98be546b39a6", + "startup_script_behavior": "non-blocking", + "token": "b40bdbf8-bf41-4822-a71e-03016079ddbe", "troubleshooting_url": null }, "sensitive_values": { @@ -55,7 +52,7 @@ "type": "coder_agent", "name": "dev2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -71,19 +68,16 @@ } ], "env": null, - "id": "e94994f2-cab5-4288-8ff3-a290c95e4e25", + "id": "959048f4-3f1d-4cb0-93da-1dfacdbb7976", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "c0757e3a-4be4-4643-b3ba-b27234169eb1", + "startup_script_behavior": "non-blocking", + "token": "71ef9752-9257-478c-bf5e-c6713a9f5073", "troubleshooting_url": null }, "sensitive_values": { @@ -100,18 +94,18 @@ "type": "coder_app", "name": "app1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { - "agent_id": "571523c7-e7a3-420a-b65d-39d15f5f3267", + "agent_id": "9ba3ef14-bb43-4470-b019-129bf16eb0b2", "command": null, "display_name": null, "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "id": "bf2b3c44-1b1d-49c5-9149-4f2f18590c60", - "name": null, + "id": "f125297a-130c-4c29-a1bf-905f95841fff", + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app1", "subdomain": null, @@ -130,9 +124,9 @@ "type": "coder_app", "name": "app2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { - "agent_id": "571523c7-e7a3-420a-b65d-39d15f5f3267", + "agent_id": "9ba3ef14-bb43-4470-b019-129bf16eb0b2", "command": null, "display_name": null, "external": false, @@ -143,11 +137,11 @@ "url": "http://localhost:13337/healthz" } ], + "hidden": false, "icon": null, - "id": "580cf864-a64d-4430-98b7-fa37c44083f8", - "name": null, + "id": "687e66e5-4888-417d-8fbd-263764dc5011", + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app2", "subdomain": true, @@ -168,18 +162,18 @@ "type": "coder_app", "name": "app3", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { - "agent_id": "e94994f2-cab5-4288-8ff3-a290c95e4e25", + "agent_id": "959048f4-3f1d-4cb0-93da-1dfacdbb7976", "command": null, "display_name": null, "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "id": "182dca7b-12ab-4c58-9424-23b7d61135a9", - "name": null, + "id": "70f10886-fa90-4089-b290-c2d44c5073ae", + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app3", "subdomain": false, @@ -200,7 +194,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "3778543820798621894", + "id": "1056762545519872704", "triggers": null }, "sensitive_values": {}, @@ -216,7 +210,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "1094622314762410115", + "id": "784993046206959042", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/multiple-agents-multiple-envs/multiple-agents-multiple-envs.tf b/provisioner/terraform/testdata/multiple-agents-multiple-envs/multiple-agents-multiple-envs.tf index d167d44942776..e12a895d14baa 100644 --- a/provisioner/terraform/testdata/multiple-agents-multiple-envs/multiple-agents-multiple-envs.tf +++ b/provisioner/terraform/testdata/multiple-agents-multiple-envs/multiple-agents-multiple-envs.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/multiple-agents-multiple-envs/multiple-agents-multiple-envs.tfplan.json b/provisioner/terraform/testdata/multiple-agents-multiple-envs/multiple-agents-multiple-envs.tfplan.json index 4cb28ae592516..7fe81435861e4 100644 --- a/provisioner/terraform/testdata/multiple-agents-multiple-envs/multiple-agents-multiple-envs.tfplan.json +++ b/provisioner/terraform/testdata/multiple-agents-multiple-envs/multiple-agents-multiple-envs.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,23 +10,20 @@ "type": "coder_agent", "name": "dev1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -41,23 +38,20 @@ "type": "coder_agent", "name": "dev2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -72,7 +66,7 @@ "type": "coder_env", "name": "env1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "name": "ENV_1", "value": "Env 1" @@ -85,7 +79,7 @@ "type": "coder_env", "name": "env2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "name": "ENV_2", "value": "Env 2" @@ -98,7 +92,7 @@ "type": "coder_env", "name": "env3", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "name": "ENV_3", "value": "Env 3" @@ -150,16 +144,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -194,16 +185,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -338,7 +326,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "null": { "name": "null", @@ -361,7 +349,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_agent.dev2", @@ -377,7 +365,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_env.env1", @@ -399,7 +387,7 @@ "constant_value": "Env 1" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_env.env2", @@ -421,7 +409,7 @@ "constant_value": "Env 2" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_env.env3", @@ -443,7 +431,7 @@ "constant_value": "Env 3" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "null_resource.dev1", @@ -472,19 +460,19 @@ }, "relevant_attributes": [ { - "resource": "coder_agent.dev1", + "resource": "coder_agent.dev2", "attribute": [ "id" ] }, { - "resource": "coder_agent.dev2", + "resource": "coder_agent.dev1", "attribute": [ "id" ] } ], - "timestamp": "2024-10-28T20:08:06Z", + "timestamp": "2025-02-18T10:58:12Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/multiple-agents-multiple-envs/multiple-agents-multiple-envs.tfstate.json b/provisioner/terraform/testdata/multiple-agents-multiple-envs/multiple-agents-multiple-envs.tfstate.json index f87b6f0a9eb56..f7801ad37220c 100644 --- a/provisioner/terraform/testdata/multiple-agents-multiple-envs/multiple-agents-multiple-envs.tfstate.json +++ b/provisioner/terraform/testdata/multiple-agents-multiple-envs/multiple-agents-multiple-envs.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -10,7 +10,7 @@ "type": "coder_agent", "name": "dev1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -26,19 +26,16 @@ } ], "env": null, - "id": "702e7cd2-95a0-46cf-8ef7-c1dfbd3e56b9", + "id": "5494b9d3-a230-41a4-8f50-be69397ab4cf", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "1cfd79e3-3f9c-4d66-b7c2-42c385c26012", + "startup_script_behavior": "non-blocking", + "token": "84f93622-75a4-4bf1-b806-b981066d4870", "troubleshooting_url": null }, "sensitive_values": { @@ -55,7 +52,7 @@ "type": "coder_agent", "name": "dev2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -71,19 +68,16 @@ } ], "env": null, - "id": "ca137ba9-45ce-44ff-8e30-59a86565fa7d", + "id": "a4cb672c-020b-4729-b451-c7fabba4669c", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "0d3aa4f8-025c-4044-8053-d077484355fb", + "startup_script_behavior": "non-blocking", + "token": "2861b097-2ea6-4c3a-a64c-5a726b9e3700", "troubleshooting_url": null }, "sensitive_values": { @@ -100,10 +94,10 @@ "type": "coder_env", "name": "env1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { - "agent_id": "702e7cd2-95a0-46cf-8ef7-c1dfbd3e56b9", - "id": "e3d37294-2407-4286-a519-7551b901ba54", + "agent_id": "5494b9d3-a230-41a4-8f50-be69397ab4cf", + "id": "4ec31abd-b84a-45b6-80bd-c78eecf387f1", "name": "ENV_1", "value": "Env 1" }, @@ -118,10 +112,10 @@ "type": "coder_env", "name": "env2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { - "agent_id": "702e7cd2-95a0-46cf-8ef7-c1dfbd3e56b9", - "id": "9451575b-da89-4297-a42d-4aaf0a23775d", + "agent_id": "5494b9d3-a230-41a4-8f50-be69397ab4cf", + "id": "c0f4dac3-2b1a-4903-a0f1-2743f2000f1b", "name": "ENV_2", "value": "Env 2" }, @@ -136,10 +130,10 @@ "type": "coder_env", "name": "env3", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { - "agent_id": "ca137ba9-45ce-44ff-8e30-59a86565fa7d", - "id": "948e3fb5-12a1-454b-b85e-d4dc1f01838f", + "agent_id": "a4cb672c-020b-4729-b451-c7fabba4669c", + "id": "e0ccf967-d767-4077-b521-20132af3217a", "name": "ENV_3", "value": "Env 3" }, @@ -156,7 +150,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "7502424400840788651", + "id": "7748417950448815454", "triggers": null }, "sensitive_values": {}, @@ -172,7 +166,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "3916143681500058654", + "id": "1466092153882814278", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/multiple-agents-multiple-monitors/multiple-agents-multiple-monitors.tf b/provisioner/terraform/testdata/multiple-agents-multiple-monitors/multiple-agents-multiple-monitors.tf new file mode 100644 index 0000000000000..f86ceb180edb5 --- /dev/null +++ b/provisioner/terraform/testdata/multiple-agents-multiple-monitors/multiple-agents-multiple-monitors.tf @@ -0,0 +1,67 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "2.2.0-pre0" + } + } +} + +resource "coder_agent" "dev1" { + os = "linux" + arch = "amd64" + resources_monitoring { + memory { + enabled = true + threshold = 80 + } + } +} + +resource "coder_agent" "dev2" { + os = "linux" + arch = "amd64" + resources_monitoring { + memory { + enabled = true + threshold = 99 + } + volume { + path = "/volume1" + enabled = true + threshold = 80 + } + volume { + path = "/volume2" + enabled = false + threshold = 50 + } + } +} + +# app1 is for testing subdomain default. +resource "coder_app" "app1" { + agent_id = coder_agent.dev1.id + slug = "app1" + # subdomain should default to false. + # subdomain = false +} + +# app2 tests that subdomaincan be true, and that healthchecks work. +resource "coder_app" "app2" { + agent_id = coder_agent.dev1.id + slug = "app2" + subdomain = true + healthcheck { + url = "http://localhost:13337/healthz" + interval = 5 + threshold = 6 + } +} + +resource "null_resource" "dev" { + depends_on = [ + coder_agent.dev1, + coder_agent.dev2 + ] +} diff --git a/provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tfplan.dot b/provisioner/terraform/testdata/multiple-agents-multiple-monitors/multiple-agents-multiple-monitors.tfplan.dot similarity index 54% rename from provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tfplan.dot rename to provisioner/terraform/testdata/multiple-agents-multiple-monitors/multiple-agents-multiple-monitors.tfplan.dot index 119f00d4b3840..51af7273b391a 100644 --- a/provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tfplan.dot +++ b/provisioner/terraform/testdata/multiple-agents-multiple-monitors/multiple-agents-multiple-monitors.tfplan.dot @@ -2,20 +2,23 @@ digraph { compound = "true" newrank = "true" subgraph "root" { - "[root] coder_agent.main (expand)" [label = "coder_agent.main", shape = "box"] - "[root] data.coder_git_auth.github (expand)" [label = "data.coder_git_auth.github", shape = "box"] - "[root] data.coder_git_auth.gitlab (expand)" [label = "data.coder_git_auth.gitlab", shape = "box"] + "[root] coder_agent.dev1 (expand)" [label = "coder_agent.dev1", shape = "box"] + "[root] coder_agent.dev2 (expand)" [label = "coder_agent.dev2", shape = "box"] + "[root] coder_app.app1 (expand)" [label = "coder_app.app1", shape = "box"] + "[root] coder_app.app2 (expand)" [label = "coder_app.app2", shape = "box"] "[root] null_resource.dev (expand)" [label = "null_resource.dev", shape = "box"] "[root] provider[\"registry.terraform.io/coder/coder\"]" [label = "provider[\"registry.terraform.io/coder/coder\"]", shape = "diamond"] "[root] provider[\"registry.terraform.io/hashicorp/null\"]" [label = "provider[\"registry.terraform.io/hashicorp/null\"]", shape = "diamond"] - "[root] coder_agent.main (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" - "[root] data.coder_git_auth.github (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" - "[root] data.coder_git_auth.gitlab (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" - "[root] null_resource.dev (expand)" -> "[root] coder_agent.main (expand)" + "[root] coder_agent.dev1 (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] coder_agent.dev2 (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] coder_app.app1 (expand)" -> "[root] coder_agent.dev1 (expand)" + "[root] coder_app.app2 (expand)" -> "[root] coder_agent.dev1 (expand)" + "[root] null_resource.dev (expand)" -> "[root] coder_agent.dev1 (expand)" + "[root] null_resource.dev (expand)" -> "[root] coder_agent.dev2 (expand)" "[root] null_resource.dev (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"]" - "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_agent.main (expand)" - "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] data.coder_git_auth.github (expand)" - "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] data.coder_git_auth.gitlab (expand)" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_agent.dev2 (expand)" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_app.app1 (expand)" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_app.app2 (expand)" "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" -> "[root] null_resource.dev (expand)" "[root] root" -> "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" "[root] root" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" diff --git a/provisioner/terraform/testdata/multiple-agents-multiple-monitors/multiple-agents-multiple-monitors.tfplan.json b/provisioner/terraform/testdata/multiple-agents-multiple-monitors/multiple-agents-multiple-monitors.tfplan.json new file mode 100644 index 0000000000000..b5481b4c89463 --- /dev/null +++ b/provisioner/terraform/testdata/multiple-agents-multiple-monitors/multiple-agents-multiple-monitors.tfplan.json @@ -0,0 +1,625 @@ +{ + "format_version": "1.2", + "terraform_version": "1.10.5", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "coder_agent.dev1", + "mode": "managed", + "type": "coder_agent", + "name": "dev1", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 1, + "values": { + "arch": "amd64", + "auth": "token", + "connection_timeout": 120, + "dir": null, + "env": null, + "metadata": [], + "motd_file": null, + "order": null, + "os": "linux", + "resources_monitoring": [ + { + "memory": [ + { + "enabled": true, + "threshold": 80 + } + ], + "volume": [] + } + ], + "shutdown_script": null, + "startup_script": null, + "startup_script_behavior": "non-blocking", + "troubleshooting_url": null + }, + "sensitive_values": { + "display_apps": [], + "metadata": [], + "resources_monitoring": [ + { + "memory": [ + {} + ], + "volume": [] + } + ], + "token": true + } + }, + { + "address": "coder_agent.dev2", + "mode": "managed", + "type": "coder_agent", + "name": "dev2", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 1, + "values": { + "arch": "amd64", + "auth": "token", + "connection_timeout": 120, + "dir": null, + "env": null, + "metadata": [], + "motd_file": null, + "order": null, + "os": "linux", + "resources_monitoring": [ + { + "memory": [ + { + "enabled": true, + "threshold": 99 + } + ], + "volume": [ + { + "enabled": false, + "path": "/volume2", + "threshold": 50 + }, + { + "enabled": true, + "path": "/volume1", + "threshold": 80 + } + ] + } + ], + "shutdown_script": null, + "startup_script": null, + "startup_script_behavior": "non-blocking", + "troubleshooting_url": null + }, + "sensitive_values": { + "display_apps": [], + "metadata": [], + "resources_monitoring": [ + { + "memory": [ + {} + ], + "volume": [ + {}, + {} + ] + } + ], + "token": true + } + }, + { + "address": "coder_app.app1", + "mode": "managed", + "type": "coder_app", + "name": "app1", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 1, + "values": { + "command": null, + "display_name": null, + "external": false, + "healthcheck": [], + "hidden": false, + "icon": null, + "open_in": "slim-window", + "order": null, + "share": "owner", + "slug": "app1", + "subdomain": null, + "url": null + }, + "sensitive_values": { + "healthcheck": [] + } + }, + { + "address": "coder_app.app2", + "mode": "managed", + "type": "coder_app", + "name": "app2", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 1, + "values": { + "command": null, + "display_name": null, + "external": false, + "healthcheck": [ + { + "interval": 5, + "threshold": 6, + "url": "http://localhost:13337/healthz" + } + ], + "hidden": false, + "icon": null, + "open_in": "slim-window", + "order": null, + "share": "owner", + "slug": "app2", + "subdomain": true, + "url": null + }, + "sensitive_values": { + "healthcheck": [ + {} + ] + } + }, + { + "address": "null_resource.dev", + "mode": "managed", + "type": "null_resource", + "name": "dev", + "provider_name": "registry.terraform.io/hashicorp/null", + "schema_version": 0, + "values": { + "triggers": null + }, + "sensitive_values": {} + } + ] + } + }, + "resource_changes": [ + { + "address": "coder_agent.dev1", + "mode": "managed", + "type": "coder_agent", + "name": "dev1", + "provider_name": "registry.terraform.io/coder/coder", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "arch": "amd64", + "auth": "token", + "connection_timeout": 120, + "dir": null, + "env": null, + "metadata": [], + "motd_file": null, + "order": null, + "os": "linux", + "resources_monitoring": [ + { + "memory": [ + { + "enabled": true, + "threshold": 80 + } + ], + "volume": [] + } + ], + "shutdown_script": null, + "startup_script": null, + "startup_script_behavior": "non-blocking", + "troubleshooting_url": null + }, + "after_unknown": { + "display_apps": true, + "id": true, + "init_script": true, + "metadata": [], + "resources_monitoring": [ + { + "memory": [ + {} + ], + "volume": [] + } + ], + "token": true + }, + "before_sensitive": false, + "after_sensitive": { + "display_apps": [], + "metadata": [], + "resources_monitoring": [ + { + "memory": [ + {} + ], + "volume": [] + } + ], + "token": true + } + } + }, + { + "address": "coder_agent.dev2", + "mode": "managed", + "type": "coder_agent", + "name": "dev2", + "provider_name": "registry.terraform.io/coder/coder", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "arch": "amd64", + "auth": "token", + "connection_timeout": 120, + "dir": null, + "env": null, + "metadata": [], + "motd_file": null, + "order": null, + "os": "linux", + "resources_monitoring": [ + { + "memory": [ + { + "enabled": true, + "threshold": 99 + } + ], + "volume": [ + { + "enabled": false, + "path": "/volume2", + "threshold": 50 + }, + { + "enabled": true, + "path": "/volume1", + "threshold": 80 + } + ] + } + ], + "shutdown_script": null, + "startup_script": null, + "startup_script_behavior": "non-blocking", + "troubleshooting_url": null + }, + "after_unknown": { + "display_apps": true, + "id": true, + "init_script": true, + "metadata": [], + "resources_monitoring": [ + { + "memory": [ + {} + ], + "volume": [ + {}, + {} + ] + } + ], + "token": true + }, + "before_sensitive": false, + "after_sensitive": { + "display_apps": [], + "metadata": [], + "resources_monitoring": [ + { + "memory": [ + {} + ], + "volume": [ + {}, + {} + ] + } + ], + "token": true + } + } + }, + { + "address": "coder_app.app1", + "mode": "managed", + "type": "coder_app", + "name": "app1", + "provider_name": "registry.terraform.io/coder/coder", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "command": null, + "display_name": null, + "external": false, + "healthcheck": [], + "hidden": false, + "icon": null, + "open_in": "slim-window", + "order": null, + "share": "owner", + "slug": "app1", + "subdomain": null, + "url": null + }, + "after_unknown": { + "agent_id": true, + "healthcheck": [], + "id": true + }, + "before_sensitive": false, + "after_sensitive": { + "healthcheck": [] + } + } + }, + { + "address": "coder_app.app2", + "mode": "managed", + "type": "coder_app", + "name": "app2", + "provider_name": "registry.terraform.io/coder/coder", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "command": null, + "display_name": null, + "external": false, + "healthcheck": [ + { + "interval": 5, + "threshold": 6, + "url": "http://localhost:13337/healthz" + } + ], + "hidden": false, + "icon": null, + "open_in": "slim-window", + "order": null, + "share": "owner", + "slug": "app2", + "subdomain": true, + "url": null + }, + "after_unknown": { + "agent_id": true, + "healthcheck": [ + {} + ], + "id": true + }, + "before_sensitive": false, + "after_sensitive": { + "healthcheck": [ + {} + ] + } + } + }, + { + "address": "null_resource.dev", + "mode": "managed", + "type": "null_resource", + "name": "dev", + "provider_name": "registry.terraform.io/hashicorp/null", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "triggers": null + }, + "after_unknown": { + "id": true + }, + "before_sensitive": false, + "after_sensitive": {} + } + } + ], + "configuration": { + "provider_config": { + "coder": { + "name": "coder", + "full_name": "registry.terraform.io/coder/coder", + "version_constraint": "2.2.0-pre0" + }, + "null": { + "name": "null", + "full_name": "registry.terraform.io/hashicorp/null" + } + }, + "root_module": { + "resources": [ + { + "address": "coder_agent.dev1", + "mode": "managed", + "type": "coder_agent", + "name": "dev1", + "provider_config_key": "coder", + "expressions": { + "arch": { + "constant_value": "amd64" + }, + "os": { + "constant_value": "linux" + }, + "resources_monitoring": [ + { + "memory": [ + { + "enabled": { + "constant_value": true + }, + "threshold": { + "constant_value": 80 + } + } + ] + } + ] + }, + "schema_version": 1 + }, + { + "address": "coder_agent.dev2", + "mode": "managed", + "type": "coder_agent", + "name": "dev2", + "provider_config_key": "coder", + "expressions": { + "arch": { + "constant_value": "amd64" + }, + "os": { + "constant_value": "linux" + }, + "resources_monitoring": [ + { + "memory": [ + { + "enabled": { + "constant_value": true + }, + "threshold": { + "constant_value": 99 + } + } + ], + "volume": [ + { + "enabled": { + "constant_value": true + }, + "path": { + "constant_value": "/volume1" + }, + "threshold": { + "constant_value": 80 + } + }, + { + "enabled": { + "constant_value": false + }, + "path": { + "constant_value": "/volume2" + }, + "threshold": { + "constant_value": 50 + } + } + ] + } + ] + }, + "schema_version": 1 + }, + { + "address": "coder_app.app1", + "mode": "managed", + "type": "coder_app", + "name": "app1", + "provider_config_key": "coder", + "expressions": { + "agent_id": { + "references": [ + "coder_agent.dev1.id", + "coder_agent.dev1" + ] + }, + "slug": { + "constant_value": "app1" + } + }, + "schema_version": 1 + }, + { + "address": "coder_app.app2", + "mode": "managed", + "type": "coder_app", + "name": "app2", + "provider_config_key": "coder", + "expressions": { + "agent_id": { + "references": [ + "coder_agent.dev1.id", + "coder_agent.dev1" + ] + }, + "healthcheck": [ + { + "interval": { + "constant_value": 5 + }, + "threshold": { + "constant_value": 6 + }, + "url": { + "constant_value": "http://localhost:13337/healthz" + } + } + ], + "slug": { + "constant_value": "app2" + }, + "subdomain": { + "constant_value": true + } + }, + "schema_version": 1 + }, + { + "address": "null_resource.dev", + "mode": "managed", + "type": "null_resource", + "name": "dev", + "provider_config_key": "null", + "schema_version": 0, + "depends_on": [ + "coder_agent.dev1", + "coder_agent.dev2" + ] + } + ] + } + }, + "relevant_attributes": [ + { + "resource": "coder_agent.dev1", + "attribute": [ + "id" + ] + } + ], + "timestamp": "2025-02-18T10:58:12Z", + "applyable": true, + "complete": true, + "errored": false +} diff --git a/provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tfstate.dot b/provisioner/terraform/testdata/multiple-agents-multiple-monitors/multiple-agents-multiple-monitors.tfstate.dot similarity index 54% rename from provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tfstate.dot rename to provisioner/terraform/testdata/multiple-agents-multiple-monitors/multiple-agents-multiple-monitors.tfstate.dot index 119f00d4b3840..51af7273b391a 100644 --- a/provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tfstate.dot +++ b/provisioner/terraform/testdata/multiple-agents-multiple-monitors/multiple-agents-multiple-monitors.tfstate.dot @@ -2,20 +2,23 @@ digraph { compound = "true" newrank = "true" subgraph "root" { - "[root] coder_agent.main (expand)" [label = "coder_agent.main", shape = "box"] - "[root] data.coder_git_auth.github (expand)" [label = "data.coder_git_auth.github", shape = "box"] - "[root] data.coder_git_auth.gitlab (expand)" [label = "data.coder_git_auth.gitlab", shape = "box"] + "[root] coder_agent.dev1 (expand)" [label = "coder_agent.dev1", shape = "box"] + "[root] coder_agent.dev2 (expand)" [label = "coder_agent.dev2", shape = "box"] + "[root] coder_app.app1 (expand)" [label = "coder_app.app1", shape = "box"] + "[root] coder_app.app2 (expand)" [label = "coder_app.app2", shape = "box"] "[root] null_resource.dev (expand)" [label = "null_resource.dev", shape = "box"] "[root] provider[\"registry.terraform.io/coder/coder\"]" [label = "provider[\"registry.terraform.io/coder/coder\"]", shape = "diamond"] "[root] provider[\"registry.terraform.io/hashicorp/null\"]" [label = "provider[\"registry.terraform.io/hashicorp/null\"]", shape = "diamond"] - "[root] coder_agent.main (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" - "[root] data.coder_git_auth.github (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" - "[root] data.coder_git_auth.gitlab (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" - "[root] null_resource.dev (expand)" -> "[root] coder_agent.main (expand)" + "[root] coder_agent.dev1 (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] coder_agent.dev2 (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] coder_app.app1 (expand)" -> "[root] coder_agent.dev1 (expand)" + "[root] coder_app.app2 (expand)" -> "[root] coder_agent.dev1 (expand)" + "[root] null_resource.dev (expand)" -> "[root] coder_agent.dev1 (expand)" + "[root] null_resource.dev (expand)" -> "[root] coder_agent.dev2 (expand)" "[root] null_resource.dev (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"]" - "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_agent.main (expand)" - "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] data.coder_git_auth.github (expand)" - "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] data.coder_git_auth.gitlab (expand)" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_agent.dev2 (expand)" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_app.app1 (expand)" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_app.app2 (expand)" "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" -> "[root] null_resource.dev (expand)" "[root] root" -> "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" "[root] root" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" diff --git a/provisioner/terraform/testdata/multiple-agents-multiple-monitors/multiple-agents-multiple-monitors.tfstate.json b/provisioner/terraform/testdata/multiple-agents-multiple-monitors/multiple-agents-multiple-monitors.tfstate.json new file mode 100644 index 0000000000000..85ef0a7ccddad --- /dev/null +++ b/provisioner/terraform/testdata/multiple-agents-multiple-monitors/multiple-agents-multiple-monitors.tfstate.json @@ -0,0 +1,231 @@ +{ + "format_version": "1.0", + "terraform_version": "1.10.5", + "values": { + "root_module": { + "resources": [ + { + "address": "coder_agent.dev1", + "mode": "managed", + "type": "coder_agent", + "name": "dev1", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 1, + "values": { + "arch": "amd64", + "auth": "token", + "connection_timeout": 120, + "dir": null, + "display_apps": [ + { + "port_forwarding_helper": true, + "ssh_helper": true, + "vscode": true, + "vscode_insiders": false, + "web_terminal": true + } + ], + "env": null, + "id": "9c36f8be-874a-40f6-a395-f37d6d910a83", + "init_script": "", + "metadata": [], + "motd_file": null, + "order": null, + "os": "linux", + "resources_monitoring": [ + { + "memory": [ + { + "enabled": true, + "threshold": 80 + } + ], + "volume": [] + } + ], + "shutdown_script": null, + "startup_script": null, + "startup_script_behavior": "non-blocking", + "token": "1bed5f78-a309-4049-9805-b5f52a17306d", + "troubleshooting_url": null + }, + "sensitive_values": { + "display_apps": [ + {} + ], + "metadata": [], + "resources_monitoring": [ + { + "memory": [ + {} + ], + "volume": [] + } + ], + "token": true + } + }, + { + "address": "coder_agent.dev2", + "mode": "managed", + "type": "coder_agent", + "name": "dev2", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 1, + "values": { + "arch": "amd64", + "auth": "token", + "connection_timeout": 120, + "dir": null, + "display_apps": [ + { + "port_forwarding_helper": true, + "ssh_helper": true, + "vscode": true, + "vscode_insiders": false, + "web_terminal": true + } + ], + "env": null, + "id": "23009046-30ce-40d4-81f4-f8e7726335a5", + "init_script": "", + "metadata": [], + "motd_file": null, + "order": null, + "os": "linux", + "resources_monitoring": [ + { + "memory": [ + { + "enabled": true, + "threshold": 99 + } + ], + "volume": [ + { + "enabled": false, + "path": "/volume2", + "threshold": 50 + }, + { + "enabled": true, + "path": "/volume1", + "threshold": 80 + } + ] + } + ], + "shutdown_script": null, + "startup_script": null, + "startup_script_behavior": "non-blocking", + "token": "3d40e367-25e5-43a3-8b7a-8528b31edbbd", + "troubleshooting_url": null + }, + "sensitive_values": { + "display_apps": [ + {} + ], + "metadata": [], + "resources_monitoring": [ + { + "memory": [ + {} + ], + "volume": [ + {}, + {} + ] + } + ], + "token": true + } + }, + { + "address": "coder_app.app1", + "mode": "managed", + "type": "coder_app", + "name": "app1", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 1, + "values": { + "agent_id": "9c36f8be-874a-40f6-a395-f37d6d910a83", + "command": null, + "display_name": null, + "external": false, + "healthcheck": [], + "hidden": false, + "icon": null, + "id": "c8ff409a-d30d-4e62-a5a1-771f90d712ca", + "open_in": "slim-window", + "order": null, + "share": "owner", + "slug": "app1", + "subdomain": null, + "url": null + }, + "sensitive_values": { + "healthcheck": [] + }, + "depends_on": [ + "coder_agent.dev1" + ] + }, + { + "address": "coder_app.app2", + "mode": "managed", + "type": "coder_app", + "name": "app2", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 1, + "values": { + "agent_id": "9c36f8be-874a-40f6-a395-f37d6d910a83", + "command": null, + "display_name": null, + "external": false, + "healthcheck": [ + { + "interval": 5, + "threshold": 6, + "url": "http://localhost:13337/healthz" + } + ], + "hidden": false, + "icon": null, + "id": "23c1f02f-cc1a-4e64-b64f-dc2294781c14", + "open_in": "slim-window", + "order": null, + "share": "owner", + "slug": "app2", + "subdomain": true, + "url": null + }, + "sensitive_values": { + "healthcheck": [ + {} + ] + }, + "depends_on": [ + "coder_agent.dev1" + ] + }, + { + "address": "null_resource.dev", + "mode": "managed", + "type": "null_resource", + "name": "dev", + "provider_name": "registry.terraform.io/hashicorp/null", + "schema_version": 0, + "values": { + "id": "4679211063326469519", + "triggers": null + }, + "sensitive_values": {}, + "depends_on": [ + "coder_agent.dev1", + "coder_agent.dev2" + ] + } + ] + } + } +} diff --git a/provisioner/terraform/testdata/multiple-agents-multiple-scripts/multiple-agents-multiple-scripts.tf b/provisioner/terraform/testdata/multiple-agents-multiple-scripts/multiple-agents-multiple-scripts.tf index af041e2da350d..c0aee0d2d97e5 100644 --- a/provisioner/terraform/testdata/multiple-agents-multiple-scripts/multiple-agents-multiple-scripts.tf +++ b/provisioner/terraform/testdata/multiple-agents-multiple-scripts/multiple-agents-multiple-scripts.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/multiple-agents-multiple-scripts/multiple-agents-multiple-scripts.tfplan.json b/provisioner/terraform/testdata/multiple-agents-multiple-scripts/multiple-agents-multiple-scripts.tfplan.json index ab14e49f02989..628c97c8563ff 100644 --- a/provisioner/terraform/testdata/multiple-agents-multiple-scripts/multiple-agents-multiple-scripts.tfplan.json +++ b/provisioner/terraform/testdata/multiple-agents-multiple-scripts/multiple-agents-multiple-scripts.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,23 +10,20 @@ "type": "coder_agent", "name": "dev1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -41,23 +38,20 @@ "type": "coder_agent", "name": "dev2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -72,7 +66,7 @@ "type": "coder_script", "name": "script1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "cron": null, "display_name": "Foobar Script 1", @@ -92,7 +86,7 @@ "type": "coder_script", "name": "script2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "cron": null, "display_name": "Foobar Script 2", @@ -112,7 +106,7 @@ "type": "coder_script", "name": "script3", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "cron": null, "display_name": "Foobar Script 3", @@ -171,16 +165,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -215,16 +206,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -380,7 +368,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "null": { "name": "null", @@ -403,7 +391,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_agent.dev2", @@ -419,7 +407,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_script.script1", @@ -444,7 +432,7 @@ "constant_value": "echo foobar 1" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_script.script2", @@ -469,7 +457,7 @@ "constant_value": "echo foobar 2" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_script.script3", @@ -494,7 +482,7 @@ "constant_value": "echo foobar 3" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "null_resource.dev1", @@ -535,7 +523,7 @@ ] } ], - "timestamp": "2024-10-28T20:08:08Z", + "timestamp": "2025-02-18T10:58:12Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/multiple-agents-multiple-scripts/multiple-agents-multiple-scripts.tfstate.json b/provisioner/terraform/testdata/multiple-agents-multiple-scripts/multiple-agents-multiple-scripts.tfstate.json index 37c4ef13ee6fb..918dccb57bd11 100644 --- a/provisioner/terraform/testdata/multiple-agents-multiple-scripts/multiple-agents-multiple-scripts.tfstate.json +++ b/provisioner/terraform/testdata/multiple-agents-multiple-scripts/multiple-agents-multiple-scripts.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -10,7 +10,7 @@ "type": "coder_agent", "name": "dev1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -26,19 +26,16 @@ } ], "env": null, - "id": "753eb8c0-e2b7-4cbc-b0ff-1370ce2e4022", + "id": "56eebdd7-8348-439a-8ee9-3cd9a4967479", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "77b179b6-0e2d-4307-9ba0-98325fc96e37", + "startup_script_behavior": "non-blocking", + "token": "bc6f97e3-265d-49e9-b08b-e2bc38736da0", "troubleshooting_url": null }, "sensitive_values": { @@ -55,7 +52,7 @@ "type": "coder_agent", "name": "dev2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -71,19 +68,16 @@ } ], "env": null, - "id": "86f7e422-1798-4de5-8209-69b023808241", + "id": "36b8da5b-7a03-4da7-a081-f4ae599d7302", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "aa4ae02d-4084-4dff-951c-af10f78a98c2", + "startup_script_behavior": "non-blocking", + "token": "fa30098e-d8d2-4dad-87ad-3e0a328d2084", "troubleshooting_url": null }, "sensitive_values": { @@ -100,13 +94,13 @@ "type": "coder_script", "name": "script1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { - "agent_id": "753eb8c0-e2b7-4cbc-b0ff-1370ce2e4022", + "agent_id": "56eebdd7-8348-439a-8ee9-3cd9a4967479", "cron": null, "display_name": "Foobar Script 1", "icon": null, - "id": "eb1eb8f4-3a4a-4040-bd6a-0abce01d6330", + "id": "29d2f25b-f774-4bb8-9ef4-9aa03a4b3765", "log_path": null, "run_on_start": true, "run_on_stop": false, @@ -125,13 +119,13 @@ "type": "coder_script", "name": "script2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { - "agent_id": "753eb8c0-e2b7-4cbc-b0ff-1370ce2e4022", + "agent_id": "56eebdd7-8348-439a-8ee9-3cd9a4967479", "cron": null, "display_name": "Foobar Script 2", "icon": null, - "id": "1de43abc-8416-4455-87ca-23fb425b4eeb", + "id": "7e7a2376-3028-493c-8ce1-665efd6c5d9c", "log_path": null, "run_on_start": true, "run_on_stop": false, @@ -150,13 +144,13 @@ "type": "coder_script", "name": "script3", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { - "agent_id": "86f7e422-1798-4de5-8209-69b023808241", + "agent_id": "36b8da5b-7a03-4da7-a081-f4ae599d7302", "cron": null, "display_name": "Foobar Script 3", "icon": null, - "id": "ede835f7-4018-464c-807d-7e07af7de9d3", + "id": "c6c46bde-7eff-462b-805b-82597a8095d2", "log_path": null, "run_on_start": true, "run_on_stop": false, @@ -177,7 +171,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "4207133259459553257", + "id": "3047178084751259009", "triggers": null }, "sensitive_values": {}, @@ -193,7 +187,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "5647997484430231619", + "id": "6983265822377125070", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tf b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tf index d44a981d168bb..b9187beb93acf 100644 --- a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tf +++ b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } @@ -17,10 +17,8 @@ resource "coder_agent" "dev2" { arch = "amd64" connection_timeout = 1 motd_file = "/etc/motd" - startup_script_timeout = 30 startup_script_behavior = "non-blocking" shutdown_script = "echo bye bye" - shutdown_script_timeout = 30 } resource "coder_agent" "dev3" { @@ -34,7 +32,6 @@ resource "coder_agent" "dev4" { os = "linux" arch = "amd64" # Test deprecated login_before_ready=false => startup_script_behavior=blocking. - login_before_ready = false } resource "null_resource" "dev" { diff --git a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfplan.json b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfplan.json index 67da167932aa4..bf0bd8b21d340 100644 --- a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfplan.json +++ b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,23 +10,20 @@ "type": "coder_agent", "name": "dev1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -41,23 +38,20 @@ "type": "coder_agent", "name": "dev2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", "connection_timeout": 1, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": "/etc/motd", "order": null, "os": "darwin", "shutdown_script": "echo bye bye", - "shutdown_script_timeout": 30, "startup_script": null, "startup_script_behavior": "non-blocking", - "startup_script_timeout": 30, "troubleshooting_url": null }, "sensitive_values": { @@ -72,23 +66,20 @@ "type": "coder_agent", "name": "dev3", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "arm64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "windows", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, "startup_script_behavior": "blocking", - "startup_script_timeout": 300, "troubleshooting_url": "https://coder.com/troubleshoot" }, "sensitive_values": { @@ -103,23 +94,20 @@ "type": "coder_agent", "name": "dev4", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": false, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -161,16 +149,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -205,16 +190,13 @@ "connection_timeout": 1, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": "/etc/motd", "order": null, "os": "darwin", "shutdown_script": "echo bye bye", - "shutdown_script_timeout": 30, "startup_script": null, "startup_script_behavior": "non-blocking", - "startup_script_timeout": 30, "troubleshooting_url": null }, "after_unknown": { @@ -249,16 +231,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "windows", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, "startup_script_behavior": "blocking", - "startup_script_timeout": 300, "troubleshooting_url": "https://coder.com/troubleshoot" }, "after_unknown": { @@ -293,16 +272,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": false, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -347,7 +323,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "null": { "name": "null", @@ -370,7 +346,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_agent.dev2", @@ -394,17 +370,11 @@ "shutdown_script": { "constant_value": "echo bye bye" }, - "shutdown_script_timeout": { - "constant_value": 30 - }, "startup_script_behavior": { "constant_value": "non-blocking" - }, - "startup_script_timeout": { - "constant_value": 30 } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_agent.dev3", @@ -426,7 +396,7 @@ "constant_value": "https://coder.com/troubleshoot" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_agent.dev4", @@ -438,14 +408,11 @@ "arch": { "constant_value": "amd64" }, - "login_before_ready": { - "constant_value": false - }, "os": { "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "null_resource.dev", @@ -464,7 +431,7 @@ ] } }, - "timestamp": "2024-10-28T20:08:03Z", + "timestamp": "2025-02-18T10:58:12Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfstate.json b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfstate.json index cd8edc0ae29bc..71987deb178cc 100644 --- a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfstate.json +++ b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -10,7 +10,7 @@ "type": "coder_agent", "name": "dev1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -26,19 +26,16 @@ } ], "env": null, - "id": "c76ed902-d4cb-4905-9961-4d58dda135f9", + "id": "f65fcb62-ef69-44e8-b8eb-56224c9e9d6f", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "f1aa99ea-570d-49cf-aef9-a4241e3cb023", + "startup_script_behavior": "non-blocking", + "token": "57047ef7-1433-4938-a604-4dd2812b1039", "troubleshooting_url": null }, "sensitive_values": { @@ -55,7 +52,7 @@ "type": "coder_agent", "name": "dev2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -71,19 +68,16 @@ } ], "env": null, - "id": "1b037439-4eb3-408e-83da-28dc93645944", + "id": "d366a56f-2899-4e96-b0a1-3e97ac9bd834", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": "/etc/motd", "order": null, "os": "darwin", "shutdown_script": "echo bye bye", - "shutdown_script_timeout": 30, "startup_script": null, "startup_script_behavior": "non-blocking", - "startup_script_timeout": 30, - "token": "20d4e89e-d6de-4eb7-8877-f9186d684aa5", + "token": "59a6c328-d6ac-450d-a507-de6c14cb16d0", "troubleshooting_url": null }, "sensitive_values": { @@ -100,7 +94,7 @@ "type": "coder_agent", "name": "dev3", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "arm64", "auth": "token", @@ -116,19 +110,16 @@ } ], "env": null, - "id": "453b5404-8ea4-4197-8664-3638e6a012ca", + "id": "907bbf6b-fa77-4138-a348-ef5d0fb98b15", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "windows", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, "startup_script_behavior": "blocking", - "startup_script_timeout": 300, - "token": "0355cb42-9da0-4bad-b2aa-74db1df76fef", + "token": "7f0bb618-c82a-491b-891a-6d9f3abeeca0", "troubleshooting_url": "https://coder.com/troubleshoot" }, "sensitive_values": { @@ -145,7 +136,7 @@ "type": "coder_agent", "name": "dev4", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -161,19 +152,16 @@ } ], "env": null, - "id": "c0a68e9b-5b29-4d95-b664-5ac71dd633cf", + "id": "e9b11e47-0238-4915-9539-ac06617f3398", "init_script": "", - "login_before_ready": false, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "34b78439-5d6e-431b-b06c-339f97a1e9cf", + "startup_script_behavior": "non-blocking", + "token": "102a2043-9a42-4490-b0b4-c4fb215552e0", "troubleshooting_url": null }, "sensitive_values": { @@ -192,7 +180,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "5109814714394194897", + "id": "2948336473894256689", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tf b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tf index c7c4f9968b5c3..c52f4a58b36f4 100644 --- a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tf +++ b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfplan.json b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfplan.json index b156c3b5068b6..3f18f84cf30ec 100644 --- a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfplan.json +++ b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,23 +10,20 @@ "type": "coder_agent", "name": "dev1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -41,16 +38,16 @@ "type": "coder_app", "name": "app1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "command": null, "display_name": null, "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "name": null, + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app1", "subdomain": null, @@ -66,7 +63,7 @@ "type": "coder_app", "name": "app2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "command": null, "display_name": null, @@ -78,10 +75,10 @@ "url": "http://localhost:13337/healthz" } ], + "hidden": false, "icon": null, - "name": null, + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app2", "subdomain": true, @@ -99,16 +96,16 @@ "type": "coder_app", "name": "app3", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "command": null, "display_name": null, "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "name": null, + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app3", "subdomain": false, @@ -151,16 +148,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -194,10 +188,10 @@ "display_name": null, "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "name": null, + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app1", "subdomain": null, @@ -236,10 +230,10 @@ "url": "http://localhost:13337/healthz" } ], + "hidden": false, "icon": null, - "name": null, + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app2", "subdomain": true, @@ -276,10 +270,10 @@ "display_name": null, "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "name": null, + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app3", "subdomain": false, @@ -323,7 +317,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "null": { "name": "null", @@ -346,7 +340,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_app.app1", @@ -365,7 +359,7 @@ "constant_value": "app1" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_app.app2", @@ -400,7 +394,7 @@ "constant_value": true } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_app.app3", @@ -422,7 +416,7 @@ "constant_value": false } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "null_resource.dev", @@ -446,7 +440,7 @@ ] } ], - "timestamp": "2024-10-28T20:08:10Z", + "timestamp": "2025-02-18T10:58:12Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfstate.json b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfstate.json index d3fc254bf40b0..9a21887d3ed4b 100644 --- a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfstate.json +++ b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -10,7 +10,7 @@ "type": "coder_agent", "name": "dev1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -26,19 +26,16 @@ } ], "env": null, - "id": "b3ea3cb0-176c-4642-9bf5-cfa72e0782cc", + "id": "e7f1e434-ad52-4175-b8d1-4fab9fbe7891", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "30533677-f04a-493b-b6cb-314d9abf7769", + "startup_script_behavior": "non-blocking", + "token": "da1c4966-5bb7-459e-8b7e-ce1cf189e49d", "troubleshooting_url": null }, "sensitive_values": { @@ -55,18 +52,18 @@ "type": "coder_app", "name": "app1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { - "agent_id": "b3ea3cb0-176c-4642-9bf5-cfa72e0782cc", + "agent_id": "e7f1e434-ad52-4175-b8d1-4fab9fbe7891", "command": null, "display_name": null, "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "id": "537e9069-492b-4721-96dd-cffba275ecd9", - "name": null, + "id": "41882acb-ad8c-4436-a756-e55160e2eba7", + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app1", "subdomain": null, @@ -85,9 +82,9 @@ "type": "coder_app", "name": "app2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { - "agent_id": "b3ea3cb0-176c-4642-9bf5-cfa72e0782cc", + "agent_id": "e7f1e434-ad52-4175-b8d1-4fab9fbe7891", "command": null, "display_name": null, "external": false, @@ -98,11 +95,11 @@ "url": "http://localhost:13337/healthz" } ], + "hidden": false, "icon": null, - "id": "3a4c78a0-7ea3-44aa-9ea8-4e08e387b4b6", - "name": null, + "id": "28fb460e-746b-47b9-8c88-fc546f2ca6c4", + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app2", "subdomain": true, @@ -123,18 +120,18 @@ "type": "coder_app", "name": "app3", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { - "agent_id": "b3ea3cb0-176c-4642-9bf5-cfa72e0782cc", + "agent_id": "e7f1e434-ad52-4175-b8d1-4fab9fbe7891", "command": null, "display_name": null, "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "id": "23555681-0ecb-4962-8e85-367d3a9d0228", - "name": null, + "id": "2751d89f-6c41-4b50-9982-9270ba0660b0", + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app3", "subdomain": false, @@ -155,7 +152,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "2905101599123333983", + "id": "1493563047742372481", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/presets/external-module/child-external-module/main.tf b/provisioner/terraform/testdata/presets/external-module/child-external-module/main.tf new file mode 100644 index 0000000000000..ac6f4c621a9d0 --- /dev/null +++ b/provisioner/terraform/testdata/presets/external-module/child-external-module/main.tf @@ -0,0 +1,28 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "0.22.0" + } + docker = { + source = "kreuzwerker/docker" + version = "~> 2.22" + } + } +} + +data "coder_parameter" "child_first_parameter_from_module" { + name = "First parameter from child module" + mutable = true + type = "string" + description = "First parameter from child module" + default = "abcdef" +} + +data "coder_parameter" "child_second_parameter_from_module" { + name = "Second parameter from child module" + mutable = true + type = "string" + description = "Second parameter from child module" + default = "ghijkl" +} diff --git a/provisioner/terraform/testdata/presets/external-module/main.tf b/provisioner/terraform/testdata/presets/external-module/main.tf new file mode 100644 index 0000000000000..55e942ec24e1f --- /dev/null +++ b/provisioner/terraform/testdata/presets/external-module/main.tf @@ -0,0 +1,32 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "0.22.0" + } + docker = { + source = "kreuzwerker/docker" + version = "~> 2.22" + } + } +} + +module "this_is_external_child_module" { + source = "./child-external-module" +} + +data "coder_parameter" "first_parameter_from_module" { + name = "First parameter from module" + mutable = true + type = "string" + description = "First parameter from module" + default = "abcdef" +} + +data "coder_parameter" "second_parameter_from_module" { + name = "Second parameter from module" + mutable = true + type = "string" + description = "Second parameter from module" + default = "ghijkl" +} diff --git a/provisioner/terraform/testdata/presets/presets.tf b/provisioner/terraform/testdata/presets/presets.tf new file mode 100644 index 0000000000000..cb372930d48b0 --- /dev/null +++ b/provisioner/terraform/testdata/presets/presets.tf @@ -0,0 +1,39 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "0.22.0" + } + } +} + +module "this_is_external_module" { + source = "./external-module" +} + +data "coder_parameter" "sample" { + name = "Sample" + type = "string" + description = "blah blah" + default = "ok" +} + +data "coder_workspace_preset" "MyFirstProject" { + name = "My First Project" + parameters = { + (data.coder_parameter.sample.name) = "A1B2C3" + # TODO (sasswart): Add support for parameters from external modules + # (data.coder_parameter.first_parameter_from_module.name) = "A1B2C3" + # (data.coder_parameter.child_first_parameter_from_module.name) = "A1B2C3" + } +} + +resource "coder_agent" "dev" { + os = "windows" + arch = "arm64" +} + +resource "null_resource" "dev" { + depends_on = [coder_agent.dev] +} + diff --git a/provisioner/terraform/testdata/presets/presets.tfplan.dot b/provisioner/terraform/testdata/presets/presets.tfplan.dot new file mode 100644 index 0000000000000..bc545095b9d7a --- /dev/null +++ b/provisioner/terraform/testdata/presets/presets.tfplan.dot @@ -0,0 +1,45 @@ +digraph { + compound = "true" + newrank = "true" + subgraph "root" { + "[root] coder_agent.dev (expand)" [label = "coder_agent.dev", shape = "box"] + "[root] data.coder_parameter.sample (expand)" [label = "data.coder_parameter.sample", shape = "box"] + "[root] data.coder_workspace_preset.MyFirstProject (expand)" [label = "data.coder_workspace_preset.MyFirstProject", shape = "box"] + "[root] module.this_is_external_module.data.coder_parameter.first_parameter_from_module (expand)" [label = "module.this_is_external_module.data.coder_parameter.first_parameter_from_module", shape = "box"] + "[root] module.this_is_external_module.data.coder_parameter.second_parameter_from_module (expand)" [label = "module.this_is_external_module.data.coder_parameter.second_parameter_from_module", shape = "box"] + "[root] module.this_is_external_module.module.this_is_external_child_module.data.coder_parameter.child_first_parameter_from_module (expand)" [label = "module.this_is_external_module.module.this_is_external_child_module.data.coder_parameter.child_first_parameter_from_module", shape = "box"] + "[root] module.this_is_external_module.module.this_is_external_child_module.data.coder_parameter.child_second_parameter_from_module (expand)" [label = "module.this_is_external_module.module.this_is_external_child_module.data.coder_parameter.child_second_parameter_from_module", shape = "box"] + "[root] null_resource.dev (expand)" [label = "null_resource.dev", shape = "box"] + "[root] provider[\"registry.terraform.io/coder/coder\"]" [label = "provider[\"registry.terraform.io/coder/coder\"]", shape = "diamond"] + "[root] provider[\"registry.terraform.io/hashicorp/null\"]" [label = "provider[\"registry.terraform.io/hashicorp/null\"]", shape = "diamond"] + "[root] coder_agent.dev (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] data.coder_parameter.sample (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] data.coder_workspace_preset.MyFirstProject (expand)" -> "[root] data.coder_parameter.sample (expand)" + "[root] module.this_is_external_module (close)" -> "[root] module.this_is_external_module.data.coder_parameter.first_parameter_from_module (expand)" + "[root] module.this_is_external_module (close)" -> "[root] module.this_is_external_module.data.coder_parameter.second_parameter_from_module (expand)" + "[root] module.this_is_external_module (close)" -> "[root] module.this_is_external_module.module.this_is_external_child_module (close)" + "[root] module.this_is_external_module.data.coder_parameter.first_parameter_from_module (expand)" -> "[root] module.this_is_external_module (expand)" + "[root] module.this_is_external_module.data.coder_parameter.first_parameter_from_module (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] module.this_is_external_module.data.coder_parameter.second_parameter_from_module (expand)" -> "[root] module.this_is_external_module (expand)" + "[root] module.this_is_external_module.data.coder_parameter.second_parameter_from_module (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] module.this_is_external_module.module.this_is_external_child_module (close)" -> "[root] module.this_is_external_module.module.this_is_external_child_module.data.coder_parameter.child_first_parameter_from_module (expand)" + "[root] module.this_is_external_module.module.this_is_external_child_module (close)" -> "[root] module.this_is_external_module.module.this_is_external_child_module.data.coder_parameter.child_second_parameter_from_module (expand)" + "[root] module.this_is_external_module.module.this_is_external_child_module (expand)" -> "[root] module.this_is_external_module (expand)" + "[root] module.this_is_external_module.module.this_is_external_child_module.data.coder_parameter.child_first_parameter_from_module (expand)" -> "[root] module.this_is_external_module.module.this_is_external_child_module (expand)" + "[root] module.this_is_external_module.module.this_is_external_child_module.data.coder_parameter.child_first_parameter_from_module (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] module.this_is_external_module.module.this_is_external_child_module.data.coder_parameter.child_second_parameter_from_module (expand)" -> "[root] module.this_is_external_module.module.this_is_external_child_module (expand)" + "[root] module.this_is_external_module.module.this_is_external_child_module.data.coder_parameter.child_second_parameter_from_module (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] null_resource.dev (expand)" -> "[root] coder_agent.dev (expand)" + "[root] null_resource.dev (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"]" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_agent.dev (expand)" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] data.coder_workspace_preset.MyFirstProject (expand)" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] module.this_is_external_module.data.coder_parameter.first_parameter_from_module (expand)" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] module.this_is_external_module.data.coder_parameter.second_parameter_from_module (expand)" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] module.this_is_external_module.module.this_is_external_child_module.data.coder_parameter.child_first_parameter_from_module (expand)" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] module.this_is_external_module.module.this_is_external_child_module.data.coder_parameter.child_second_parameter_from_module (expand)" + "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" -> "[root] null_resource.dev (expand)" + "[root] root" -> "[root] module.this_is_external_module (close)" + "[root] root" -> "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" + "[root] root" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" + } +} diff --git a/provisioner/terraform/testdata/presets/presets.tfplan.json b/provisioner/terraform/testdata/presets/presets.tfplan.json new file mode 100644 index 0000000000000..6ee4b6705c975 --- /dev/null +++ b/provisioner/terraform/testdata/presets/presets.tfplan.json @@ -0,0 +1,504 @@ +{ + "format_version": "1.2", + "terraform_version": "1.9.8", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "coder_agent.dev", + "mode": "managed", + "type": "coder_agent", + "name": "dev", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 1, + "values": { + "arch": "arm64", + "auth": "token", + "connection_timeout": 120, + "dir": null, + "env": null, + "metadata": [], + "motd_file": null, + "order": null, + "os": "windows", + "shutdown_script": null, + "startup_script": null, + "startup_script_behavior": "non-blocking", + "troubleshooting_url": null + }, + "sensitive_values": { + "display_apps": [], + "metadata": [], + "token": true + } + }, + { + "address": "null_resource.dev", + "mode": "managed", + "type": "null_resource", + "name": "dev", + "provider_name": "registry.terraform.io/hashicorp/null", + "schema_version": 0, + "values": { + "triggers": null + }, + "sensitive_values": {} + } + ] + } + }, + "resource_changes": [ + { + "address": "coder_agent.dev", + "mode": "managed", + "type": "coder_agent", + "name": "dev", + "provider_name": "registry.terraform.io/coder/coder", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "arch": "arm64", + "auth": "token", + "connection_timeout": 120, + "dir": null, + "env": null, + "metadata": [], + "motd_file": null, + "order": null, + "os": "windows", + "shutdown_script": null, + "startup_script": null, + "startup_script_behavior": "non-blocking", + "troubleshooting_url": null + }, + "after_unknown": { + "display_apps": true, + "id": true, + "init_script": true, + "metadata": [], + "token": true + }, + "before_sensitive": false, + "after_sensitive": { + "display_apps": [], + "metadata": [], + "token": true + } + } + }, + { + "address": "null_resource.dev", + "mode": "managed", + "type": "null_resource", + "name": "dev", + "provider_name": "registry.terraform.io/hashicorp/null", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "triggers": null + }, + "after_unknown": { + "id": true + }, + "before_sensitive": false, + "after_sensitive": {} + } + } + ], + "prior_state": { + "format_version": "1.0", + "terraform_version": "1.9.8", + "values": { + "root_module": { + "resources": [ + { + "address": "data.coder_parameter.sample", + "mode": "data", + "type": "coder_parameter", + "name": "sample", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "default": "ok", + "description": "blah blah", + "display_name": null, + "ephemeral": false, + "icon": null, + "id": "1e5ebd18-fd9e-435e-9b85-d5dded4b2d69", + "mutable": false, + "name": "Sample", + "option": null, + "optional": true, + "order": null, + "type": "string", + "validation": [], + "value": "ok" + }, + "sensitive_values": { + "validation": [] + } + }, + { + "address": "data.coder_workspace_preset.MyFirstProject", + "mode": "data", + "type": "coder_workspace_preset", + "name": "MyFirstProject", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "id": "My First Project", + "name": "My First Project", + "parameters": { + "Sample": "A1B2C3" + } + }, + "sensitive_values": { + "parameters": {} + } + } + ], + "child_modules": [ + { + "resources": [ + { + "address": "module.this_is_external_module.data.coder_parameter.first_parameter_from_module", + "mode": "data", + "type": "coder_parameter", + "name": "first_parameter_from_module", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "default": "abcdef", + "description": "First parameter from module", + "display_name": null, + "ephemeral": false, + "icon": null, + "id": "600375fe-cb06-4d7d-92b6-8e2c93d4d9dd", + "mutable": true, + "name": "First parameter from module", + "option": null, + "optional": true, + "order": null, + "type": "string", + "validation": [], + "value": "abcdef" + }, + "sensitive_values": { + "validation": [] + } + }, + { + "address": "module.this_is_external_module.data.coder_parameter.second_parameter_from_module", + "mode": "data", + "type": "coder_parameter", + "name": "second_parameter_from_module", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "default": "ghijkl", + "description": "Second parameter from module", + "display_name": null, + "ephemeral": false, + "icon": null, + "id": "c58f2ba6-9db3-49aa-8795-33fdb18f3e67", + "mutable": true, + "name": "Second parameter from module", + "option": null, + "optional": true, + "order": null, + "type": "string", + "validation": [], + "value": "ghijkl" + }, + "sensitive_values": { + "validation": [] + } + } + ], + "address": "module.this_is_external_module", + "child_modules": [ + { + "resources": [ + { + "address": "module.this_is_external_module.module.this_is_external_child_module.data.coder_parameter.child_first_parameter_from_module", + "mode": "data", + "type": "coder_parameter", + "name": "child_first_parameter_from_module", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "default": "abcdef", + "description": "First parameter from child module", + "display_name": null, + "ephemeral": false, + "icon": null, + "id": "7d212d9b-f6cb-4611-989e-4512d4f86c10", + "mutable": true, + "name": "First parameter from child module", + "option": null, + "optional": true, + "order": null, + "type": "string", + "validation": [], + "value": "abcdef" + }, + "sensitive_values": { + "validation": [] + } + }, + { + "address": "module.this_is_external_module.module.this_is_external_child_module.data.coder_parameter.child_second_parameter_from_module", + "mode": "data", + "type": "coder_parameter", + "name": "child_second_parameter_from_module", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "default": "ghijkl", + "description": "Second parameter from child module", + "display_name": null, + "ephemeral": false, + "icon": null, + "id": "6f71825d-4332-4f1c-a8d9-8bc118fa6a45", + "mutable": true, + "name": "Second parameter from child module", + "option": null, + "optional": true, + "order": null, + "type": "string", + "validation": [], + "value": "ghijkl" + }, + "sensitive_values": { + "validation": [] + } + } + ], + "address": "module.this_is_external_module.module.this_is_external_child_module" + } + ] + } + ] + } + } + }, + "configuration": { + "provider_config": { + "coder": { + "name": "coder", + "full_name": "registry.terraform.io/coder/coder", + "version_constraint": "0.22.0" + }, + "module.this_is_external_module:docker": { + "name": "docker", + "full_name": "registry.terraform.io/kreuzwerker/docker", + "version_constraint": "~> 2.22", + "module_address": "module.this_is_external_module" + }, + "null": { + "name": "null", + "full_name": "registry.terraform.io/hashicorp/null" + } + }, + "root_module": { + "resources": [ + { + "address": "coder_agent.dev", + "mode": "managed", + "type": "coder_agent", + "name": "dev", + "provider_config_key": "coder", + "expressions": { + "arch": { + "constant_value": "arm64" + }, + "os": { + "constant_value": "windows" + } + }, + "schema_version": 1 + }, + { + "address": "null_resource.dev", + "mode": "managed", + "type": "null_resource", + "name": "dev", + "provider_config_key": "null", + "schema_version": 0, + "depends_on": [ + "coder_agent.dev" + ] + }, + { + "address": "data.coder_parameter.sample", + "mode": "data", + "type": "coder_parameter", + "name": "sample", + "provider_config_key": "coder", + "expressions": { + "default": { + "constant_value": "ok" + }, + "description": { + "constant_value": "blah blah" + }, + "name": { + "constant_value": "Sample" + }, + "type": { + "constant_value": "string" + } + }, + "schema_version": 0 + }, + { + "address": "data.coder_workspace_preset.MyFirstProject", + "mode": "data", + "type": "coder_workspace_preset", + "name": "MyFirstProject", + "provider_config_key": "coder", + "expressions": { + "name": { + "constant_value": "My First Project" + }, + "parameters": { + "references": [ + "data.coder_parameter.sample.name", + "data.coder_parameter.sample" + ] + } + }, + "schema_version": 0 + } + ], + "module_calls": { + "this_is_external_module": { + "source": "./external-module", + "module": { + "resources": [ + { + "address": "data.coder_parameter.first_parameter_from_module", + "mode": "data", + "type": "coder_parameter", + "name": "first_parameter_from_module", + "provider_config_key": "coder", + "expressions": { + "default": { + "constant_value": "abcdef" + }, + "description": { + "constant_value": "First parameter from module" + }, + "mutable": { + "constant_value": true + }, + "name": { + "constant_value": "First parameter from module" + }, + "type": { + "constant_value": "string" + } + }, + "schema_version": 0 + }, + { + "address": "data.coder_parameter.second_parameter_from_module", + "mode": "data", + "type": "coder_parameter", + "name": "second_parameter_from_module", + "provider_config_key": "coder", + "expressions": { + "default": { + "constant_value": "ghijkl" + }, + "description": { + "constant_value": "Second parameter from module" + }, + "mutable": { + "constant_value": true + }, + "name": { + "constant_value": "Second parameter from module" + }, + "type": { + "constant_value": "string" + } + }, + "schema_version": 0 + } + ], + "module_calls": { + "this_is_external_child_module": { + "source": "./child-external-module", + "module": { + "resources": [ + { + "address": "data.coder_parameter.child_first_parameter_from_module", + "mode": "data", + "type": "coder_parameter", + "name": "child_first_parameter_from_module", + "provider_config_key": "coder", + "expressions": { + "default": { + "constant_value": "abcdef" + }, + "description": { + "constant_value": "First parameter from child module" + }, + "mutable": { + "constant_value": true + }, + "name": { + "constant_value": "First parameter from child module" + }, + "type": { + "constant_value": "string" + } + }, + "schema_version": 0 + }, + { + "address": "data.coder_parameter.child_second_parameter_from_module", + "mode": "data", + "type": "coder_parameter", + "name": "child_second_parameter_from_module", + "provider_config_key": "coder", + "expressions": { + "default": { + "constant_value": "ghijkl" + }, + "description": { + "constant_value": "Second parameter from child module" + }, + "mutable": { + "constant_value": true + }, + "name": { + "constant_value": "Second parameter from child module" + }, + "type": { + "constant_value": "string" + } + }, + "schema_version": 0 + } + ] + } + } + } + } + } + } + } + }, + "timestamp": "2025-02-06T07:28:26Z", + "applyable": true, + "complete": true, + "errored": false +} diff --git a/provisioner/terraform/testdata/presets/presets.tfstate.dot b/provisioner/terraform/testdata/presets/presets.tfstate.dot new file mode 100644 index 0000000000000..bc545095b9d7a --- /dev/null +++ b/provisioner/terraform/testdata/presets/presets.tfstate.dot @@ -0,0 +1,45 @@ +digraph { + compound = "true" + newrank = "true" + subgraph "root" { + "[root] coder_agent.dev (expand)" [label = "coder_agent.dev", shape = "box"] + "[root] data.coder_parameter.sample (expand)" [label = "data.coder_parameter.sample", shape = "box"] + "[root] data.coder_workspace_preset.MyFirstProject (expand)" [label = "data.coder_workspace_preset.MyFirstProject", shape = "box"] + "[root] module.this_is_external_module.data.coder_parameter.first_parameter_from_module (expand)" [label = "module.this_is_external_module.data.coder_parameter.first_parameter_from_module", shape = "box"] + "[root] module.this_is_external_module.data.coder_parameter.second_parameter_from_module (expand)" [label = "module.this_is_external_module.data.coder_parameter.second_parameter_from_module", shape = "box"] + "[root] module.this_is_external_module.module.this_is_external_child_module.data.coder_parameter.child_first_parameter_from_module (expand)" [label = "module.this_is_external_module.module.this_is_external_child_module.data.coder_parameter.child_first_parameter_from_module", shape = "box"] + "[root] module.this_is_external_module.module.this_is_external_child_module.data.coder_parameter.child_second_parameter_from_module (expand)" [label = "module.this_is_external_module.module.this_is_external_child_module.data.coder_parameter.child_second_parameter_from_module", shape = "box"] + "[root] null_resource.dev (expand)" [label = "null_resource.dev", shape = "box"] + "[root] provider[\"registry.terraform.io/coder/coder\"]" [label = "provider[\"registry.terraform.io/coder/coder\"]", shape = "diamond"] + "[root] provider[\"registry.terraform.io/hashicorp/null\"]" [label = "provider[\"registry.terraform.io/hashicorp/null\"]", shape = "diamond"] + "[root] coder_agent.dev (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] data.coder_parameter.sample (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] data.coder_workspace_preset.MyFirstProject (expand)" -> "[root] data.coder_parameter.sample (expand)" + "[root] module.this_is_external_module (close)" -> "[root] module.this_is_external_module.data.coder_parameter.first_parameter_from_module (expand)" + "[root] module.this_is_external_module (close)" -> "[root] module.this_is_external_module.data.coder_parameter.second_parameter_from_module (expand)" + "[root] module.this_is_external_module (close)" -> "[root] module.this_is_external_module.module.this_is_external_child_module (close)" + "[root] module.this_is_external_module.data.coder_parameter.first_parameter_from_module (expand)" -> "[root] module.this_is_external_module (expand)" + "[root] module.this_is_external_module.data.coder_parameter.first_parameter_from_module (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] module.this_is_external_module.data.coder_parameter.second_parameter_from_module (expand)" -> "[root] module.this_is_external_module (expand)" + "[root] module.this_is_external_module.data.coder_parameter.second_parameter_from_module (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] module.this_is_external_module.module.this_is_external_child_module (close)" -> "[root] module.this_is_external_module.module.this_is_external_child_module.data.coder_parameter.child_first_parameter_from_module (expand)" + "[root] module.this_is_external_module.module.this_is_external_child_module (close)" -> "[root] module.this_is_external_module.module.this_is_external_child_module.data.coder_parameter.child_second_parameter_from_module (expand)" + "[root] module.this_is_external_module.module.this_is_external_child_module (expand)" -> "[root] module.this_is_external_module (expand)" + "[root] module.this_is_external_module.module.this_is_external_child_module.data.coder_parameter.child_first_parameter_from_module (expand)" -> "[root] module.this_is_external_module.module.this_is_external_child_module (expand)" + "[root] module.this_is_external_module.module.this_is_external_child_module.data.coder_parameter.child_first_parameter_from_module (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] module.this_is_external_module.module.this_is_external_child_module.data.coder_parameter.child_second_parameter_from_module (expand)" -> "[root] module.this_is_external_module.module.this_is_external_child_module (expand)" + "[root] module.this_is_external_module.module.this_is_external_child_module.data.coder_parameter.child_second_parameter_from_module (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]" + "[root] null_resource.dev (expand)" -> "[root] coder_agent.dev (expand)" + "[root] null_resource.dev (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"]" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_agent.dev (expand)" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] data.coder_workspace_preset.MyFirstProject (expand)" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] module.this_is_external_module.data.coder_parameter.first_parameter_from_module (expand)" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] module.this_is_external_module.data.coder_parameter.second_parameter_from_module (expand)" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] module.this_is_external_module.module.this_is_external_child_module.data.coder_parameter.child_first_parameter_from_module (expand)" + "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] module.this_is_external_module.module.this_is_external_child_module.data.coder_parameter.child_second_parameter_from_module (expand)" + "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" -> "[root] null_resource.dev (expand)" + "[root] root" -> "[root] module.this_is_external_module (close)" + "[root] root" -> "[root] provider[\"registry.terraform.io/coder/coder\"] (close)" + "[root] root" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" + } +} diff --git a/provisioner/terraform/testdata/presets/presets.tfstate.json b/provisioner/terraform/testdata/presets/presets.tfstate.json new file mode 100644 index 0000000000000..c85a1ed6ee7ea --- /dev/null +++ b/provisioner/terraform/testdata/presets/presets.tfstate.json @@ -0,0 +1,235 @@ +{ + "format_version": "1.0", + "terraform_version": "1.9.8", + "values": { + "root_module": { + "resources": [ + { + "address": "data.coder_parameter.sample", + "mode": "data", + "type": "coder_parameter", + "name": "sample", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "default": "ok", + "description": "blah blah", + "display_name": null, + "ephemeral": false, + "icon": null, + "id": "2919245a-ab45-4d7e-8b12-eab87c8dae93", + "mutable": false, + "name": "Sample", + "option": null, + "optional": true, + "order": null, + "type": "string", + "validation": [], + "value": "ok" + }, + "sensitive_values": { + "validation": [] + } + }, + { + "address": "data.coder_workspace_preset.MyFirstProject", + "mode": "data", + "type": "coder_workspace_preset", + "name": "MyFirstProject", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "id": "My First Project", + "name": "My First Project", + "parameters": { + "Sample": "A1B2C3" + } + }, + "sensitive_values": { + "parameters": {} + } + }, + { + "address": "coder_agent.dev", + "mode": "managed", + "type": "coder_agent", + "name": "dev", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 1, + "values": { + "arch": "arm64", + "auth": "token", + "connection_timeout": 120, + "dir": null, + "display_apps": [ + { + "port_forwarding_helper": true, + "ssh_helper": true, + "vscode": true, + "vscode_insiders": false, + "web_terminal": true + } + ], + "env": null, + "id": "409b5e6b-e062-4597-9d52-e1b9995fbcbc", + "init_script": "", + "metadata": [], + "motd_file": null, + "order": null, + "os": "windows", + "shutdown_script": null, + "startup_script": null, + "startup_script_behavior": "non-blocking", + "token": "4ffba3f0-5f6f-4c81-8cc7-1e85f9585e26", + "troubleshooting_url": null + }, + "sensitive_values": { + "display_apps": [ + {} + ], + "metadata": [], + "token": true + } + }, + { + "address": "null_resource.dev", + "mode": "managed", + "type": "null_resource", + "name": "dev", + "provider_name": "registry.terraform.io/hashicorp/null", + "schema_version": 0, + "values": { + "id": "5205838407378573477", + "triggers": null + }, + "sensitive_values": {}, + "depends_on": [ + "coder_agent.dev" + ] + } + ], + "child_modules": [ + { + "resources": [ + { + "address": "module.this_is_external_module.data.coder_parameter.first_parameter_from_module", + "mode": "data", + "type": "coder_parameter", + "name": "first_parameter_from_module", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "default": "abcdef", + "description": "First parameter from module", + "display_name": null, + "ephemeral": false, + "icon": null, + "id": "754b099d-7ee7-4716-83fa-cd9afc746a1f", + "mutable": true, + "name": "First parameter from module", + "option": null, + "optional": true, + "order": null, + "type": "string", + "validation": [], + "value": "abcdef" + }, + "sensitive_values": { + "validation": [] + } + }, + { + "address": "module.this_is_external_module.data.coder_parameter.second_parameter_from_module", + "mode": "data", + "type": "coder_parameter", + "name": "second_parameter_from_module", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "default": "ghijkl", + "description": "Second parameter from module", + "display_name": null, + "ephemeral": false, + "icon": null, + "id": "0a4e4511-d8bd-47b9-bb7a-ffddd09c7da4", + "mutable": true, + "name": "Second parameter from module", + "option": null, + "optional": true, + "order": null, + "type": "string", + "validation": [], + "value": "ghijkl" + }, + "sensitive_values": { + "validation": [] + } + } + ], + "address": "module.this_is_external_module", + "child_modules": [ + { + "resources": [ + { + "address": "module.this_is_external_module.module.this_is_external_child_module.data.coder_parameter.child_first_parameter_from_module", + "mode": "data", + "type": "coder_parameter", + "name": "child_first_parameter_from_module", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "default": "abcdef", + "description": "First parameter from child module", + "display_name": null, + "ephemeral": false, + "icon": null, + "id": "1c981b95-6d26-4222-96e8-6552e43ecb51", + "mutable": true, + "name": "First parameter from child module", + "option": null, + "optional": true, + "order": null, + "type": "string", + "validation": [], + "value": "abcdef" + }, + "sensitive_values": { + "validation": [] + } + }, + { + "address": "module.this_is_external_module.module.this_is_external_child_module.data.coder_parameter.child_second_parameter_from_module", + "mode": "data", + "type": "coder_parameter", + "name": "child_second_parameter_from_module", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "default": "ghijkl", + "description": "Second parameter from child module", + "display_name": null, + "ephemeral": false, + "icon": null, + "id": "f4667b4c-217f-494d-9811-7f8b58913c43", + "mutable": true, + "name": "Second parameter from child module", + "option": null, + "optional": true, + "order": null, + "type": "string", + "validation": [], + "value": "ghijkl" + }, + "sensitive_values": { + "validation": [] + } + } + ], + "address": "module.this_is_external_module.module.this_is_external_child_module" + } + ] + } + ] + } + } +} diff --git a/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tf b/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tf index b316db7c3cdf1..b88a672f0047a 100644 --- a/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tf +++ b/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tfplan.json b/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tfplan.json index 3b7881701038c..078f6a63738f8 100644 --- a/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tfplan.json +++ b/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,14 +10,13 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [ { "display_name": "Process Count", @@ -32,10 +31,8 @@ "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -52,7 +49,7 @@ "type": "coder_metadata", "name": "about_info", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "daily_cost": 29, "hide": true, @@ -83,7 +80,7 @@ "type": "coder_metadata", "name": "other_info", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "daily_cost": 20, "hide": true, @@ -135,7 +132,6 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [ { "display_name": "Process Count", @@ -150,10 +146,8 @@ "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -291,7 +285,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "null": { "name": "null", @@ -333,7 +327,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_metadata.about_info", @@ -373,7 +367,7 @@ ] } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_metadata.other_info", @@ -408,7 +402,7 @@ ] } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "null_resource.about", @@ -432,7 +426,7 @@ ] } ], - "timestamp": "2024-10-28T20:08:13Z", + "timestamp": "2025-02-18T10:58:12Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tfstate.json b/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tfstate.json index 170630d0e3103..79b8ec551eb4d 100644 --- a/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tfstate.json +++ b/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -10,7 +10,7 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -26,9 +26,8 @@ } ], "env": null, - "id": "0cbc2449-fbaa-447a-8487-6c47367af0be", + "id": "febc1e16-503f-42c3-b1ab-b067d172a860", "init_script": "", - "login_before_ready": true, "metadata": [ { "display_name": "Process Count", @@ -43,11 +42,9 @@ "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "b03606cc-1ed3-4187-964d-389cf2ef223f", + "startup_script_behavior": "non-blocking", + "token": "2b609454-ea6a-4ec8-ba03-d305712894d1", "troubleshooting_url": null }, "sensitive_values": { @@ -66,12 +63,12 @@ "type": "coder_metadata", "name": "about_info", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "daily_cost": 29, "hide": true, "icon": "/icon/server.svg", - "id": "d6c33b98-addd-4d97-8659-405350bc06c1", + "id": "0ea63fbe-3e81-4c34-9edc-c2b1ddc62c46", "item": [ { "is_null": false, @@ -86,7 +83,7 @@ "value": "" } ], - "resource_id": "5673227143105805783" + "resource_id": "856574543079218847" }, "sensitive_values": { "item": [ @@ -105,12 +102,12 @@ "type": "coder_metadata", "name": "other_info", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "daily_cost": 20, "hide": true, "icon": "/icon/server.svg", - "id": "76594f08-2261-4114-a61f-e07107a86f89", + "id": "2a367f6b-b055-425c-bdc0-7c63cafdc146", "item": [ { "is_null": false, @@ -119,7 +116,7 @@ "value": "world" } ], - "resource_id": "5673227143105805783" + "resource_id": "856574543079218847" }, "sensitive_values": { "item": [ @@ -139,7 +136,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "5673227143105805783", + "id": "856574543079218847", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tf b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tf index cd46057ce8526..eb9f2eff89877 100644 --- a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tf +++ b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfplan.json b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfplan.json index f9c24830c6ef3..f3f97e8b96897 100644 --- a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfplan.json +++ b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,14 +10,13 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [ { "display_name": "Process Count", @@ -32,10 +31,8 @@ "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -52,7 +49,7 @@ "type": "coder_metadata", "name": "about_info", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "daily_cost": 29, "hide": true, @@ -122,7 +119,6 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [ { "display_name": "Process Count", @@ -137,10 +133,8 @@ "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -256,7 +250,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "null": { "name": "null", @@ -301,7 +295,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_metadata.about_info", @@ -360,7 +354,7 @@ ] } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "null_resource.about", @@ -384,7 +378,7 @@ ] } ], - "timestamp": "2024-10-28T20:08:11Z", + "timestamp": "2025-02-18T10:58:12Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfstate.json b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfstate.json index a41aff216b11c..5089c0b42e3e7 100644 --- a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfstate.json +++ b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -10,7 +10,7 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -26,9 +26,8 @@ } ], "env": null, - "id": "3bcbc547-b434-4dbd-b5ed-551edfba1b5c", + "id": "bf7c9d15-6b61-4012-9cd8-10ba7ca9a4d8", "init_script": "", - "login_before_ready": true, "metadata": [ { "display_name": "Process Count", @@ -43,11 +42,9 @@ "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "2d25fcc3-a355-4e92-98c6-ab780894ffee", + "startup_script_behavior": "non-blocking", + "token": "91d4aa20-db80-4404-a68c-a19abeb4a5b9", "troubleshooting_url": null }, "sensitive_values": { @@ -66,12 +63,12 @@ "type": "coder_metadata", "name": "about_info", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "daily_cost": 29, "hide": true, "icon": "/icon/server.svg", - "id": "d9ce721c-dff3-44fd-92d1-155f37c84a56", + "id": "b96f5efa-fe45-4a6a-9bd2-70e2063b7b2a", "item": [ { "is_null": false, @@ -98,7 +95,7 @@ "value": "squirrel" } ], - "resource_id": "4099397325680267994" + "resource_id": "978725577783936679" }, "sensitive_values": { "item": [ @@ -121,7 +118,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "4099397325680267994", + "id": "978725577783936679", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/rich-parameters-order/rich-parameters-order.tf b/provisioner/terraform/testdata/rich-parameters-order/rich-parameters-order.tf index 82e7a6f95694e..fc684a6e583ee 100644 --- a/provisioner/terraform/testdata/rich-parameters-order/rich-parameters-order.tf +++ b/provisioner/terraform/testdata/rich-parameters-order/rich-parameters-order.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/rich-parameters-order/rich-parameters-order.tfplan.json b/provisioner/terraform/testdata/rich-parameters-order/rich-parameters-order.tfplan.json index 72120dfaabeec..46ac62ce6f09e 100644 --- a/provisioner/terraform/testdata/rich-parameters-order/rich-parameters-order.tfplan.json +++ b/provisioner/terraform/testdata/rich-parameters-order/rich-parameters-order.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,23 +10,20 @@ "type": "coder_agent", "name": "dev", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "arm64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "windows", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -68,16 +65,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "windows", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -119,7 +113,7 @@ ], "prior_state": { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -136,7 +130,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "e8805d7c-1636-4416-9520-b83234d68ddc", + "id": "b106fb5a-0ab1-4530-8cc0-9ff9a515dff4", "mutable": false, "name": "Example", "option": null, @@ -163,7 +157,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "df43829a-49ce-4911-97ef-2fca78456c9f", + "id": "5b1c2605-c7a4-4248-bf92-b761e36e0111", "mutable": false, "name": "Sample", "option": null, @@ -186,7 +180,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "null": { "name": "null", @@ -209,7 +203,7 @@ "constant_value": "windows" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "null_resource.dev", @@ -269,7 +263,7 @@ ] } }, - "timestamp": "2024-10-28T20:08:17Z", + "timestamp": "2025-02-18T10:58:12Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/rich-parameters-order/rich-parameters-order.tfstate.json b/provisioner/terraform/testdata/rich-parameters-order/rich-parameters-order.tfstate.json index 1d675d685a37c..bade7edb803c5 100644 --- a/provisioner/terraform/testdata/rich-parameters-order/rich-parameters-order.tfstate.json +++ b/provisioner/terraform/testdata/rich-parameters-order/rich-parameters-order.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -17,7 +17,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "81ada233-3a30-49d3-a56f-aca92f19c411", + "id": "3f56c659-fe68-47c3-9765-cd09abe69de7", "mutable": false, "name": "Example", "option": null, @@ -44,7 +44,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "4dc1049f-0d54-408a-a412-95629ae5cd84", + "id": "2ecde94b-399a-43c7-b50a-3603895aff83", "mutable": false, "name": "Sample", "option": null, @@ -64,7 +64,7 @@ "type": "coder_agent", "name": "dev", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "arm64", "auth": "token", @@ -80,19 +80,16 @@ } ], "env": null, - "id": "86cc4d6e-23b3-4632-9bc9-d3a321e8b906", + "id": "a2171da1-5f68-446f-97e3-1c2755552840", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "windows", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "0c3e7639-bafc-4e62-8e38-cb4e1b44e3f3", + "startup_script_behavior": "non-blocking", + "token": "a986f085-2697-4d95-a431-6545716ca36b", "troubleshooting_url": null }, "sensitive_values": { @@ -111,7 +108,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "2501594036325466407", + "id": "5482122353677678043", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/rich-parameters-validation/rich-parameters-validation.tf b/provisioner/terraform/testdata/rich-parameters-validation/rich-parameters-validation.tf index c05e8d5d4ae32..8067c0fa9337c 100644 --- a/provisioner/terraform/testdata/rich-parameters-validation/rich-parameters-validation.tf +++ b/provisioner/terraform/testdata/rich-parameters-validation/rich-parameters-validation.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/rich-parameters-validation/rich-parameters-validation.tfplan.json b/provisioner/terraform/testdata/rich-parameters-validation/rich-parameters-validation.tfplan.json index 66153605ee4a0..1f7a216dc7a3f 100644 --- a/provisioner/terraform/testdata/rich-parameters-validation/rich-parameters-validation.tfplan.json +++ b/provisioner/terraform/testdata/rich-parameters-validation/rich-parameters-validation.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,23 +10,20 @@ "type": "coder_agent", "name": "dev", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "arm64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "windows", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -68,16 +65,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "windows", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -119,7 +113,7 @@ ], "prior_state": { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -136,7 +130,7 @@ "display_name": null, "ephemeral": true, "icon": null, - "id": "df8ad066-047d-434d-baa3-e19517ee7395", + "id": "65767637-5ffa-400f-be3f-f03868bd7070", "mutable": true, "name": "number_example", "option": null, @@ -163,7 +157,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "7d9658aa-ff69-477a-9063-e9fd49fd9a9b", + "id": "d8ee017a-1a92-43f2-aaa8-483573c08485", "mutable": false, "name": "number_example_max", "option": null, @@ -202,7 +196,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "bd6fcaac-db7f-4c4d-a664-fe7f47fad28a", + "id": "1516f72d-71aa-4ae8-95b5-4dbcf999e173", "mutable": false, "name": "number_example_max_zero", "option": null, @@ -241,7 +235,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "8d42942d-5a10-43c9-a31d-d3fe9a7814e8", + "id": "720ff4a2-4f26-42d5-a0f8-4e5c92b3133e", "mutable": false, "name": "number_example_min", "option": null, @@ -280,7 +274,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "695301d0-8325-4685-824d-1ca9591689e3", + "id": "395bcef8-1f59-4a4f-b104-f0c4b6686193", "mutable": false, "name": "number_example_min_max", "option": null, @@ -319,7 +313,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "cd921934-d1b1-4370-8a73-2d43658ea877", + "id": "29b2943d-e736-4635-a553-097ebe51e7ec", "mutable": false, "name": "number_example_min_zero", "option": null, @@ -354,7 +348,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "null": { "name": "null", @@ -377,7 +371,7 @@ "constant_value": "windows" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "null_resource.dev", @@ -551,7 +545,7 @@ ] } }, - "timestamp": "2024-10-28T20:08:18Z", + "timestamp": "2025-02-18T10:58:12Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/rich-parameters-validation/rich-parameters-validation.tfstate.json b/provisioner/terraform/testdata/rich-parameters-validation/rich-parameters-validation.tfstate.json index 35b981c3a9b54..1580f18bb97d8 100644 --- a/provisioner/terraform/testdata/rich-parameters-validation/rich-parameters-validation.tfstate.json +++ b/provisioner/terraform/testdata/rich-parameters-validation/rich-parameters-validation.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -17,7 +17,7 @@ "display_name": null, "ephemeral": true, "icon": null, - "id": "e09e9110-2f11-4a45-bc9f-dc7a12834ef0", + "id": "35958620-8fa6-479e-b2aa-19202d594b03", "mutable": true, "name": "number_example", "option": null, @@ -44,7 +44,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "7ba6324d-d8fd-43b8-91d2-d970a424db8b", + "id": "518c5dad-6069-4c24-8e0b-1ee75a52da3b", "mutable": false, "name": "number_example_max", "option": null, @@ -83,7 +83,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "64e12007-8479-43bf-956b-86fe7ae73066", + "id": "050653a6-301b-4916-a871-32d007e1294d", "mutable": false, "name": "number_example_max_zero", "option": null, @@ -122,7 +122,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "32681b2b-682f-4a5f-9aa6-c05be9d41a89", + "id": "4704cc0b-6c9d-422d-ba21-c488d780619e", "mutable": false, "name": "number_example_min", "option": null, @@ -161,7 +161,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "03b67b89-0d35-449d-8997-f5ce4b7c1518", + "id": "a8575ac7-8cf3-4deb-a716-ab5a31467e0b", "mutable": false, "name": "number_example_min_max", "option": null, @@ -200,7 +200,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "2201fc53-38c6-4a68-b3b9-4f6ef3390962", + "id": "1efc1290-5939-401c-8287-7b8d6724cdb6", "mutable": false, "name": "number_example_min_zero", "option": null, @@ -232,7 +232,7 @@ "type": "coder_agent", "name": "dev", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "arm64", "auth": "token", @@ -248,19 +248,16 @@ } ], "env": null, - "id": "060ffd05-39a9-4fa3-81a3-7d9d8e655bf8", + "id": "356b8996-c71d-479a-b161-ac3828a1831e", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "windows", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "58ed35b2-6124-4183-a493-40cb0174f4d2", + "startup_script_behavior": "non-blocking", + "token": "27611e1a-9de5-433b-81e4-cbd9f92dfe06", "troubleshooting_url": null }, "sensitive_values": { @@ -279,7 +276,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "4610812354433374355", + "id": "7456139785400247293", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/rich-parameters/external-module/child-external-module/main.tf b/provisioner/terraform/testdata/rich-parameters/external-module/child-external-module/main.tf index ac6f4c621a9d0..e8afbbf917fb5 100644 --- a/provisioner/terraform/testdata/rich-parameters/external-module/child-external-module/main.tf +++ b/provisioner/terraform/testdata/rich-parameters/external-module/child-external-module/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } docker = { source = "kreuzwerker/docker" diff --git a/provisioner/terraform/testdata/rich-parameters/external-module/main.tf b/provisioner/terraform/testdata/rich-parameters/external-module/main.tf index 55e942ec24e1f..0cf81d0162d07 100644 --- a/provisioner/terraform/testdata/rich-parameters/external-module/main.tf +++ b/provisioner/terraform/testdata/rich-parameters/external-module/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } docker = { source = "kreuzwerker/docker" diff --git a/provisioner/terraform/testdata/rich-parameters/rich-parameters.tf b/provisioner/terraform/testdata/rich-parameters/rich-parameters.tf index fc85769c8e9cc..24582eac30a5d 100644 --- a/provisioner/terraform/testdata/rich-parameters/rich-parameters.tf +++ b/provisioner/terraform/testdata/rich-parameters/rich-parameters.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/rich-parameters/rich-parameters.tfplan.json b/provisioner/terraform/testdata/rich-parameters/rich-parameters.tfplan.json index 1ec2927a40ad1..e6b5b1cab49dd 100644 --- a/provisioner/terraform/testdata/rich-parameters/rich-parameters.tfplan.json +++ b/provisioner/terraform/testdata/rich-parameters/rich-parameters.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,23 +10,20 @@ "type": "coder_agent", "name": "dev", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "arm64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "windows", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -68,16 +65,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "windows", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -119,7 +113,7 @@ ], "prior_state": { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -136,7 +130,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "cbec5bff-b81a-4815-99c0-40c0629779fb", + "id": "14d20380-9100-4218-afca-15d066dec134", "mutable": false, "name": "Example", "option": [ @@ -180,7 +174,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "dd1c36b7-a961-4eb2-9687-c32b5ee54fbc", + "id": "fec66abe-d831-4095-8520-8a654ccf309a", "mutable": false, "name": "number_example", "option": null, @@ -207,7 +201,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "f1bcac54-a58c-44b2-94f5-243a0b1492d3", + "id": "9e6cbf84-b49c-4c24-ad71-91195269ec84", "mutable": false, "name": "number_example_max_zero", "option": null, @@ -246,7 +240,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "79c76ac1-8e71-4872-9107-d7a9529f7dce", + "id": "5fbb470c-3814-4706-8fa6-c8c7e0f04c19", "mutable": false, "name": "number_example_min_max", "option": null, @@ -285,7 +279,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "da7a8aff-ffe3-402f-bf7e-b369ae04b041", + "id": "3790d994-f401-4e98-ad73-70b6f4e577d2", "mutable": false, "name": "number_example_min_zero", "option": null, @@ -324,7 +318,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "5fe2dad0-e11f-46f0-80ae-c0c3a29cd1fd", + "id": "26b3faa6-2eda-45f0-abbe-f4aba303f7cc", "mutable": false, "name": "Sample", "option": null, @@ -355,7 +349,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "920f98a1-3a6f-4602-8c87-ebbbef0310c5", + "id": "6027c1aa-dae9-48d9-90f2-b66151bf3129", "mutable": true, "name": "First parameter from module", "option": null, @@ -382,7 +376,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "f438d9ad-6c3e-44f3-95cd-1d423a9b09e5", + "id": "62262115-184d-4e14-a756-bedb553405a9", "mutable": true, "name": "Second parameter from module", "option": null, @@ -414,7 +408,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "b2c53701-be53-4591-aacf-1c83f75bcf15", + "id": "9ced5a2a-0e83-44fe-8088-6db4df59c15e", "mutable": true, "name": "First parameter from child module", "option": null, @@ -441,7 +435,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "038b18d4-d430-4703-886a-b7e10e01f856", + "id": "f9564821-9614-4931-b760-2b942d59214a", "mutable": true, "name": "Second parameter from child module", "option": null, @@ -469,7 +463,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "module.this_is_external_module:docker": { "name": "docker", @@ -498,7 +492,7 @@ "constant_value": "windows" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "null_resource.dev", @@ -794,7 +788,7 @@ } } }, - "timestamp": "2024-10-28T20:08:15Z", + "timestamp": "2025-02-18T10:58:12Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/rich-parameters/rich-parameters.tfstate.json b/provisioner/terraform/testdata/rich-parameters/rich-parameters.tfstate.json index 1bfc1835dfcaf..e83a026c81717 100644 --- a/provisioner/terraform/testdata/rich-parameters/rich-parameters.tfstate.json +++ b/provisioner/terraform/testdata/rich-parameters/rich-parameters.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -17,7 +17,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "8586d419-7e61-4e67-b8df-d98d8ac7ffd3", + "id": "bfd26633-f683-494b-8f71-1697c81488c3", "mutable": false, "name": "Example", "option": [ @@ -61,7 +61,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "0cc54450-13a6-486c-b542-6e23a9f3596b", + "id": "53a78857-abc2-4447-8329-cc12e160aaba", "mutable": false, "name": "number_example", "option": null, @@ -88,7 +88,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "0c0b913a-0bde-4b9e-8a70-06d9b6d38a26", + "id": "2ac0c3b2-f97f-47ad-beda-54264ba69422", "mutable": false, "name": "number_example_max_zero", "option": null, @@ -127,7 +127,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "37fd5372-2741-49dd-bf01-6ba29a24c9dd", + "id": "3b06ad67-0ab3-434c-b934-81e409e21565", "mutable": false, "name": "number_example_min_max", "option": null, @@ -166,7 +166,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "c0fd84ff-117f-442a-95f7-e8368ba7ce1d", + "id": "6f7c9117-36e4-47d5-8f23-a4e495a62895", "mutable": false, "name": "number_example_min_zero", "option": null, @@ -205,7 +205,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "ab067ffc-99de-4705-97fe-16c713d2d115", + "id": "5311db13-4521-4566-aac1-c70db8976ba5", "mutable": false, "name": "Sample", "option": null, @@ -225,7 +225,7 @@ "type": "coder_agent", "name": "dev", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "arm64", "auth": "token", @@ -241,19 +241,16 @@ } ], "env": null, - "id": "7daab302-d00e-48d4-878c-47afbe3a13bc", + "id": "2d891d31-82ac-4fdd-b922-25c1dfac956c", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "windows", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "e98c452d-cbe9-4ae1-8382-a986089dccb4", + "startup_script_behavior": "non-blocking", + "token": "6942a4c6-24f6-42b5-bcc7-d3e26d00d950", "troubleshooting_url": null }, "sensitive_values": { @@ -272,7 +269,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "2355126481625628137", + "id": "6111468857109842799", "triggers": null }, "sensitive_values": {}, @@ -297,7 +294,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "0978cc7c-f787-406c-a050-9272bbb52085", + "id": "1adeea93-ddc4-4dd8-b328-e167161bbe84", "mutable": true, "name": "First parameter from module", "option": null, @@ -324,7 +321,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "cd01d7da-9f56-460d-b163-e88a0a9a5f67", + "id": "4bb326d9-cf43-4947-b26c-bb668a9f7a80", "mutable": true, "name": "Second parameter from module", "option": null, @@ -356,7 +353,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "528e845a-843b-48b3-a421-a22340726d5a", + "id": "a2b6d1e4-2e77-4eff-a81b-0fe285750824", "mutable": true, "name": "First parameter from child module", "option": null, @@ -383,7 +380,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "f486efbb-2fc6-4091-9eca-0088ac6cd3cc", + "id": "9dac8aaa-ccf6-4c94-90d2-2009bfbbd596", "mutable": true, "name": "Second parameter from child module", "option": null, diff --git a/provisioner/terraform/testdata/version.txt b/provisioner/terraform/testdata/version.txt index 66beabb5795e7..db77e0ee9760a 100644 --- a/provisioner/terraform/testdata/version.txt +++ b/provisioner/terraform/testdata/version.txt @@ -1 +1 @@ -1.9.8 +1.10.5 diff --git a/provisioner/terraform/timings_test.go b/provisioner/terraform/timings_test.go index e128b4d654d56..ec91caf301831 100644 --- a/provisioner/terraform/timings_test.go +++ b/provisioner/terraform/timings_test.go @@ -28,7 +28,7 @@ func TestTimingsFromProvision(t *testing.T) { // Given: a fake terraform bin that behaves as we expect it to. fakeBin := filepath.Join(cwd, "testdata", "timings-aggregation/fake-terraform.sh") - t.Logf(fakeBin) + t.Log(fakeBin) ctx, api := setupProvisioner(t, &provisionerServeOptions{ binaryPath: fakeBin, diff --git a/provisionerd/proto/provisionerd.pb.go b/provisionerd/proto/provisionerd.pb.go index 8cf14a85787ac..24b1c4b8453ce 100644 --- a/provisionerd/proto/provisionerd.pb.go +++ b/provisionerd/proto/provisionerd.pb.go @@ -1290,6 +1290,7 @@ type CompletedJob_TemplateImport struct { ExternalAuthProviders []*proto.ExternalAuthProviderResource `protobuf:"bytes,5,rep,name=external_auth_providers,json=externalAuthProviders,proto3" json:"external_auth_providers,omitempty"` StartModules []*proto.Module `protobuf:"bytes,6,rep,name=start_modules,json=startModules,proto3" json:"start_modules,omitempty"` StopModules []*proto.Module `protobuf:"bytes,7,rep,name=stop_modules,json=stopModules,proto3" json:"stop_modules,omitempty"` + Presets []*proto.Preset `protobuf:"bytes,8,rep,name=presets,proto3" json:"presets,omitempty"` } func (x *CompletedJob_TemplateImport) Reset() { @@ -1373,6 +1374,13 @@ func (x *CompletedJob_TemplateImport) GetStopModules() []*proto.Module { return nil } +func (x *CompletedJob_TemplateImport) GetPresets() []*proto.Preset { + if x != nil { + return x.Presets + } + return nil +} + type CompletedJob_TemplateDryRun struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1556,7 +1564,7 @@ var file_provisionerd_proto_provisionerd_proto_rawDesc = []byte{ 0x2e, 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x52, 0x07, 0x74, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x73, 0x1a, 0x10, 0x0a, 0x0e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x1a, 0x10, 0x0a, 0x0e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x44, 0x72, - 0x79, 0x52, 0x75, 0x6e, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xd0, 0x08, 0x0a, + 0x79, 0x52, 0x75, 0x6e, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xff, 0x08, 0x0a, 0x0c, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x12, 0x15, 0x0a, 0x06, 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x54, 0x0a, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, @@ -1587,7 +1595,7 @@ var file_provisionerd_proto_provisionerd_proto_rawDesc = []byte{ 0x69, 0x6e, 0x67, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x52, 0x07, 0x6d, 0x6f, 0x64, 0x75, - 0x6c, 0x65, 0x73, 0x1a, 0xeb, 0x03, 0x0a, 0x0e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, + 0x6c, 0x65, 0x73, 0x1a, 0x9a, 0x04, 0x0a, 0x0e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x3e, 0x0a, 0x0f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, @@ -1618,108 +1626,111 @@ var file_provisionerd_proto_provisionerd_proto_rawDesc = []byte{ 0x70, 0x5f, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x52, 0x0b, 0x73, 0x74, 0x6f, 0x70, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, - 0x73, 0x1a, 0x74, 0x0a, 0x0e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x44, 0x72, 0x79, - 0x52, 0x75, 0x6e, 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, - 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x6d, 0x6f, 0x64, 0x75, - 0x6c, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x52, 0x07, - 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x73, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, - 0xb0, 0x01, 0x0a, 0x03, 0x4c, 0x6f, 0x67, 0x12, 0x2f, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, - 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x4c, 0x6f, 0x67, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, - 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, - 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, - 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, - 0x5f, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x64, 0x41, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, - 0x74, 0x70, 0x75, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, - 0x75, 0x74, 0x22, 0xa6, 0x03, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x6a, 0x6f, 0x62, 0x5f, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x25, - 0x0a, 0x04, 0x6c, 0x6f, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, - 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x4c, 0x6f, 0x67, 0x52, - 0x04, 0x6c, 0x6f, 0x67, 0x73, 0x12, 0x4c, 0x0a, 0x12, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, - 0x65, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, - 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, - 0x52, 0x11, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, - 0x6c, 0x65, 0x73, 0x12, 0x4c, 0x0a, 0x14, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x72, 0x69, - 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, - 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x12, 0x75, - 0x73, 0x65, 0x72, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x73, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x64, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x06, 0x72, 0x65, 0x61, 0x64, 0x6d, 0x65, 0x12, 0x58, 0x0a, 0x0e, 0x77, 0x6f, 0x72, - 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x31, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, + 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x70, 0x72, 0x65, 0x73, 0x65, 0x74, 0x73, 0x18, 0x08, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, + 0x2e, 0x50, 0x72, 0x65, 0x73, 0x65, 0x74, 0x52, 0x07, 0x70, 0x72, 0x65, 0x73, 0x65, 0x74, 0x73, + 0x1a, 0x74, 0x0a, 0x0e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x44, 0x72, 0x79, 0x52, + 0x75, 0x6e, 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x6d, 0x6f, 0x64, 0x75, 0x6c, + 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x52, 0x07, 0x6d, + 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x73, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xb0, + 0x01, 0x0a, 0x03, 0x4c, 0x6f, 0x67, 0x12, 0x2f, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x4c, 0x6f, 0x67, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, + 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, + 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, + 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x64, 0x41, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74, + 0x70, 0x75, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, + 0x74, 0x22, 0xa6, 0x03, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x25, 0x0a, + 0x04, 0x6c, 0x6f, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x4c, 0x6f, 0x67, 0x52, 0x04, + 0x6c, 0x6f, 0x67, 0x73, 0x12, 0x4c, 0x0a, 0x12, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, + 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, + 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x52, + 0x11, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, + 0x65, 0x73, 0x12, 0x4c, 0x0a, 0x14, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, + 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x56, + 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x12, 0x75, 0x73, + 0x65, 0x72, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, + 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x64, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x06, 0x72, 0x65, 0x61, 0x64, 0x6d, 0x65, 0x12, 0x58, 0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x31, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x52, 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x61, + 0x67, 0x73, 0x1a, 0x40, 0x0a, 0x12, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, + 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x3a, 0x02, 0x38, 0x01, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x22, 0x7a, 0x0a, 0x11, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x1a, 0x0a, 0x08, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x08, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x65, 0x64, 0x12, 0x43, 0x0a, 0x0f, 0x76, + 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x65, 0x72, 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x52, 0x0e, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, + 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x22, 0x4a, 0x0a, 0x12, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, + 0x51, 0x75, 0x6f, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x15, 0x0a, 0x06, + 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6a, 0x6f, + 0x62, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x61, 0x69, 0x6c, 0x79, 0x5f, 0x63, 0x6f, 0x73, + 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x64, 0x61, 0x69, 0x6c, 0x79, 0x43, 0x6f, + 0x73, 0x74, 0x22, 0x68, 0x0a, 0x13, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, 0x6f, 0x74, + 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x72, 0x65, + 0x64, 0x69, 0x74, 0x73, 0x5f, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6d, 0x65, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x0f, 0x63, 0x72, 0x65, 0x64, 0x69, 0x74, 0x73, 0x43, 0x6f, 0x6e, 0x73, + 0x75, 0x6d, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x62, 0x75, 0x64, 0x67, 0x65, 0x74, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x62, 0x75, 0x64, 0x67, 0x65, 0x74, 0x22, 0x0f, 0x0a, 0x0d, + 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x41, 0x63, 0x71, 0x75, 0x69, 0x72, 0x65, 0x2a, 0x34, 0x0a, + 0x09, 0x4c, 0x6f, 0x67, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x12, 0x50, 0x52, + 0x4f, 0x56, 0x49, 0x53, 0x49, 0x4f, 0x4e, 0x45, 0x52, 0x5f, 0x44, 0x41, 0x45, 0x4d, 0x4f, 0x4e, + 0x10, 0x00, 0x12, 0x0f, 0x0a, 0x0b, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x53, 0x49, 0x4f, 0x4e, 0x45, + 0x52, 0x10, 0x01, 0x32, 0xc5, 0x03, 0x0a, 0x11, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x12, 0x41, 0x0a, 0x0a, 0x41, 0x63, 0x71, + 0x75, 0x69, 0x72, 0x65, 0x4a, 0x6f, 0x62, 0x12, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x19, 0x2e, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x41, 0x63, 0x71, 0x75, + 0x69, 0x72, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x22, 0x03, 0x88, 0x02, 0x01, 0x12, 0x52, 0x0a, 0x14, + 0x41, 0x63, 0x71, 0x75, 0x69, 0x72, 0x65, 0x4a, 0x6f, 0x62, 0x57, 0x69, 0x74, 0x68, 0x43, 0x61, + 0x6e, 0x63, 0x65, 0x6c, 0x12, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x65, 0x72, 0x64, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x41, 0x63, 0x71, 0x75, 0x69, 0x72, + 0x65, 0x1a, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, + 0x2e, 0x41, 0x63, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x28, 0x01, 0x30, 0x01, + 0x12, 0x52, 0x0a, 0x0b, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x12, + 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x43, + 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, + 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x09, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, + 0x62, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x61, 0x67, 0x73, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, - 0x61, 0x67, 0x73, 0x1a, 0x40, 0x0a, 0x12, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x3a, 0x02, 0x38, 0x01, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x22, 0x7a, 0x0a, 0x11, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x08, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x65, 0x64, 0x12, 0x43, 0x0a, 0x0f, - 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, - 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x65, 0x72, 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x52, 0x0e, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x73, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x22, 0x4a, 0x0a, 0x12, 0x43, 0x6f, 0x6d, 0x6d, 0x69, - 0x74, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x15, 0x0a, - 0x06, 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6a, - 0x6f, 0x62, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x61, 0x69, 0x6c, 0x79, 0x5f, 0x63, 0x6f, - 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x64, 0x61, 0x69, 0x6c, 0x79, 0x43, - 0x6f, 0x73, 0x74, 0x22, 0x68, 0x0a, 0x13, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, 0x6f, - 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x72, - 0x65, 0x64, 0x69, 0x74, 0x73, 0x5f, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6d, 0x65, 0x64, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x0f, 0x63, 0x72, 0x65, 0x64, 0x69, 0x74, 0x73, 0x43, 0x6f, 0x6e, - 0x73, 0x75, 0x6d, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x62, 0x75, 0x64, 0x67, 0x65, 0x74, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x62, 0x75, 0x64, 0x67, 0x65, 0x74, 0x22, 0x0f, 0x0a, - 0x0d, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x41, 0x63, 0x71, 0x75, 0x69, 0x72, 0x65, 0x2a, 0x34, - 0x0a, 0x09, 0x4c, 0x6f, 0x67, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x12, 0x50, - 0x52, 0x4f, 0x56, 0x49, 0x53, 0x49, 0x4f, 0x4e, 0x45, 0x52, 0x5f, 0x44, 0x41, 0x45, 0x4d, 0x4f, - 0x4e, 0x10, 0x00, 0x12, 0x0f, 0x0a, 0x0b, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x53, 0x49, 0x4f, 0x4e, - 0x45, 0x52, 0x10, 0x01, 0x32, 0xc5, 0x03, 0x0a, 0x11, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, - 0x6f, 0x6e, 0x65, 0x72, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x12, 0x41, 0x0a, 0x0a, 0x41, 0x63, - 0x71, 0x75, 0x69, 0x72, 0x65, 0x4a, 0x6f, 0x62, 0x12, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, - 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x19, 0x2e, - 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x41, 0x63, 0x71, - 0x75, 0x69, 0x72, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x22, 0x03, 0x88, 0x02, 0x01, 0x12, 0x52, 0x0a, - 0x14, 0x41, 0x63, 0x71, 0x75, 0x69, 0x72, 0x65, 0x4a, 0x6f, 0x62, 0x57, 0x69, 0x74, 0x68, 0x43, - 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x12, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x41, 0x63, 0x71, 0x75, 0x69, - 0x72, 0x65, 0x1a, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x64, 0x2e, 0x41, 0x63, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x28, 0x01, 0x30, - 0x01, 0x12, 0x52, 0x0a, 0x0b, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x61, - 0x12, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, - 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x64, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x09, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, - 0x6f, 0x62, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x64, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x64, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x07, 0x46, 0x61, 0x69, 0x6c, 0x4a, 0x6f, 0x62, 0x12, 0x17, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x46, 0x61, - 0x69, 0x6c, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, - 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3e, 0x0a, 0x0b, - 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x12, 0x1a, 0x2e, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, - 0x65, 0x74, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, - 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x2e, 0x5a, 0x2c, - 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, - 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, - 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, + 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x37, 0x0a, 0x07, 0x46, 0x61, 0x69, 0x6c, 0x4a, 0x6f, 0x62, 0x12, 0x17, 0x2e, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x46, 0x61, 0x69, + 0x6c, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3e, 0x0a, 0x0b, 0x43, + 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, + 0x74, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x2e, 0x5a, 0x2c, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( @@ -1770,6 +1781,7 @@ var file_provisionerd_proto_provisionerd_proto_goTypes = []interface{}{ (*proto.Module)(nil), // 30: provisioner.Module (*proto.RichParameter)(nil), // 31: provisioner.RichParameter (*proto.ExternalAuthProviderResource)(nil), // 32: provisioner.ExternalAuthProviderResource + (*proto.Preset)(nil), // 33: provisioner.Preset } var file_provisionerd_proto_provisionerd_proto_depIdxs = []int32{ 11, // 0: provisionerd.AcquiredJob.workspace_build:type_name -> provisionerd.AcquiredJob.WorkspaceBuild @@ -1808,25 +1820,26 @@ var file_provisionerd_proto_provisionerd_proto_depIdxs = []int32{ 32, // 33: provisionerd.CompletedJob.TemplateImport.external_auth_providers:type_name -> provisioner.ExternalAuthProviderResource 30, // 34: provisionerd.CompletedJob.TemplateImport.start_modules:type_name -> provisioner.Module 30, // 35: provisionerd.CompletedJob.TemplateImport.stop_modules:type_name -> provisioner.Module - 29, // 36: provisionerd.CompletedJob.TemplateDryRun.resources:type_name -> provisioner.Resource - 30, // 37: provisionerd.CompletedJob.TemplateDryRun.modules:type_name -> provisioner.Module - 1, // 38: provisionerd.ProvisionerDaemon.AcquireJob:input_type -> provisionerd.Empty - 10, // 39: provisionerd.ProvisionerDaemon.AcquireJobWithCancel:input_type -> provisionerd.CancelAcquire - 8, // 40: provisionerd.ProvisionerDaemon.CommitQuota:input_type -> provisionerd.CommitQuotaRequest - 6, // 41: provisionerd.ProvisionerDaemon.UpdateJob:input_type -> provisionerd.UpdateJobRequest - 3, // 42: provisionerd.ProvisionerDaemon.FailJob:input_type -> provisionerd.FailedJob - 4, // 43: provisionerd.ProvisionerDaemon.CompleteJob:input_type -> provisionerd.CompletedJob - 2, // 44: provisionerd.ProvisionerDaemon.AcquireJob:output_type -> provisionerd.AcquiredJob - 2, // 45: provisionerd.ProvisionerDaemon.AcquireJobWithCancel:output_type -> provisionerd.AcquiredJob - 9, // 46: provisionerd.ProvisionerDaemon.CommitQuota:output_type -> provisionerd.CommitQuotaResponse - 7, // 47: provisionerd.ProvisionerDaemon.UpdateJob:output_type -> provisionerd.UpdateJobResponse - 1, // 48: provisionerd.ProvisionerDaemon.FailJob:output_type -> provisionerd.Empty - 1, // 49: provisionerd.ProvisionerDaemon.CompleteJob:output_type -> provisionerd.Empty - 44, // [44:50] is the sub-list for method output_type - 38, // [38:44] is the sub-list for method input_type - 38, // [38:38] is the sub-list for extension type_name - 38, // [38:38] is the sub-list for extension extendee - 0, // [0:38] is the sub-list for field type_name + 33, // 36: provisionerd.CompletedJob.TemplateImport.presets:type_name -> provisioner.Preset + 29, // 37: provisionerd.CompletedJob.TemplateDryRun.resources:type_name -> provisioner.Resource + 30, // 38: provisionerd.CompletedJob.TemplateDryRun.modules:type_name -> provisioner.Module + 1, // 39: provisionerd.ProvisionerDaemon.AcquireJob:input_type -> provisionerd.Empty + 10, // 40: provisionerd.ProvisionerDaemon.AcquireJobWithCancel:input_type -> provisionerd.CancelAcquire + 8, // 41: provisionerd.ProvisionerDaemon.CommitQuota:input_type -> provisionerd.CommitQuotaRequest + 6, // 42: provisionerd.ProvisionerDaemon.UpdateJob:input_type -> provisionerd.UpdateJobRequest + 3, // 43: provisionerd.ProvisionerDaemon.FailJob:input_type -> provisionerd.FailedJob + 4, // 44: provisionerd.ProvisionerDaemon.CompleteJob:input_type -> provisionerd.CompletedJob + 2, // 45: provisionerd.ProvisionerDaemon.AcquireJob:output_type -> provisionerd.AcquiredJob + 2, // 46: provisionerd.ProvisionerDaemon.AcquireJobWithCancel:output_type -> provisionerd.AcquiredJob + 9, // 47: provisionerd.ProvisionerDaemon.CommitQuota:output_type -> provisionerd.CommitQuotaResponse + 7, // 48: provisionerd.ProvisionerDaemon.UpdateJob:output_type -> provisionerd.UpdateJobResponse + 1, // 49: provisionerd.ProvisionerDaemon.FailJob:output_type -> provisionerd.Empty + 1, // 50: provisionerd.ProvisionerDaemon.CompleteJob:output_type -> provisionerd.Empty + 45, // [45:51] is the sub-list for method output_type + 39, // [39:45] is the sub-list for method input_type + 39, // [39:39] is the sub-list for extension type_name + 39, // [39:39] is the sub-list for extension extendee + 0, // [0:39] is the sub-list for field type_name } func init() { file_provisionerd_proto_provisionerd_proto_init() } diff --git a/provisionerd/proto/provisionerd.proto b/provisionerd/proto/provisionerd.proto index ad1a43e49a33d..301cd06987868 100644 --- a/provisionerd/proto/provisionerd.proto +++ b/provisionerd/proto/provisionerd.proto @@ -84,6 +84,7 @@ message CompletedJob { repeated provisioner.ExternalAuthProviderResource external_auth_providers = 5; repeated provisioner.Module start_modules = 6; repeated provisioner.Module stop_modules = 7; + repeated provisioner.Preset presets = 8; } message TemplateDryRun { repeated provisioner.Resource resources = 1; diff --git a/provisionerd/proto/version.go b/provisionerd/proto/version.go index 8d46a1dd87587..3b4ffb6e4bc8b 100644 --- a/provisionerd/proto/version.go +++ b/provisionerd/proto/version.go @@ -6,9 +6,12 @@ import "github.com/coder/coder/v2/apiversion" // // API v1.2: // - Add support for `open_in` parameters in the workspace apps. +// +// API v1.3: +// - Add new field named `resources_monitoring` in the Agent with resources monitoring.. const ( CurrentMajor = 1 - CurrentMinor = 2 + CurrentMinor = 3 ) // CurrentVersion is the current provisionerd API version. diff --git a/provisionerd/provisionerd.go b/provisionerd/provisionerd.go index e3b8da8bfe2d9..b461bc593ee36 100644 --- a/provisionerd/provisionerd.go +++ b/provisionerd/provisionerd.go @@ -56,6 +56,7 @@ type Options struct { TracerProvider trace.TracerProvider Metrics *Metrics + ExternalProvisioner bool ForceCancelInterval time.Duration UpdateInterval time.Duration LogBufferInterval time.Duration @@ -97,12 +98,13 @@ func New(clientDialer Dialer, opts *Options) *Server { clientDialer: clientDialer, clientCh: make(chan proto.DRPCProvisionerDaemonClient), - closeContext: ctx, - closeCancel: ctxCancel, - closedCh: make(chan struct{}), - shuttingDownCh: make(chan struct{}), - acquireDoneCh: make(chan struct{}), - initConnectionCh: opts.InitConnectionCh, + closeContext: ctx, + closeCancel: ctxCancel, + closedCh: make(chan struct{}), + shuttingDownCh: make(chan struct{}), + acquireDoneCh: make(chan struct{}), + initConnectionCh: opts.InitConnectionCh, + externalProvisioner: opts.ExternalProvisioner, } daemon.wg.Add(2) @@ -141,8 +143,9 @@ type Server struct { // shuttingDownCh will receive when we start graceful shutdown shuttingDownCh chan struct{} // acquireDoneCh will receive when the acquireLoop exits - acquireDoneCh chan struct{} - activeJob *runner.Runner + acquireDoneCh chan struct{} + activeJob *runner.Runner + externalProvisioner bool } type Metrics struct { @@ -212,6 +215,10 @@ func NewMetrics(reg prometheus.Registerer) Metrics { func (p *Server) connect() { defer p.opts.Logger.Debug(p.closeContext, "connect loop exited") defer p.wg.Done() + logConnect := p.opts.Logger.Debug + if p.externalProvisioner { + logConnect = p.opts.Logger.Info + } // An exponential back-off occurs when the connection is failing to dial. // This is to prevent server spam in case of a coderd outage. connectLoop: @@ -239,7 +246,12 @@ connectLoop: p.opts.Logger.Warn(p.closeContext, "coderd client failed to dial", slog.Error(err)) continue } - p.opts.Logger.Info(p.closeContext, "successfully connected to coderd") + // This log is useful to verify that an external provisioner daemon is + // successfully connecting to coderd. It doesn't add much value if the + // daemon is built-in, so we only log it on the info level if p.externalProvisioner + // is true. This log message is mentioned in the docs: + // https://github.com/coder/coder/blob/5bd86cb1c06561d1d3e90ce689da220467e525c0/docs/admin/provisioners.md#L346 + logConnect(p.closeContext, "successfully connected to coderd") retrier.Reset() p.initConnectionOnce.Do(func() { close(p.initConnectionCh) @@ -252,7 +264,7 @@ connectLoop: client.DRPCConn().Close() return case <-client.DRPCConn().Closed(): - p.opts.Logger.Info(p.closeContext, "connection to coderd closed") + logConnect(p.closeContext, "connection to coderd closed") continue connectLoop case p.clientCh <- client: continue diff --git a/provisionerd/runner/runner.go b/provisionerd/runner/runner.go index c4f1799dd0db5..99aeb6cb3097e 100644 --- a/provisionerd/runner/runner.go +++ b/provisionerd/runner/runner.go @@ -590,6 +590,7 @@ func (r *Runner) runTemplateImport(ctx context.Context) (*proto.CompletedJob, *p ExternalAuthProviders: startProvision.ExternalAuthProviders, StartModules: startProvision.Modules, StopModules: stopProvision.Modules, + Presets: startProvision.Presets, }, }, }, nil @@ -650,6 +651,7 @@ type templateImportProvision struct { Parameters []*sdkproto.RichParameter ExternalAuthProviders []*sdkproto.ExternalAuthProviderResource Modules []*sdkproto.Module + Presets []*sdkproto.Preset } // Performs a dry-run provision when importing a template. @@ -742,6 +744,7 @@ func (r *Runner) runTemplateImportProvisionWithRichParameters( Parameters: c.Parameters, ExternalAuthProviders: c.ExternalAuthProviders, Modules: c.Modules, + Presets: c.Presets, }, nil default: return nil, xerrors.Errorf("invalid message type %q received from provisioner", diff --git a/provisionersdk/archive.go b/provisionersdk/archive.go index 410315c18a238..a069639a1eba6 100644 --- a/provisionersdk/archive.go +++ b/provisionersdk/archive.go @@ -175,7 +175,7 @@ func Untar(directory string, r io.Reader) error { if err != nil { return err } - file, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) + file, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.FileMode(header.Mode)) if err != nil { return err } diff --git a/provisionersdk/archive_test.go b/provisionersdk/archive_test.go index 7f31fb7730485..12362275a72b9 100644 --- a/provisionersdk/archive_test.go +++ b/provisionersdk/archive_test.go @@ -184,18 +184,70 @@ func TestTar(t *testing.T) { func TestUntar(t *testing.T) { t.Parallel() - log := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}) - dir := t.TempDir() - file, err := os.CreateTemp(dir, "*.tf") - require.NoError(t, err) - _ = file.Close() - archive := new(bytes.Buffer) - err = provisionersdk.Tar(archive, log, dir, 1024) - require.NoError(t, err) - dir = t.TempDir() - err = provisionersdk.Untar(dir, archive) - require.NoError(t, err) - _, err = os.Stat(filepath.Join(dir, filepath.Base(file.Name()))) - require.NoError(t, err) + t.Run("Basic", func(t *testing.T) { + t.Parallel() + + log := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}) + + dir := t.TempDir() + file, err := os.CreateTemp(dir, "*.tf") + require.NoError(t, err) + _ = file.Close() + + archive := new(bytes.Buffer) + err = provisionersdk.Tar(archive, log, dir, 1024) + require.NoError(t, err) + + dir = t.TempDir() + err = provisionersdk.Untar(dir, archive) + require.NoError(t, err) + + _, err = os.Stat(filepath.Join(dir, filepath.Base(file.Name()))) + require.NoError(t, err) + }) + + t.Run("Overwrite", func(t *testing.T) { + t.Parallel() + + log := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}) + + dir1 := t.TempDir() + dir2 := t.TempDir() + + // 1. Create directory with .tf file. + file, err := os.CreateTemp(dir1, "*.tf") + require.NoError(t, err) + _ = file.Close() + + err = os.WriteFile(file.Name(), []byte("# ab"), 0o600) + require.NoError(t, err) + + archive := new(bytes.Buffer) + + // 2. Build tar archive. + err = provisionersdk.Tar(archive, log, dir1, 4096) + require.NoError(t, err) + + // 3. Untar to the second location. + err = provisionersdk.Untar(dir2, archive) + require.NoError(t, err) + + // 4. Modify the .tf file + err = os.WriteFile(file.Name(), []byte("# c"), 0o600) + require.NoError(t, err) + + // 5. Build tar archive with modified .tf file + err = provisionersdk.Tar(archive, log, dir1, 4096) + require.NoError(t, err) + + // 6. Untar to a second location. + err = provisionersdk.Untar(dir2, archive) + require.NoError(t, err) + + // Verify if the file has been fully overwritten + content, err := os.ReadFile(filepath.Join(dir2, filepath.Base(file.Name()))) + require.NoError(t, err) + require.Equal(t, "# c", string(content)) + }) } diff --git a/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go index 2acbc5d87b743..df74e01a4050b 100644 --- a/provisionersdk/proto/provisioner.pb.go +++ b/provisionersdk/proto/provisioner.pb.go @@ -699,6 +699,117 @@ func (x *RichParameterValue) GetValue() string { return "" } +// Preset represents a set of preset parameters for a template version. +type Preset struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Parameters []*PresetParameter `protobuf:"bytes,2,rep,name=parameters,proto3" json:"parameters,omitempty"` +} + +func (x *Preset) Reset() { + *x = Preset{} + if protoimpl.UnsafeEnabled { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Preset) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Preset) ProtoMessage() {} + +func (x *Preset) ProtoReflect() protoreflect.Message { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Preset.ProtoReflect.Descriptor instead. +func (*Preset) Descriptor() ([]byte, []int) { + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{5} +} + +func (x *Preset) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Preset) GetParameters() []*PresetParameter { + if x != nil { + return x.Parameters + } + return nil +} + +type PresetParameter struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` +} + +func (x *PresetParameter) Reset() { + *x = PresetParameter{} + if protoimpl.UnsafeEnabled { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PresetParameter) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PresetParameter) ProtoMessage() {} + +func (x *PresetParameter) ProtoReflect() protoreflect.Message { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PresetParameter.ProtoReflect.Descriptor instead. +func (*PresetParameter) Descriptor() ([]byte, []int) { + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{6} +} + +func (x *PresetParameter) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *PresetParameter) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + // VariableValue holds the key/value mapping of a Terraform variable. type VariableValue struct { state protoimpl.MessageState @@ -713,7 +824,7 @@ type VariableValue struct { func (x *VariableValue) Reset() { *x = VariableValue{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[5] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -726,7 +837,7 @@ func (x *VariableValue) String() string { func (*VariableValue) ProtoMessage() {} func (x *VariableValue) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[5] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -739,7 +850,7 @@ func (x *VariableValue) ProtoReflect() protoreflect.Message { // Deprecated: Use VariableValue.ProtoReflect.Descriptor instead. func (*VariableValue) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{5} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{7} } func (x *VariableValue) GetName() string { @@ -776,7 +887,7 @@ type Log struct { func (x *Log) Reset() { *x = Log{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[6] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -789,7 +900,7 @@ func (x *Log) String() string { func (*Log) ProtoMessage() {} func (x *Log) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[6] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -802,7 +913,7 @@ func (x *Log) ProtoReflect() protoreflect.Message { // Deprecated: Use Log.ProtoReflect.Descriptor instead. func (*Log) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{6} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{8} } func (x *Log) GetLevel() LogLevel { @@ -830,7 +941,7 @@ type InstanceIdentityAuth struct { func (x *InstanceIdentityAuth) Reset() { *x = InstanceIdentityAuth{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[7] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -843,7 +954,7 @@ func (x *InstanceIdentityAuth) String() string { func (*InstanceIdentityAuth) ProtoMessage() {} func (x *InstanceIdentityAuth) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[7] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -856,7 +967,7 @@ func (x *InstanceIdentityAuth) ProtoReflect() protoreflect.Message { // Deprecated: Use InstanceIdentityAuth.ProtoReflect.Descriptor instead. func (*InstanceIdentityAuth) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{7} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{9} } func (x *InstanceIdentityAuth) GetInstanceId() string { @@ -878,7 +989,7 @@ type ExternalAuthProviderResource struct { func (x *ExternalAuthProviderResource) Reset() { *x = ExternalAuthProviderResource{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[8] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -891,7 +1002,7 @@ func (x *ExternalAuthProviderResource) String() string { func (*ExternalAuthProviderResource) ProtoMessage() {} func (x *ExternalAuthProviderResource) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[8] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -904,7 +1015,7 @@ func (x *ExternalAuthProviderResource) ProtoReflect() protoreflect.Message { // Deprecated: Use ExternalAuthProviderResource.ProtoReflect.Descriptor instead. func (*ExternalAuthProviderResource) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{8} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{10} } func (x *ExternalAuthProviderResource) GetId() string { @@ -933,7 +1044,7 @@ type ExternalAuthProvider struct { func (x *ExternalAuthProvider) Reset() { *x = ExternalAuthProvider{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[9] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -946,7 +1057,7 @@ func (x *ExternalAuthProvider) String() string { func (*ExternalAuthProvider) ProtoMessage() {} func (x *ExternalAuthProvider) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[9] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -959,7 +1070,7 @@ func (x *ExternalAuthProvider) ProtoReflect() protoreflect.Message { // Deprecated: Use ExternalAuthProvider.ProtoReflect.Descriptor instead. func (*ExternalAuthProvider) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{9} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{11} } func (x *ExternalAuthProvider) GetId() string { @@ -1002,16 +1113,17 @@ type Agent struct { // Field 15, 16, 17 were related to scripts, which are now removed. Metadata []*Agent_Metadata `protobuf:"bytes,18,rep,name=metadata,proto3" json:"metadata,omitempty"` // Field 19 was startup_script_behavior, now removed. - DisplayApps *DisplayApps `protobuf:"bytes,20,opt,name=display_apps,json=displayApps,proto3" json:"display_apps,omitempty"` - Scripts []*Script `protobuf:"bytes,21,rep,name=scripts,proto3" json:"scripts,omitempty"` - ExtraEnvs []*Env `protobuf:"bytes,22,rep,name=extra_envs,json=extraEnvs,proto3" json:"extra_envs,omitempty"` - Order int64 `protobuf:"varint,23,opt,name=order,proto3" json:"order,omitempty"` + DisplayApps *DisplayApps `protobuf:"bytes,20,opt,name=display_apps,json=displayApps,proto3" json:"display_apps,omitempty"` + Scripts []*Script `protobuf:"bytes,21,rep,name=scripts,proto3" json:"scripts,omitempty"` + ExtraEnvs []*Env `protobuf:"bytes,22,rep,name=extra_envs,json=extraEnvs,proto3" json:"extra_envs,omitempty"` + Order int64 `protobuf:"varint,23,opt,name=order,proto3" json:"order,omitempty"` + ResourcesMonitoring *ResourcesMonitoring `protobuf:"bytes,24,opt,name=resources_monitoring,json=resourcesMonitoring,proto3" json:"resources_monitoring,omitempty"` } func (x *Agent) Reset() { *x = Agent{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[10] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1024,7 +1136,7 @@ func (x *Agent) String() string { func (*Agent) ProtoMessage() {} func (x *Agent) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[10] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[12] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1037,7 +1149,7 @@ func (x *Agent) ProtoReflect() protoreflect.Message { // Deprecated: Use Agent.ProtoReflect.Descriptor instead. func (*Agent) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{10} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{12} } func (x *Agent) GetId() string { @@ -1166,6 +1278,13 @@ func (x *Agent) GetOrder() int64 { return 0 } +func (x *Agent) GetResourcesMonitoring() *ResourcesMonitoring { + if x != nil { + return x.ResourcesMonitoring + } + return nil +} + type isAgent_Auth interface { isAgent_Auth() } @@ -1182,6 +1301,179 @@ func (*Agent_Token) isAgent_Auth() {} func (*Agent_InstanceId) isAgent_Auth() {} +type ResourcesMonitoring struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Memory *MemoryResourceMonitor `protobuf:"bytes,1,opt,name=memory,proto3" json:"memory,omitempty"` + Volumes []*VolumeResourceMonitor `protobuf:"bytes,2,rep,name=volumes,proto3" json:"volumes,omitempty"` +} + +func (x *ResourcesMonitoring) Reset() { + *x = ResourcesMonitoring{} + if protoimpl.UnsafeEnabled { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ResourcesMonitoring) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResourcesMonitoring) ProtoMessage() {} + +func (x *ResourcesMonitoring) ProtoReflect() protoreflect.Message { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[13] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResourcesMonitoring.ProtoReflect.Descriptor instead. +func (*ResourcesMonitoring) Descriptor() ([]byte, []int) { + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{13} +} + +func (x *ResourcesMonitoring) GetMemory() *MemoryResourceMonitor { + if x != nil { + return x.Memory + } + return nil +} + +func (x *ResourcesMonitoring) GetVolumes() []*VolumeResourceMonitor { + if x != nil { + return x.Volumes + } + return nil +} + +type MemoryResourceMonitor struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` + Threshold int32 `protobuf:"varint,2,opt,name=threshold,proto3" json:"threshold,omitempty"` +} + +func (x *MemoryResourceMonitor) Reset() { + *x = MemoryResourceMonitor{} + if protoimpl.UnsafeEnabled { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MemoryResourceMonitor) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MemoryResourceMonitor) ProtoMessage() {} + +func (x *MemoryResourceMonitor) ProtoReflect() protoreflect.Message { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[14] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MemoryResourceMonitor.ProtoReflect.Descriptor instead. +func (*MemoryResourceMonitor) Descriptor() ([]byte, []int) { + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{14} +} + +func (x *MemoryResourceMonitor) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +func (x *MemoryResourceMonitor) GetThreshold() int32 { + if x != nil { + return x.Threshold + } + return 0 +} + +type VolumeResourceMonitor struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + Enabled bool `protobuf:"varint,2,opt,name=enabled,proto3" json:"enabled,omitempty"` + Threshold int32 `protobuf:"varint,3,opt,name=threshold,proto3" json:"threshold,omitempty"` +} + +func (x *VolumeResourceMonitor) Reset() { + *x = VolumeResourceMonitor{} + if protoimpl.UnsafeEnabled { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *VolumeResourceMonitor) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VolumeResourceMonitor) ProtoMessage() {} + +func (x *VolumeResourceMonitor) ProtoReflect() protoreflect.Message { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[15] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VolumeResourceMonitor.ProtoReflect.Descriptor instead. +func (*VolumeResourceMonitor) Descriptor() ([]byte, []int) { + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{15} +} + +func (x *VolumeResourceMonitor) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *VolumeResourceMonitor) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +func (x *VolumeResourceMonitor) GetThreshold() int32 { + if x != nil { + return x.Threshold + } + return 0 +} + type DisplayApps struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1197,7 +1489,7 @@ type DisplayApps struct { func (x *DisplayApps) Reset() { *x = DisplayApps{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[11] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1210,7 +1502,7 @@ func (x *DisplayApps) String() string { func (*DisplayApps) ProtoMessage() {} func (x *DisplayApps) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[11] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1223,7 +1515,7 @@ func (x *DisplayApps) ProtoReflect() protoreflect.Message { // Deprecated: Use DisplayApps.ProtoReflect.Descriptor instead. func (*DisplayApps) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{11} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{16} } func (x *DisplayApps) GetVscode() bool { @@ -1273,7 +1565,7 @@ type Env struct { func (x *Env) Reset() { *x = Env{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[12] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1286,7 +1578,7 @@ func (x *Env) String() string { func (*Env) ProtoMessage() {} func (x *Env) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[12] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1299,7 +1591,7 @@ func (x *Env) ProtoReflect() protoreflect.Message { // Deprecated: Use Env.ProtoReflect.Descriptor instead. func (*Env) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{12} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{17} } func (x *Env) GetName() string { @@ -1336,7 +1628,7 @@ type Script struct { func (x *Script) Reset() { *x = Script{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[13] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1349,7 +1641,7 @@ func (x *Script) String() string { func (*Script) ProtoMessage() {} func (x *Script) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[13] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1362,7 +1654,7 @@ func (x *Script) ProtoReflect() protoreflect.Message { // Deprecated: Use Script.ProtoReflect.Descriptor instead. func (*Script) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{13} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{18} } func (x *Script) GetDisplayName() string { @@ -1453,7 +1745,7 @@ type App struct { func (x *App) Reset() { *x = App{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[14] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1466,7 +1758,7 @@ func (x *App) String() string { func (*App) ProtoMessage() {} func (x *App) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[14] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1479,7 +1771,7 @@ func (x *App) ProtoReflect() protoreflect.Message { // Deprecated: Use App.ProtoReflect.Descriptor instead. func (*App) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{14} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{19} } func (x *App) GetSlug() string { @@ -1580,7 +1872,7 @@ type Healthcheck struct { func (x *Healthcheck) Reset() { *x = Healthcheck{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[15] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1593,7 +1885,7 @@ func (x *Healthcheck) String() string { func (*Healthcheck) ProtoMessage() {} func (x *Healthcheck) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[15] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1606,7 +1898,7 @@ func (x *Healthcheck) ProtoReflect() protoreflect.Message { // Deprecated: Use Healthcheck.ProtoReflect.Descriptor instead. func (*Healthcheck) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{15} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{20} } func (x *Healthcheck) GetUrl() string { @@ -1650,7 +1942,7 @@ type Resource struct { func (x *Resource) Reset() { *x = Resource{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[16] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1663,7 +1955,7 @@ func (x *Resource) String() string { func (*Resource) ProtoMessage() {} func (x *Resource) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[16] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1676,7 +1968,7 @@ func (x *Resource) ProtoReflect() protoreflect.Message { // Deprecated: Use Resource.ProtoReflect.Descriptor instead. func (*Resource) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{16} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{21} } func (x *Resource) GetName() string { @@ -1755,7 +2047,7 @@ type Module struct { func (x *Module) Reset() { *x = Module{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[17] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1768,7 +2060,7 @@ func (x *Module) String() string { func (*Module) ProtoMessage() {} func (x *Module) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[17] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[22] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1781,7 +2073,7 @@ func (x *Module) ProtoReflect() protoreflect.Message { // Deprecated: Use Module.ProtoReflect.Descriptor instead. func (*Module) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{17} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{22} } func (x *Module) GetSource() string { @@ -1834,7 +2126,7 @@ type Metadata struct { func (x *Metadata) Reset() { *x = Metadata{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[18] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1847,7 +2139,7 @@ func (x *Metadata) String() string { func (*Metadata) ProtoMessage() {} func (x *Metadata) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[18] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[23] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1860,7 +2152,7 @@ func (x *Metadata) ProtoReflect() protoreflect.Message { // Deprecated: Use Metadata.ProtoReflect.Descriptor instead. func (*Metadata) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{18} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{23} } func (x *Metadata) GetCoderUrl() string { @@ -2005,7 +2297,7 @@ type Config struct { func (x *Config) Reset() { *x = Config{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[19] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2018,7 +2310,7 @@ func (x *Config) String() string { func (*Config) ProtoMessage() {} func (x *Config) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[19] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[24] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2031,7 +2323,7 @@ func (x *Config) ProtoReflect() protoreflect.Message { // Deprecated: Use Config.ProtoReflect.Descriptor instead. func (*Config) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{19} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{24} } func (x *Config) GetTemplateSourceArchive() []byte { @@ -2065,7 +2357,7 @@ type ParseRequest struct { func (x *ParseRequest) Reset() { *x = ParseRequest{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[20] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2078,7 +2370,7 @@ func (x *ParseRequest) String() string { func (*ParseRequest) ProtoMessage() {} func (x *ParseRequest) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[20] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[25] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2091,7 +2383,7 @@ func (x *ParseRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ParseRequest.ProtoReflect.Descriptor instead. func (*ParseRequest) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{20} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{25} } // ParseComplete indicates a request to parse completed. @@ -2109,7 +2401,7 @@ type ParseComplete struct { func (x *ParseComplete) Reset() { *x = ParseComplete{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[21] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2122,7 +2414,7 @@ func (x *ParseComplete) String() string { func (*ParseComplete) ProtoMessage() {} func (x *ParseComplete) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[21] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[26] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2135,7 +2427,7 @@ func (x *ParseComplete) ProtoReflect() protoreflect.Message { // Deprecated: Use ParseComplete.ProtoReflect.Descriptor instead. func (*ParseComplete) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{21} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{26} } func (x *ParseComplete) GetError() string { @@ -2181,7 +2473,7 @@ type PlanRequest struct { func (x *PlanRequest) Reset() { *x = PlanRequest{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[22] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2194,7 +2486,7 @@ func (x *PlanRequest) String() string { func (*PlanRequest) ProtoMessage() {} func (x *PlanRequest) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[22] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[27] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2207,7 +2499,7 @@ func (x *PlanRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PlanRequest.ProtoReflect.Descriptor instead. func (*PlanRequest) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{22} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{27} } func (x *PlanRequest) GetMetadata() *Metadata { @@ -2250,12 +2542,13 @@ type PlanComplete struct { ExternalAuthProviders []*ExternalAuthProviderResource `protobuf:"bytes,4,rep,name=external_auth_providers,json=externalAuthProviders,proto3" json:"external_auth_providers,omitempty"` Timings []*Timing `protobuf:"bytes,6,rep,name=timings,proto3" json:"timings,omitempty"` Modules []*Module `protobuf:"bytes,7,rep,name=modules,proto3" json:"modules,omitempty"` + Presets []*Preset `protobuf:"bytes,8,rep,name=presets,proto3" json:"presets,omitempty"` } func (x *PlanComplete) Reset() { *x = PlanComplete{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[23] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2268,7 +2561,7 @@ func (x *PlanComplete) String() string { func (*PlanComplete) ProtoMessage() {} func (x *PlanComplete) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[23] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[28] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2281,7 +2574,7 @@ func (x *PlanComplete) ProtoReflect() protoreflect.Message { // Deprecated: Use PlanComplete.ProtoReflect.Descriptor instead. func (*PlanComplete) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{23} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{28} } func (x *PlanComplete) GetError() string { @@ -2326,6 +2619,13 @@ func (x *PlanComplete) GetModules() []*Module { return nil } +func (x *PlanComplete) GetPresets() []*Preset { + if x != nil { + return x.Presets + } + return nil +} + // ApplyRequest asks the provisioner to apply the changes. Apply MUST be preceded by a successful plan request/response // in the same Session. The plan data is not transmitted over the wire and is cached by the provisioner in the Session. type ApplyRequest struct { @@ -2339,7 +2639,7 @@ type ApplyRequest struct { func (x *ApplyRequest) Reset() { *x = ApplyRequest{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[24] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2352,7 +2652,7 @@ func (x *ApplyRequest) String() string { func (*ApplyRequest) ProtoMessage() {} func (x *ApplyRequest) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[24] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[29] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2365,7 +2665,7 @@ func (x *ApplyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ApplyRequest.ProtoReflect.Descriptor instead. func (*ApplyRequest) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{24} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{29} } func (x *ApplyRequest) GetMetadata() *Metadata { @@ -2392,7 +2692,7 @@ type ApplyComplete struct { func (x *ApplyComplete) Reset() { *x = ApplyComplete{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[25] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2405,7 +2705,7 @@ func (x *ApplyComplete) String() string { func (*ApplyComplete) ProtoMessage() {} func (x *ApplyComplete) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[25] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[30] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2418,7 +2718,7 @@ func (x *ApplyComplete) ProtoReflect() protoreflect.Message { // Deprecated: Use ApplyComplete.ProtoReflect.Descriptor instead. func (*ApplyComplete) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{25} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{30} } func (x *ApplyComplete) GetState() []byte { @@ -2480,7 +2780,7 @@ type Timing struct { func (x *Timing) Reset() { *x = Timing{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[26] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2493,7 +2793,7 @@ func (x *Timing) String() string { func (*Timing) ProtoMessage() {} func (x *Timing) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[26] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[31] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2506,7 +2806,7 @@ func (x *Timing) ProtoReflect() protoreflect.Message { // Deprecated: Use Timing.ProtoReflect.Descriptor instead. func (*Timing) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{26} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{31} } func (x *Timing) GetStart() *timestamppb.Timestamp { @@ -2568,7 +2868,7 @@ type CancelRequest struct { func (x *CancelRequest) Reset() { *x = CancelRequest{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[27] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2581,7 +2881,7 @@ func (x *CancelRequest) String() string { func (*CancelRequest) ProtoMessage() {} func (x *CancelRequest) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[27] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[32] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2594,7 +2894,7 @@ func (x *CancelRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CancelRequest.ProtoReflect.Descriptor instead. func (*CancelRequest) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{27} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{32} } type Request struct { @@ -2615,7 +2915,7 @@ type Request struct { func (x *Request) Reset() { *x = Request{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[28] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2628,7 +2928,7 @@ func (x *Request) String() string { func (*Request) ProtoMessage() {} func (x *Request) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[28] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[33] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2641,7 +2941,7 @@ func (x *Request) ProtoReflect() protoreflect.Message { // Deprecated: Use Request.ProtoReflect.Descriptor instead. func (*Request) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{28} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{33} } func (m *Request) GetType() isRequest_Type { @@ -2737,7 +3037,7 @@ type Response struct { func (x *Response) Reset() { *x = Response{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[29] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2750,7 +3050,7 @@ func (x *Response) String() string { func (*Response) ProtoMessage() {} func (x *Response) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[29] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[34] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2763,7 +3063,7 @@ func (x *Response) ProtoReflect() protoreflect.Message { // Deprecated: Use Response.ProtoReflect.Descriptor instead. func (*Response) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{29} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{34} } func (m *Response) GetType() isResponse_Type { @@ -2845,7 +3145,7 @@ type Agent_Metadata struct { func (x *Agent_Metadata) Reset() { *x = Agent_Metadata{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[30] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2858,7 +3158,7 @@ func (x *Agent_Metadata) String() string { func (*Agent_Metadata) ProtoMessage() {} func (x *Agent_Metadata) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[30] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[35] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2871,7 +3171,7 @@ func (x *Agent_Metadata) ProtoReflect() protoreflect.Message { // Deprecated: Use Agent_Metadata.ProtoReflect.Descriptor instead. func (*Agent_Metadata) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{10, 0} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{12, 0} } func (x *Agent_Metadata) GetKey() string { @@ -2930,7 +3230,7 @@ type Resource_Metadata struct { func (x *Resource_Metadata) Reset() { *x = Resource_Metadata{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[32] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2943,7 +3243,7 @@ func (x *Resource_Metadata) String() string { func (*Resource_Metadata) ProtoMessage() {} func (x *Resource_Metadata) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[32] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[37] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2956,7 +3256,7 @@ func (x *Resource_Metadata) ProtoReflect() protoreflect.Message { // Deprecated: Use Resource_Metadata.ProtoReflect.Descriptor instead. func (*Resource_Metadata) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{16, 0} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{21, 0} } func (x *Resource_Metadata) GetKey() string { @@ -3059,409 +3359,448 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x57, 0x0a, 0x0d, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, - 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x22, 0x4a, - 0x0a, 0x03, 0x4c, 0x6f, 0x67, 0x12, 0x2b, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, - 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, - 0x65, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x22, 0x37, 0x0a, 0x14, 0x49, 0x6e, - 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x41, 0x75, - 0x74, 0x68, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, - 0x65, 0x49, 0x64, 0x22, 0x4a, 0x0a, 0x1c, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, - 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x02, 0x69, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x22, - 0x49, 0x0a, 0x14, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, - 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x63, 0x63, 0x65, 0x73, - 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, - 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xa0, 0x07, 0x0a, 0x05, 0x41, - 0x67, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x03, 0x65, 0x6e, 0x76, 0x18, - 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x76, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x52, 0x03, 0x65, 0x6e, 0x76, 0x12, 0x29, 0x0a, 0x10, 0x6f, 0x70, 0x65, 0x72, 0x61, - 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x79, 0x73, 0x74, - 0x65, 0x6d, 0x12, 0x22, 0x0a, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, - 0x72, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, - 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, - 0x6f, 0x72, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, - 0x74, 0x6f, 0x72, 0x79, 0x12, 0x24, 0x0a, 0x04, 0x61, 0x70, 0x70, 0x73, 0x18, 0x08, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x2e, 0x41, 0x70, 0x70, 0x52, 0x04, 0x61, 0x70, 0x70, 0x73, 0x12, 0x16, 0x0a, 0x05, 0x74, 0x6f, - 0x6b, 0x65, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x05, 0x74, 0x6f, 0x6b, - 0x65, 0x6e, 0x12, 0x21, 0x0a, 0x0b, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x69, - 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, - 0x6e, 0x63, 0x65, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x1a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x6f, - 0x6e, 0x64, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x05, 0x52, 0x18, 0x63, 0x6f, 0x6e, 0x6e, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x53, 0x65, 0x63, 0x6f, - 0x6e, 0x64, 0x73, 0x12, 0x2f, 0x0a, 0x13, 0x74, 0x72, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x73, 0x68, - 0x6f, 0x6f, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x12, 0x74, 0x72, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x73, 0x68, 0x6f, 0x6f, 0x74, 0x69, 0x6e, - 0x67, 0x55, 0x72, 0x6c, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x6f, 0x74, 0x64, 0x5f, 0x66, 0x69, 0x6c, - 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x6f, 0x74, 0x64, 0x46, 0x69, 0x6c, - 0x65, 0x12, 0x37, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x12, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, - 0x72, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x3b, 0x0a, 0x0c, 0x64, 0x69, - 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x61, 0x70, 0x70, 0x73, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x44, - 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x41, 0x70, 0x70, 0x73, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, - 0x6c, 0x61, 0x79, 0x41, 0x70, 0x70, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x73, 0x18, 0x15, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, - 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x52, 0x07, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x73, 0x12, 0x2f, 0x0a, 0x0a, 0x65, 0x78, 0x74, 0x72, 0x61, 0x5f, - 0x65, 0x6e, 0x76, 0x73, 0x18, 0x16, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, - 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x45, 0x6e, 0x76, 0x52, 0x09, 0x65, 0x78, - 0x74, 0x72, 0x61, 0x45, 0x6e, 0x76, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6f, 0x72, 0x64, 0x65, 0x72, - 0x18, 0x17, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x1a, 0xa3, 0x01, - 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x21, 0x0a, 0x0c, - 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, - 0x16, 0x0a, 0x06, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, - 0x76, 0x61, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, - 0x76, 0x61, 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, 0x14, 0x0a, - 0x05, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6f, 0x72, - 0x64, 0x65, 0x72, 0x1a, 0x36, 0x0a, 0x08, 0x45, 0x6e, 0x76, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, - 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, - 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x06, 0x0a, 0x04, 0x61, - 0x75, 0x74, 0x68, 0x4a, 0x04, 0x08, 0x0e, 0x10, 0x0f, 0x52, 0x12, 0x6c, 0x6f, 0x67, 0x69, 0x6e, - 0x5f, 0x62, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x5f, 0x72, 0x65, 0x61, 0x64, 0x79, 0x22, 0xc6, 0x01, - 0x0a, 0x0b, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x41, 0x70, 0x70, 0x73, 0x12, 0x16, 0x0a, - 0x06, 0x76, 0x73, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x76, - 0x73, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x76, 0x73, 0x63, 0x6f, 0x64, 0x65, 0x5f, - 0x69, 0x6e, 0x73, 0x69, 0x64, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, - 0x76, 0x73, 0x63, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x73, 0x69, 0x64, 0x65, 0x72, 0x73, 0x12, 0x21, - 0x0a, 0x0c, 0x77, 0x65, 0x62, 0x5f, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x77, 0x65, 0x62, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, - 0x6c, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x73, 0x68, 0x5f, 0x68, 0x65, 0x6c, 0x70, 0x65, 0x72, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x73, 0x68, 0x48, 0x65, 0x6c, 0x70, 0x65, 0x72, - 0x12, 0x34, 0x0a, 0x16, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, - 0x69, 0x6e, 0x67, 0x5f, 0x68, 0x65, 0x6c, 0x70, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x14, 0x70, 0x6f, 0x72, 0x74, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, - 0x48, 0x65, 0x6c, 0x70, 0x65, 0x72, 0x22, 0x2f, 0x0a, 0x03, 0x45, 0x6e, 0x76, 0x12, 0x12, 0x0a, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x9f, 0x02, 0x0a, 0x06, 0x53, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, - 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x72, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x63, 0x72, 0x6f, 0x6e, 0x12, 0x2c, 0x0a, 0x12, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x62, - 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x5f, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x10, 0x73, 0x74, 0x61, 0x72, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x4c, 0x6f, - 0x67, 0x69, 0x6e, 0x12, 0x20, 0x0a, 0x0c, 0x72, 0x75, 0x6e, 0x5f, 0x6f, 0x6e, 0x5f, 0x73, 0x74, - 0x61, 0x72, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x72, 0x75, 0x6e, 0x4f, 0x6e, - 0x53, 0x74, 0x61, 0x72, 0x74, 0x12, 0x1e, 0x0a, 0x0b, 0x72, 0x75, 0x6e, 0x5f, 0x6f, 0x6e, 0x5f, - 0x73, 0x74, 0x6f, 0x70, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x72, 0x75, 0x6e, 0x4f, - 0x6e, 0x53, 0x74, 0x6f, 0x70, 0x12, 0x27, 0x0a, 0x0f, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, - 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, - 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x19, - 0x0a, 0x08, 0x6c, 0x6f, 0x67, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x50, 0x61, 0x74, 0x68, 0x22, 0x94, 0x03, 0x0a, 0x03, 0x41, 0x70, - 0x70, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x6c, 0x75, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x73, 0x6c, 0x75, 0x67, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, - 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x69, 0x73, - 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, - 0x61, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, - 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x03, 0x75, 0x72, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x75, 0x62, 0x64, - 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x75, 0x62, - 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x3a, 0x0a, 0x0b, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, - 0x63, 0x68, 0x65, 0x63, 0x6b, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, - 0x63, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x0b, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, - 0x63, 0x6b, 0x12, 0x41, 0x0a, 0x0d, 0x73, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x6c, 0x65, - 0x76, 0x65, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x70, 0x70, 0x53, 0x68, 0x61, 0x72, 0x69, - 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x0c, 0x73, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, - 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, - 0x6c, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, - 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x05, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x69, 0x64, 0x64, 0x65, - 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x68, 0x69, 0x64, 0x64, 0x65, 0x6e, 0x12, - 0x2f, 0x0a, 0x07, 0x6f, 0x70, 0x65, 0x6e, 0x5f, 0x69, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, - 0x70, 0x70, 0x4f, 0x70, 0x65, 0x6e, 0x49, 0x6e, 0x52, 0x06, 0x6f, 0x70, 0x65, 0x6e, 0x49, 0x6e, - 0x22, 0x59, 0x0a, 0x0b, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x12, - 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, - 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x05, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x1c, 0x0a, - 0x09, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, - 0x52, 0x09, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x22, 0x92, 0x03, 0x0a, 0x08, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, - 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, - 0x12, 0x2a, 0x0a, 0x06, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, - 0x67, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x3a, 0x0a, 0x08, - 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, - 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x69, 0x64, 0x65, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x68, 0x69, 0x64, 0x65, 0x12, 0x12, 0x0a, 0x04, - 0x69, 0x63, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x69, 0x63, 0x6f, 0x6e, - 0x12, 0x23, 0x0a, 0x0d, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, - 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, - 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x61, 0x69, 0x6c, 0x79, 0x5f, 0x63, - 0x6f, 0x73, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x64, 0x61, 0x69, 0x6c, 0x79, - 0x43, 0x6f, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x5f, 0x70, - 0x61, 0x74, 0x68, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x6f, 0x64, 0x75, 0x6c, - 0x65, 0x50, 0x61, 0x74, 0x68, 0x1a, 0x69, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, - 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x5a, 0x0a, 0x06, 0x50, 0x72, 0x65, 0x73, 0x65, 0x74, 0x12, + 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x3c, 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, + 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x65, 0x73, 0x65, 0x74, 0x50, 0x61, 0x72, 0x61, + 0x6d, 0x65, 0x74, 0x65, 0x72, 0x52, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, + 0x73, 0x22, 0x3b, 0x0a, 0x0f, 0x50, 0x72, 0x65, 0x73, 0x65, 0x74, 0x50, 0x61, 0x72, 0x61, 0x6d, + 0x65, 0x74, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x57, + 0x0a, 0x0d, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, + 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x65, - 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x69, 0x73, 0x5f, 0x6e, 0x75, - 0x6c, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x69, 0x73, 0x4e, 0x75, 0x6c, 0x6c, - 0x22, 0x4c, 0x0a, 0x06, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x10, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0xac, - 0x07, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1b, 0x0a, 0x09, 0x63, - 0x6f, 0x64, 0x65, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x63, 0x6f, 0x64, 0x65, 0x72, 0x55, 0x72, 0x6c, 0x12, 0x53, 0x0a, 0x14, 0x77, 0x6f, 0x72, 0x6b, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, - 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, - 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, - 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x77, - 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x12, 0x21, 0x0a, - 0x0c, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, - 0x12, 0x2c, 0x0a, 0x12, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, - 0x6e, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x12, 0x32, - 0x0a, 0x15, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, - 0x72, 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x77, - 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x45, 0x6d, 0x61, - 0x69, 0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x65, 0x6d, 0x70, 0x6c, - 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x74, 0x65, 0x6d, 0x70, 0x6c, - 0x61, 0x74, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, - 0x6f, 0x6e, 0x12, 0x48, 0x0a, 0x21, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, - 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, - 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1d, 0x77, - 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x4f, 0x69, 0x64, - 0x63, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x41, 0x0a, 0x1d, - 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, - 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x0b, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x1a, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, - 0x6e, 0x65, 0x72, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, - 0x1f, 0x0a, 0x0b, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x0c, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x49, 0x64, - 0x12, 0x30, 0x0a, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, - 0x6e, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, - 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x4e, 0x61, - 0x6d, 0x65, 0x12, 0x34, 0x0a, 0x16, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, - 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x0e, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, - 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x42, 0x0a, 0x1e, 0x77, 0x6f, 0x72, 0x6b, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x73, 0x73, 0x68, 0x5f, - 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x1a, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, - 0x53, 0x73, 0x68, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x44, 0x0a, 0x1f, - 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, - 0x73, 0x73, 0x68, 0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, - 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x53, 0x73, 0x68, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, - 0x65, 0x79, 0x12, 0x2c, 0x0a, 0x12, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, - 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, - 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x64, - 0x12, 0x3b, 0x0a, 0x1a, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, - 0x6e, 0x65, 0x72, 0x5f, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x12, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, - 0x77, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x22, 0x8a, 0x01, - 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x17, 0x74, 0x65, 0x6d, 0x70, - 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x72, 0x63, 0x68, - 0x69, 0x76, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x15, 0x74, 0x65, 0x6d, 0x70, 0x6c, - 0x61, 0x74, 0x65, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, - 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x32, 0x0a, 0x15, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, - 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x5f, 0x6c, 0x6f, 0x67, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, - 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x0e, 0x0a, 0x0c, 0x50, 0x61, - 0x72, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xa3, 0x02, 0x0a, 0x0d, 0x50, - 0x61, 0x72, 0x73, 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, - 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, - 0x6f, 0x72, 0x12, 0x4c, 0x0a, 0x12, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x76, - 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, 0x65, 0x6d, - 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x11, 0x74, - 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, - 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x64, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, - 0x52, 0x06, 0x72, 0x65, 0x61, 0x64, 0x6d, 0x65, 0x12, 0x54, 0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x2d, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, - 0x61, 0x72, 0x73, 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x2e, 0x57, 0x6f, 0x72, - 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, - 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x61, 0x67, 0x73, 0x1a, 0x40, - 0x0a, 0x12, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x61, 0x67, 0x73, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, - 0x22, 0xb5, 0x02, 0x0a, 0x0b, 0x50, 0x6c, 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, + 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x22, 0x4a, 0x0a, 0x03, 0x4c, 0x6f, 0x67, 0x12, 0x2b, + 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, + 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x6f, + 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, + 0x70, 0x75, 0x74, 0x22, 0x37, 0x0a, 0x14, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x41, 0x75, 0x74, 0x68, 0x12, 0x1f, 0x0a, 0x0b, 0x69, + 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x22, 0x4a, 0x0a, 0x1c, + 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1a, 0x0a, 0x08, + 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, + 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x22, 0x49, 0x0a, 0x14, 0x45, 0x78, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, + 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x22, 0xf5, 0x07, 0x0a, 0x05, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, + 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x12, 0x2d, 0x0a, 0x03, 0x65, 0x6e, 0x76, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, + 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x67, 0x65, + 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x76, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x03, 0x65, 0x6e, 0x76, + 0x12, 0x29, 0x0a, 0x10, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x73, 0x79, + 0x73, 0x74, 0x65, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x6f, 0x70, 0x65, 0x72, + 0x61, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x22, 0x0a, 0x0c, 0x61, + 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x12, + 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x24, 0x0a, + 0x04, 0x61, 0x70, 0x70, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x70, 0x70, 0x52, 0x04, 0x61, + 0x70, 0x70, 0x73, 0x12, 0x16, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x09, 0x20, 0x01, + 0x28, 0x09, 0x48, 0x00, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x21, 0x0a, 0x0b, 0x69, + 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, + 0x48, 0x00, 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x12, 0x3c, + 0x0a, 0x1a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, + 0x65, 0x6f, 0x75, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x0b, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x18, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, + 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x2f, 0x0a, 0x13, + 0x74, 0x72, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x73, 0x68, 0x6f, 0x6f, 0x74, 0x69, 0x6e, 0x67, 0x5f, + 0x75, 0x72, 0x6c, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x74, 0x72, 0x6f, 0x75, 0x62, + 0x6c, 0x65, 0x73, 0x68, 0x6f, 0x6f, 0x74, 0x69, 0x6e, 0x67, 0x55, 0x72, 0x6c, 0x12, 0x1b, 0x0a, + 0x09, 0x6d, 0x6f, 0x74, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x6d, 0x6f, 0x74, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x37, 0x0a, 0x08, 0x6d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x12, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x12, 0x53, 0x0a, 0x15, 0x72, 0x69, 0x63, 0x68, 0x5f, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x52, 0x13, 0x72, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, - 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x43, 0x0a, 0x0f, 0x76, 0x61, 0x72, 0x69, - 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, - 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0e, 0x76, - 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x59, 0x0a, - 0x17, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, - 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x45, 0x78, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, - 0x72, 0x52, 0x15, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, - 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x22, 0xd6, 0x02, 0x0a, 0x0c, 0x50, 0x6c, 0x61, - 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, - 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, - 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x73, 0x12, 0x3a, 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, - 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x61, 0x74, 0x61, 0x12, 0x3b, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x61, + 0x70, 0x70, 0x73, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x41, + 0x70, 0x70, 0x73, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x41, 0x70, 0x70, 0x73, + 0x12, 0x2d, 0x0a, 0x07, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x73, 0x18, 0x15, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, + 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x52, 0x07, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x73, 0x12, + 0x2f, 0x0a, 0x0a, 0x65, 0x78, 0x74, 0x72, 0x61, 0x5f, 0x65, 0x6e, 0x76, 0x73, 0x18, 0x16, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, + 0x72, 0x2e, 0x45, 0x6e, 0x76, 0x52, 0x09, 0x65, 0x78, 0x74, 0x72, 0x61, 0x45, 0x6e, 0x76, 0x73, + 0x12, 0x14, 0x0a, 0x05, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x17, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x05, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x53, 0x0a, 0x14, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x73, 0x5f, 0x6d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x18, 0x18, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, + 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x13, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x1a, 0xa3, 0x01, 0x0a, 0x08, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, + 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, + 0x06, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, + 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, + 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6f, + 0x72, 0x64, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6f, 0x72, 0x64, 0x65, + 0x72, 0x1a, 0x36, 0x0a, 0x08, 0x45, 0x6e, 0x76, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, + 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, + 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x06, 0x0a, 0x04, 0x61, 0x75, 0x74, + 0x68, 0x4a, 0x04, 0x08, 0x0e, 0x10, 0x0f, 0x52, 0x12, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x62, + 0x65, 0x66, 0x6f, 0x72, 0x65, 0x5f, 0x72, 0x65, 0x61, 0x64, 0x79, 0x22, 0x8f, 0x01, 0x0a, 0x13, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, + 0x69, 0x6e, 0x67, 0x12, 0x3a, 0x0a, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, + 0x72, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x52, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x12, + 0x3c, 0x0a, 0x07, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x22, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x56, + 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4d, 0x6f, 0x6e, + 0x69, 0x74, 0x6f, 0x72, 0x52, 0x07, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x22, 0x4f, 0x0a, + 0x15, 0x4d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4d, + 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, + 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x09, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x22, 0x63, + 0x0a, 0x15, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x18, 0x0a, 0x07, 0x65, + 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, + 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, + 0x6c, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, + 0x6f, 0x6c, 0x64, 0x22, 0xc6, 0x01, 0x0a, 0x0b, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x41, + 0x70, 0x70, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x73, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x06, 0x76, 0x73, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x76, + 0x73, 0x63, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x6e, 0x73, 0x69, 0x64, 0x65, 0x72, 0x73, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x76, 0x73, 0x63, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x73, 0x69, + 0x64, 0x65, 0x72, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x77, 0x65, 0x62, 0x5f, 0x74, 0x65, 0x72, 0x6d, + 0x69, 0x6e, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x77, 0x65, 0x62, 0x54, + 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x73, 0x68, 0x5f, 0x68, + 0x65, 0x6c, 0x70, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x73, 0x68, + 0x48, 0x65, 0x6c, 0x70, 0x65, 0x72, 0x12, 0x34, 0x0a, 0x16, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x66, + 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x68, 0x65, 0x6c, 0x70, 0x65, 0x72, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x70, 0x6f, 0x72, 0x74, 0x46, 0x6f, 0x72, 0x77, + 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x48, 0x65, 0x6c, 0x70, 0x65, 0x72, 0x22, 0x2f, 0x0a, 0x03, + 0x45, 0x6e, 0x76, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x9f, 0x02, + 0x0a, 0x06, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, + 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, + 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x69, + 0x63, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x12, + 0x16, 0x0a, 0x06, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x72, 0x6f, 0x6e, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x72, 0x6f, 0x6e, 0x12, 0x2c, 0x0a, 0x12, 0x73, + 0x74, 0x61, 0x72, 0x74, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x5f, 0x6c, 0x6f, 0x67, 0x69, + 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x73, 0x74, 0x61, 0x72, 0x74, 0x42, 0x6c, + 0x6f, 0x63, 0x6b, 0x73, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x20, 0x0a, 0x0c, 0x72, 0x75, 0x6e, + 0x5f, 0x6f, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x0a, 0x72, 0x75, 0x6e, 0x4f, 0x6e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x12, 0x1e, 0x0a, 0x0b, 0x72, + 0x75, 0x6e, 0x5f, 0x6f, 0x6e, 0x5f, 0x73, 0x74, 0x6f, 0x70, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x09, 0x72, 0x75, 0x6e, 0x4f, 0x6e, 0x53, 0x74, 0x6f, 0x70, 0x12, 0x27, 0x0a, 0x0f, 0x74, + 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x08, + 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x53, 0x65, 0x63, + 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x6c, 0x6f, 0x67, 0x5f, 0x70, 0x61, 0x74, 0x68, + 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x50, 0x61, 0x74, 0x68, 0x22, + 0x94, 0x03, 0x0a, 0x03, 0x41, 0x70, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x6c, 0x75, 0x67, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x73, 0x6c, 0x75, 0x67, 0x12, 0x21, 0x0a, 0x0c, 0x64, + 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x18, + 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x63, + 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x12, 0x1c, + 0x0a, 0x09, 0x73, 0x75, 0x62, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x09, 0x73, 0x75, 0x62, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x3a, 0x0a, 0x0b, + 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, + 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x0b, 0x68, 0x65, 0x61, + 0x6c, 0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x41, 0x0a, 0x0d, 0x73, 0x68, 0x61, 0x72, + 0x69, 0x6e, 0x67, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x70, + 0x70, 0x53, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x0c, 0x73, + 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x65, + 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x65, + 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x6f, 0x72, 0x64, 0x65, 0x72, + 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, + 0x06, 0x68, 0x69, 0x64, 0x64, 0x65, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x68, + 0x69, 0x64, 0x64, 0x65, 0x6e, 0x12, 0x2f, 0x0a, 0x07, 0x6f, 0x70, 0x65, 0x6e, 0x5f, 0x69, 0x6e, + 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x70, 0x70, 0x4f, 0x70, 0x65, 0x6e, 0x49, 0x6e, 0x52, 0x06, + 0x6f, 0x70, 0x65, 0x6e, 0x49, 0x6e, 0x22, 0x59, 0x0a, 0x0b, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, + 0x63, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x76, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x76, 0x61, 0x6c, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, + 0x64, 0x22, 0x92, 0x03, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, + 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2a, 0x0a, 0x06, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, + 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x61, 0x67, 0x65, 0x6e, + 0x74, 0x73, 0x12, 0x3a, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x12, + 0x0a, 0x04, 0x68, 0x69, 0x64, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x68, 0x69, + 0x64, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, + 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x69, + 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x64, + 0x61, 0x69, 0x6c, 0x79, 0x5f, 0x63, 0x6f, 0x73, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x09, 0x64, 0x61, 0x69, 0x6c, 0x79, 0x43, 0x6f, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x6f, + 0x64, 0x75, 0x6c, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0a, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x50, 0x61, 0x74, 0x68, 0x1a, 0x69, 0x0a, 0x08, 0x4d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, + 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x12, 0x17, 0x0a, + 0x07, 0x69, 0x73, 0x5f, 0x6e, 0x75, 0x6c, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, + 0x69, 0x73, 0x4e, 0x75, 0x6c, 0x6c, 0x22, 0x4c, 0x0a, 0x06, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, + 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x22, 0xac, 0x07, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x55, 0x72, 0x6c, 0x12, 0x53, + 0x0a, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x74, 0x72, 0x61, 0x6e, + 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x13, + 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x77, 0x6f, 0x72, + 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x77, 0x6f, + 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, + 0x6e, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x2c, 0x0a, 0x12, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, + 0x65, 0x72, 0x49, 0x64, 0x12, 0x32, 0x0a, 0x15, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, + 0x6e, 0x65, 0x72, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x74, 0x65, 0x6d, 0x70, + 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0c, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x29, 0x0a, + 0x10, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, + 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x48, 0x0a, 0x21, 0x77, 0x6f, 0x72, 0x6b, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x6f, 0x69, 0x64, 0x63, + 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x1d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, + 0x6e, 0x65, 0x72, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 0x12, 0x41, 0x0a, 0x1d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, + 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1a, 0x77, 0x6f, 0x72, 0x6b, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, + 0x65, 0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x65, 0x6d, 0x70, + 0x6c, 0x61, 0x74, 0x65, 0x49, 0x64, 0x12, 0x30, 0x0a, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0d, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, + 0x77, 0x6e, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x34, 0x0a, 0x16, 0x77, 0x6f, 0x72, 0x6b, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x67, 0x72, 0x6f, 0x75, + 0x70, 0x73, 0x18, 0x0e, 0x20, 0x03, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x42, + 0x0a, 0x1e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, + 0x72, 0x5f, 0x73, 0x73, 0x68, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, + 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1a, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x53, 0x73, 0x68, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, + 0x65, 0x79, 0x12, 0x44, 0x0a, 0x1f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, + 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x73, 0x73, 0x68, 0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, + 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1b, 0x77, 0x6f, 0x72, + 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x53, 0x73, 0x68, 0x50, 0x72, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x2c, 0x0a, 0x12, 0x77, 0x6f, 0x72, 0x6b, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x11, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, + 0x75, 0x69, 0x6c, 0x64, 0x49, 0x64, 0x12, 0x3b, 0x0a, 0x1a, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, + 0x74, 0x79, 0x70, 0x65, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x54, + 0x79, 0x70, 0x65, 0x22, 0x8a, 0x01, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, + 0x0a, 0x17, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x15, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, + 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x32, 0x0a, 0x15, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x5f, 0x6c, 0x6f, 0x67, 0x5f, + 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, + 0x22, 0x0e, 0x0a, 0x0c, 0x50, 0x61, 0x72, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x22, 0xa3, 0x02, 0x0a, 0x0d, 0x50, 0x61, 0x72, 0x73, 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, + 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x4c, 0x0a, 0x12, 0x74, 0x65, 0x6d, 0x70, + 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x18, 0x02, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x65, 0x72, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, + 0x62, 0x6c, 0x65, 0x52, 0x11, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x61, 0x72, + 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x64, 0x6d, 0x65, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x72, 0x65, 0x61, 0x64, 0x6d, 0x65, 0x12, 0x54, + 0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x74, 0x61, 0x67, 0x73, + 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, + 0x74, 0x65, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x61, 0x67, 0x73, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x54, 0x61, 0x67, 0x73, 0x1a, 0x40, 0x0a, 0x12, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xb5, 0x02, 0x0a, 0x0b, 0x50, 0x6c, 0x61, 0x6e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, + 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x53, 0x0a, 0x15, 0x72, 0x69, 0x63, + 0x68, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, - 0x65, 0x74, 0x65, 0x72, 0x52, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, - 0x12, 0x61, 0x0a, 0x17, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x75, 0x74, - 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x29, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, - 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, - 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x15, 0x65, 0x78, - 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, - 0x65, 0x72, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x06, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, - 0x65, 0x72, 0x2e, 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x52, 0x07, 0x74, 0x69, 0x6d, 0x69, 0x6e, - 0x67, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x07, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, - 0x72, 0x2e, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x52, 0x07, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, - 0x73, 0x22, 0x41, 0x0a, 0x0c, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, - 0x72, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x22, 0xbe, 0x02, 0x0a, 0x0d, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x43, 0x6f, - 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, - 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, - 0x6f, 0x72, 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, - 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x3a, 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, - 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, 0x61, - 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x52, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, - 0x65, 0x72, 0x73, 0x12, 0x61, 0x0a, 0x17, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, - 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x18, 0x05, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x13, 0x72, 0x69, 0x63, 0x68, 0x50, + 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x43, + 0x0a, 0x0f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x52, 0x0e, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x73, 0x12, 0x59, 0x0a, 0x17, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, + 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x18, 0x04, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, - 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, - 0x15, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, - 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x69, 0x6e, 0x67, - 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, - 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x52, 0x07, 0x74, 0x69, - 0x6d, 0x69, 0x6e, 0x67, 0x73, 0x22, 0xfa, 0x01, 0x0a, 0x06, 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, - 0x12, 0x30, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x05, 0x73, 0x74, 0x61, - 0x72, 0x74, 0x12, 0x2c, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x03, 0x65, 0x6e, 0x64, - 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, - 0x73, 0x74, 0x61, 0x67, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, - 0x67, 0x65, 0x12, 0x2e, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, - 0x0e, 0x32, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, - 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, - 0x74, 0x65, 0x22, 0x0f, 0x0a, 0x0d, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x22, 0x8c, 0x02, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x2d, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x48, 0x00, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x31, - 0x0a, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, - 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x05, 0x70, 0x61, 0x72, 0x73, - 0x65, 0x12, 0x2e, 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x6c, - 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6c, 0x61, - 0x6e, 0x12, 0x31, 0x0a, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, - 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x05, 0x61, - 0x70, 0x70, 0x6c, 0x79, 0x12, 0x34, 0x0a, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, - 0x65, 0x72, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x48, 0x00, 0x52, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, - 0x70, 0x65, 0x22, 0xd1, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x24, 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, - 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x48, 0x00, - 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x12, 0x32, 0x0a, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, - 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, - 0x48, 0x00, 0x52, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x04, 0x70, 0x6c, 0x61, - 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, - 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, - 0x74, 0x65, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x12, 0x32, 0x0a, 0x05, 0x61, 0x70, - 0x70, 0x6c, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x43, 0x6f, 0x6d, - 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x42, 0x06, - 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x2a, 0x3f, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, - 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x00, 0x12, 0x09, 0x0a, - 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, - 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, - 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x2a, 0x3b, 0x0a, 0x0f, 0x41, 0x70, 0x70, 0x53, 0x68, - 0x61, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x4f, 0x57, - 0x4e, 0x45, 0x52, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, - 0x49, 0x43, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x55, 0x42, 0x4c, - 0x49, 0x43, 0x10, 0x02, 0x2a, 0x35, 0x0a, 0x09, 0x41, 0x70, 0x70, 0x4f, 0x70, 0x65, 0x6e, 0x49, - 0x6e, 0x12, 0x0e, 0x0a, 0x06, 0x57, 0x49, 0x4e, 0x44, 0x4f, 0x57, 0x10, 0x00, 0x1a, 0x02, 0x08, - 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x53, 0x4c, 0x49, 0x4d, 0x5f, 0x57, 0x49, 0x4e, 0x44, 0x4f, 0x57, - 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x41, 0x42, 0x10, 0x02, 0x2a, 0x37, 0x0a, 0x13, 0x57, - 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, - 0x04, 0x53, 0x54, 0x4f, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x53, 0x54, 0x52, - 0x4f, 0x59, 0x10, 0x02, 0x2a, 0x35, 0x0a, 0x0b, 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x54, 0x41, 0x52, 0x54, 0x45, 0x44, 0x10, 0x00, - 0x12, 0x0d, 0x0a, 0x09, 0x43, 0x4f, 0x4d, 0x50, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, - 0x0a, 0x0a, 0x06, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x02, 0x32, 0x49, 0x0a, 0x0b, 0x50, - 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x12, 0x3a, 0x0a, 0x07, 0x53, 0x65, - 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x30, 0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, - 0x2f, 0x76, 0x32, 0x2f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x73, - 0x64, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x15, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x22, 0x85, + 0x03, 0x0a, 0x0c, 0x50, 0x6c, 0x61, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, + 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, + 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x3a, 0x0a, 0x0a, 0x70, 0x61, + 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x69, 0x63, + 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x52, 0x0a, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x61, 0x0a, 0x17, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, + 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x52, 0x15, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, + 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x74, 0x69, 0x6d, + 0x69, 0x6e, 0x67, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x52, + 0x07, 0x74, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x6d, 0x6f, 0x64, 0x75, + 0x6c, 0x65, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x52, 0x07, + 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x70, 0x72, 0x65, 0x73, 0x65, + 0x74, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x65, 0x73, 0x65, 0x74, 0x52, 0x07, 0x70, + 0x72, 0x65, 0x73, 0x65, 0x74, 0x73, 0x22, 0x41, 0x0a, 0x0c, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, + 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0xbe, 0x02, 0x0a, 0x0d, 0x41, 0x70, + 0x70, 0x6c, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, + 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, + 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x3a, 0x0a, 0x0a, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, + 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x52, 0x0a, 0x70, 0x61, + 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x61, 0x0a, 0x17, 0x65, 0x78, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, + 0x65, 0x72, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x52, 0x15, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, + 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x74, + 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, 0x69, 0x6d, 0x69, 0x6e, + 0x67, 0x52, 0x07, 0x74, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x73, 0x22, 0xfa, 0x01, 0x0a, 0x06, 0x54, + 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x12, 0x30, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x2c, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x52, 0x03, 0x65, 0x6e, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, + 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x12, 0x2e, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, + 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x22, 0x0f, 0x0a, 0x0d, 0x43, 0x61, 0x6e, 0x63, 0x65, + 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x8c, 0x02, 0x0a, 0x07, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x2d, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x65, 0x72, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x00, 0x52, 0x06, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x12, 0x31, 0x0a, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, + 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, + 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x65, 0x72, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, + 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x12, 0x31, 0x0a, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x48, 0x00, 0x52, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x12, 0x34, 0x0a, 0x06, 0x63, 0x61, 0x6e, + 0x63, 0x65, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x42, + 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xd1, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, + 0x4c, 0x6f, 0x67, 0x48, 0x00, 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x12, 0x32, 0x0a, 0x05, 0x70, 0x61, + 0x72, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x43, 0x6f, 0x6d, + 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x12, 0x2f, + 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x43, + 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x12, + 0x32, 0x0a, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x70, 0x70, + 0x6c, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x05, 0x61, 0x70, + 0x70, 0x6c, 0x79, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x2a, 0x3f, 0x0a, 0x08, 0x4c, + 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, + 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x01, 0x12, 0x08, 0x0a, + 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, + 0x03, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x2a, 0x3b, 0x0a, 0x0f, + 0x41, 0x70, 0x70, 0x53, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, + 0x09, 0x0a, 0x05, 0x4f, 0x57, 0x4e, 0x45, 0x52, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x41, 0x55, + 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0a, 0x0a, + 0x06, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x10, 0x02, 0x2a, 0x35, 0x0a, 0x09, 0x41, 0x70, 0x70, + 0x4f, 0x70, 0x65, 0x6e, 0x49, 0x6e, 0x12, 0x0e, 0x0a, 0x06, 0x57, 0x49, 0x4e, 0x44, 0x4f, 0x57, + 0x10, 0x00, 0x1a, 0x02, 0x08, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x53, 0x4c, 0x49, 0x4d, 0x5f, 0x57, + 0x49, 0x4e, 0x44, 0x4f, 0x57, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x41, 0x42, 0x10, 0x02, + 0x2a, 0x37, 0x0a, 0x13, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, + 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x54, 0x41, 0x52, 0x54, + 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x54, 0x4f, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, + 0x44, 0x45, 0x53, 0x54, 0x52, 0x4f, 0x59, 0x10, 0x02, 0x2a, 0x35, 0x0a, 0x0b, 0x54, 0x69, 0x6d, + 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x54, 0x41, 0x52, + 0x54, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x43, 0x4f, 0x4d, 0x50, 0x4c, 0x45, 0x54, + 0x45, 0x44, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x02, + 0x32, 0x49, 0x0a, 0x0b, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x12, + 0x3a, 0x0a, 0x07, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x2e, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x30, 0x5a, 0x2e, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -3477,7 +3816,7 @@ func file_provisionersdk_proto_provisioner_proto_rawDescGZIP() []byte { } var file_provisionersdk_proto_provisioner_proto_enumTypes = make([]protoimpl.EnumInfo, 5) -var file_provisionersdk_proto_provisioner_proto_msgTypes = make([]protoimpl.MessageInfo, 34) +var file_provisionersdk_proto_provisioner_proto_msgTypes = make([]protoimpl.MessageInfo, 39) var file_provisionersdk_proto_provisioner_proto_goTypes = []interface{}{ (LogLevel)(0), // 0: provisioner.LogLevel (AppSharingLevel)(0), // 1: provisioner.AppSharingLevel @@ -3489,87 +3828,97 @@ var file_provisionersdk_proto_provisioner_proto_goTypes = []interface{}{ (*RichParameterOption)(nil), // 7: provisioner.RichParameterOption (*RichParameter)(nil), // 8: provisioner.RichParameter (*RichParameterValue)(nil), // 9: provisioner.RichParameterValue - (*VariableValue)(nil), // 10: provisioner.VariableValue - (*Log)(nil), // 11: provisioner.Log - (*InstanceIdentityAuth)(nil), // 12: provisioner.InstanceIdentityAuth - (*ExternalAuthProviderResource)(nil), // 13: provisioner.ExternalAuthProviderResource - (*ExternalAuthProvider)(nil), // 14: provisioner.ExternalAuthProvider - (*Agent)(nil), // 15: provisioner.Agent - (*DisplayApps)(nil), // 16: provisioner.DisplayApps - (*Env)(nil), // 17: provisioner.Env - (*Script)(nil), // 18: provisioner.Script - (*App)(nil), // 19: provisioner.App - (*Healthcheck)(nil), // 20: provisioner.Healthcheck - (*Resource)(nil), // 21: provisioner.Resource - (*Module)(nil), // 22: provisioner.Module - (*Metadata)(nil), // 23: provisioner.Metadata - (*Config)(nil), // 24: provisioner.Config - (*ParseRequest)(nil), // 25: provisioner.ParseRequest - (*ParseComplete)(nil), // 26: provisioner.ParseComplete - (*PlanRequest)(nil), // 27: provisioner.PlanRequest - (*PlanComplete)(nil), // 28: provisioner.PlanComplete - (*ApplyRequest)(nil), // 29: provisioner.ApplyRequest - (*ApplyComplete)(nil), // 30: provisioner.ApplyComplete - (*Timing)(nil), // 31: provisioner.Timing - (*CancelRequest)(nil), // 32: provisioner.CancelRequest - (*Request)(nil), // 33: provisioner.Request - (*Response)(nil), // 34: provisioner.Response - (*Agent_Metadata)(nil), // 35: provisioner.Agent.Metadata - nil, // 36: provisioner.Agent.EnvEntry - (*Resource_Metadata)(nil), // 37: provisioner.Resource.Metadata - nil, // 38: provisioner.ParseComplete.WorkspaceTagsEntry - (*timestamppb.Timestamp)(nil), // 39: google.protobuf.Timestamp + (*Preset)(nil), // 10: provisioner.Preset + (*PresetParameter)(nil), // 11: provisioner.PresetParameter + (*VariableValue)(nil), // 12: provisioner.VariableValue + (*Log)(nil), // 13: provisioner.Log + (*InstanceIdentityAuth)(nil), // 14: provisioner.InstanceIdentityAuth + (*ExternalAuthProviderResource)(nil), // 15: provisioner.ExternalAuthProviderResource + (*ExternalAuthProvider)(nil), // 16: provisioner.ExternalAuthProvider + (*Agent)(nil), // 17: provisioner.Agent + (*ResourcesMonitoring)(nil), // 18: provisioner.ResourcesMonitoring + (*MemoryResourceMonitor)(nil), // 19: provisioner.MemoryResourceMonitor + (*VolumeResourceMonitor)(nil), // 20: provisioner.VolumeResourceMonitor + (*DisplayApps)(nil), // 21: provisioner.DisplayApps + (*Env)(nil), // 22: provisioner.Env + (*Script)(nil), // 23: provisioner.Script + (*App)(nil), // 24: provisioner.App + (*Healthcheck)(nil), // 25: provisioner.Healthcheck + (*Resource)(nil), // 26: provisioner.Resource + (*Module)(nil), // 27: provisioner.Module + (*Metadata)(nil), // 28: provisioner.Metadata + (*Config)(nil), // 29: provisioner.Config + (*ParseRequest)(nil), // 30: provisioner.ParseRequest + (*ParseComplete)(nil), // 31: provisioner.ParseComplete + (*PlanRequest)(nil), // 32: provisioner.PlanRequest + (*PlanComplete)(nil), // 33: provisioner.PlanComplete + (*ApplyRequest)(nil), // 34: provisioner.ApplyRequest + (*ApplyComplete)(nil), // 35: provisioner.ApplyComplete + (*Timing)(nil), // 36: provisioner.Timing + (*CancelRequest)(nil), // 37: provisioner.CancelRequest + (*Request)(nil), // 38: provisioner.Request + (*Response)(nil), // 39: provisioner.Response + (*Agent_Metadata)(nil), // 40: provisioner.Agent.Metadata + nil, // 41: provisioner.Agent.EnvEntry + (*Resource_Metadata)(nil), // 42: provisioner.Resource.Metadata + nil, // 43: provisioner.ParseComplete.WorkspaceTagsEntry + (*timestamppb.Timestamp)(nil), // 44: google.protobuf.Timestamp } var file_provisionersdk_proto_provisioner_proto_depIdxs = []int32{ 7, // 0: provisioner.RichParameter.options:type_name -> provisioner.RichParameterOption - 0, // 1: provisioner.Log.level:type_name -> provisioner.LogLevel - 36, // 2: provisioner.Agent.env:type_name -> provisioner.Agent.EnvEntry - 19, // 3: provisioner.Agent.apps:type_name -> provisioner.App - 35, // 4: provisioner.Agent.metadata:type_name -> provisioner.Agent.Metadata - 16, // 5: provisioner.Agent.display_apps:type_name -> provisioner.DisplayApps - 18, // 6: provisioner.Agent.scripts:type_name -> provisioner.Script - 17, // 7: provisioner.Agent.extra_envs:type_name -> provisioner.Env - 20, // 8: provisioner.App.healthcheck:type_name -> provisioner.Healthcheck - 1, // 9: provisioner.App.sharing_level:type_name -> provisioner.AppSharingLevel - 2, // 10: provisioner.App.open_in:type_name -> provisioner.AppOpenIn - 15, // 11: provisioner.Resource.agents:type_name -> provisioner.Agent - 37, // 12: provisioner.Resource.metadata:type_name -> provisioner.Resource.Metadata - 3, // 13: provisioner.Metadata.workspace_transition:type_name -> provisioner.WorkspaceTransition - 6, // 14: provisioner.ParseComplete.template_variables:type_name -> provisioner.TemplateVariable - 38, // 15: provisioner.ParseComplete.workspace_tags:type_name -> provisioner.ParseComplete.WorkspaceTagsEntry - 23, // 16: provisioner.PlanRequest.metadata:type_name -> provisioner.Metadata - 9, // 17: provisioner.PlanRequest.rich_parameter_values:type_name -> provisioner.RichParameterValue - 10, // 18: provisioner.PlanRequest.variable_values:type_name -> provisioner.VariableValue - 14, // 19: provisioner.PlanRequest.external_auth_providers:type_name -> provisioner.ExternalAuthProvider - 21, // 20: provisioner.PlanComplete.resources:type_name -> provisioner.Resource - 8, // 21: provisioner.PlanComplete.parameters:type_name -> provisioner.RichParameter - 13, // 22: provisioner.PlanComplete.external_auth_providers:type_name -> provisioner.ExternalAuthProviderResource - 31, // 23: provisioner.PlanComplete.timings:type_name -> provisioner.Timing - 22, // 24: provisioner.PlanComplete.modules:type_name -> provisioner.Module - 23, // 25: provisioner.ApplyRequest.metadata:type_name -> provisioner.Metadata - 21, // 26: provisioner.ApplyComplete.resources:type_name -> provisioner.Resource - 8, // 27: provisioner.ApplyComplete.parameters:type_name -> provisioner.RichParameter - 13, // 28: provisioner.ApplyComplete.external_auth_providers:type_name -> provisioner.ExternalAuthProviderResource - 31, // 29: provisioner.ApplyComplete.timings:type_name -> provisioner.Timing - 39, // 30: provisioner.Timing.start:type_name -> google.protobuf.Timestamp - 39, // 31: provisioner.Timing.end:type_name -> google.protobuf.Timestamp - 4, // 32: provisioner.Timing.state:type_name -> provisioner.TimingState - 24, // 33: provisioner.Request.config:type_name -> provisioner.Config - 25, // 34: provisioner.Request.parse:type_name -> provisioner.ParseRequest - 27, // 35: provisioner.Request.plan:type_name -> provisioner.PlanRequest - 29, // 36: provisioner.Request.apply:type_name -> provisioner.ApplyRequest - 32, // 37: provisioner.Request.cancel:type_name -> provisioner.CancelRequest - 11, // 38: provisioner.Response.log:type_name -> provisioner.Log - 26, // 39: provisioner.Response.parse:type_name -> provisioner.ParseComplete - 28, // 40: provisioner.Response.plan:type_name -> provisioner.PlanComplete - 30, // 41: provisioner.Response.apply:type_name -> provisioner.ApplyComplete - 33, // 42: provisioner.Provisioner.Session:input_type -> provisioner.Request - 34, // 43: provisioner.Provisioner.Session:output_type -> provisioner.Response - 43, // [43:44] is the sub-list for method output_type - 42, // [42:43] is the sub-list for method input_type - 42, // [42:42] is the sub-list for extension type_name - 42, // [42:42] is the sub-list for extension extendee - 0, // [0:42] is the sub-list for field type_name + 11, // 1: provisioner.Preset.parameters:type_name -> provisioner.PresetParameter + 0, // 2: provisioner.Log.level:type_name -> provisioner.LogLevel + 41, // 3: provisioner.Agent.env:type_name -> provisioner.Agent.EnvEntry + 24, // 4: provisioner.Agent.apps:type_name -> provisioner.App + 40, // 5: provisioner.Agent.metadata:type_name -> provisioner.Agent.Metadata + 21, // 6: provisioner.Agent.display_apps:type_name -> provisioner.DisplayApps + 23, // 7: provisioner.Agent.scripts:type_name -> provisioner.Script + 22, // 8: provisioner.Agent.extra_envs:type_name -> provisioner.Env + 18, // 9: provisioner.Agent.resources_monitoring:type_name -> provisioner.ResourcesMonitoring + 19, // 10: provisioner.ResourcesMonitoring.memory:type_name -> provisioner.MemoryResourceMonitor + 20, // 11: provisioner.ResourcesMonitoring.volumes:type_name -> provisioner.VolumeResourceMonitor + 25, // 12: provisioner.App.healthcheck:type_name -> provisioner.Healthcheck + 1, // 13: provisioner.App.sharing_level:type_name -> provisioner.AppSharingLevel + 2, // 14: provisioner.App.open_in:type_name -> provisioner.AppOpenIn + 17, // 15: provisioner.Resource.agents:type_name -> provisioner.Agent + 42, // 16: provisioner.Resource.metadata:type_name -> provisioner.Resource.Metadata + 3, // 17: provisioner.Metadata.workspace_transition:type_name -> provisioner.WorkspaceTransition + 6, // 18: provisioner.ParseComplete.template_variables:type_name -> provisioner.TemplateVariable + 43, // 19: provisioner.ParseComplete.workspace_tags:type_name -> provisioner.ParseComplete.WorkspaceTagsEntry + 28, // 20: provisioner.PlanRequest.metadata:type_name -> provisioner.Metadata + 9, // 21: provisioner.PlanRequest.rich_parameter_values:type_name -> provisioner.RichParameterValue + 12, // 22: provisioner.PlanRequest.variable_values:type_name -> provisioner.VariableValue + 16, // 23: provisioner.PlanRequest.external_auth_providers:type_name -> provisioner.ExternalAuthProvider + 26, // 24: provisioner.PlanComplete.resources:type_name -> provisioner.Resource + 8, // 25: provisioner.PlanComplete.parameters:type_name -> provisioner.RichParameter + 15, // 26: provisioner.PlanComplete.external_auth_providers:type_name -> provisioner.ExternalAuthProviderResource + 36, // 27: provisioner.PlanComplete.timings:type_name -> provisioner.Timing + 27, // 28: provisioner.PlanComplete.modules:type_name -> provisioner.Module + 10, // 29: provisioner.PlanComplete.presets:type_name -> provisioner.Preset + 28, // 30: provisioner.ApplyRequest.metadata:type_name -> provisioner.Metadata + 26, // 31: provisioner.ApplyComplete.resources:type_name -> provisioner.Resource + 8, // 32: provisioner.ApplyComplete.parameters:type_name -> provisioner.RichParameter + 15, // 33: provisioner.ApplyComplete.external_auth_providers:type_name -> provisioner.ExternalAuthProviderResource + 36, // 34: provisioner.ApplyComplete.timings:type_name -> provisioner.Timing + 44, // 35: provisioner.Timing.start:type_name -> google.protobuf.Timestamp + 44, // 36: provisioner.Timing.end:type_name -> google.protobuf.Timestamp + 4, // 37: provisioner.Timing.state:type_name -> provisioner.TimingState + 29, // 38: provisioner.Request.config:type_name -> provisioner.Config + 30, // 39: provisioner.Request.parse:type_name -> provisioner.ParseRequest + 32, // 40: provisioner.Request.plan:type_name -> provisioner.PlanRequest + 34, // 41: provisioner.Request.apply:type_name -> provisioner.ApplyRequest + 37, // 42: provisioner.Request.cancel:type_name -> provisioner.CancelRequest + 13, // 43: provisioner.Response.log:type_name -> provisioner.Log + 31, // 44: provisioner.Response.parse:type_name -> provisioner.ParseComplete + 33, // 45: provisioner.Response.plan:type_name -> provisioner.PlanComplete + 35, // 46: provisioner.Response.apply:type_name -> provisioner.ApplyComplete + 38, // 47: provisioner.Provisioner.Session:input_type -> provisioner.Request + 39, // 48: provisioner.Provisioner.Session:output_type -> provisioner.Response + 48, // [48:49] is the sub-list for method output_type + 47, // [47:48] is the sub-list for method input_type + 47, // [47:47] is the sub-list for extension type_name + 47, // [47:47] is the sub-list for extension extendee + 0, // [0:47] is the sub-list for field type_name } func init() { file_provisionersdk_proto_provisioner_proto_init() } @@ -3639,7 +3988,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*VariableValue); i { + switch v := v.(*Preset); i { case 0: return &v.state case 1: @@ -3651,7 +4000,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Log); i { + switch v := v.(*PresetParameter); i { case 0: return &v.state case 1: @@ -3663,7 +4012,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*InstanceIdentityAuth); i { + switch v := v.(*VariableValue); i { case 0: return &v.state case 1: @@ -3675,7 +4024,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ExternalAuthProviderResource); i { + switch v := v.(*Log); i { case 0: return &v.state case 1: @@ -3687,7 +4036,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ExternalAuthProvider); i { + switch v := v.(*InstanceIdentityAuth); i { case 0: return &v.state case 1: @@ -3699,7 +4048,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Agent); i { + switch v := v.(*ExternalAuthProviderResource); i { case 0: return &v.state case 1: @@ -3711,7 +4060,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DisplayApps); i { + switch v := v.(*ExternalAuthProvider); i { case 0: return &v.state case 1: @@ -3723,7 +4072,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Env); i { + switch v := v.(*Agent); i { case 0: return &v.state case 1: @@ -3735,7 +4084,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Script); i { + switch v := v.(*ResourcesMonitoring); i { case 0: return &v.state case 1: @@ -3747,7 +4096,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*App); i { + switch v := v.(*MemoryResourceMonitor); i { case 0: return &v.state case 1: @@ -3759,7 +4108,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Healthcheck); i { + switch v := v.(*VolumeResourceMonitor); i { case 0: return &v.state case 1: @@ -3771,7 +4120,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Resource); i { + switch v := v.(*DisplayApps); i { case 0: return &v.state case 1: @@ -3783,7 +4132,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Module); i { + switch v := v.(*Env); i { case 0: return &v.state case 1: @@ -3795,7 +4144,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Metadata); i { + switch v := v.(*Script); i { case 0: return &v.state case 1: @@ -3807,7 +4156,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Config); i { + switch v := v.(*App); i { case 0: return &v.state case 1: @@ -3819,7 +4168,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ParseRequest); i { + switch v := v.(*Healthcheck); i { case 0: return &v.state case 1: @@ -3831,7 +4180,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ParseComplete); i { + switch v := v.(*Resource); i { case 0: return &v.state case 1: @@ -3843,7 +4192,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PlanRequest); i { + switch v := v.(*Module); i { case 0: return &v.state case 1: @@ -3855,7 +4204,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PlanComplete); i { + switch v := v.(*Metadata); i { case 0: return &v.state case 1: @@ -3867,7 +4216,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ApplyRequest); i { + switch v := v.(*Config); i { case 0: return &v.state case 1: @@ -3879,7 +4228,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ApplyComplete); i { + switch v := v.(*ParseRequest); i { case 0: return &v.state case 1: @@ -3891,7 +4240,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Timing); i { + switch v := v.(*ParseComplete); i { case 0: return &v.state case 1: @@ -3903,7 +4252,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CancelRequest); i { + switch v := v.(*PlanRequest); i { case 0: return &v.state case 1: @@ -3915,7 +4264,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Request); i { + switch v := v.(*PlanComplete); i { case 0: return &v.state case 1: @@ -3927,7 +4276,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Response); i { + switch v := v.(*ApplyRequest); i { case 0: return &v.state case 1: @@ -3939,7 +4288,19 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Agent_Metadata); i { + switch v := v.(*ApplyComplete); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_provisionersdk_proto_provisioner_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Timing); i { case 0: return &v.state case 1: @@ -3951,6 +4312,54 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CancelRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_provisionersdk_proto_provisioner_proto_msgTypes[33].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Request); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_provisionersdk_proto_provisioner_proto_msgTypes[34].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Response); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_provisionersdk_proto_provisioner_proto_msgTypes[35].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Agent_Metadata); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_provisionersdk_proto_provisioner_proto_msgTypes[37].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Resource_Metadata); i { case 0: return &v.state @@ -3964,18 +4373,18 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[3].OneofWrappers = []interface{}{} - file_provisionersdk_proto_provisioner_proto_msgTypes[10].OneofWrappers = []interface{}{ + file_provisionersdk_proto_provisioner_proto_msgTypes[12].OneofWrappers = []interface{}{ (*Agent_Token)(nil), (*Agent_InstanceId)(nil), } - file_provisionersdk_proto_provisioner_proto_msgTypes[28].OneofWrappers = []interface{}{ + file_provisionersdk_proto_provisioner_proto_msgTypes[33].OneofWrappers = []interface{}{ (*Request_Config)(nil), (*Request_Parse)(nil), (*Request_Plan)(nil), (*Request_Apply)(nil), (*Request_Cancel)(nil), } - file_provisionersdk_proto_provisioner_proto_msgTypes[29].OneofWrappers = []interface{}{ + file_provisionersdk_proto_provisioner_proto_msgTypes[34].OneofWrappers = []interface{}{ (*Response_Log)(nil), (*Response_Parse)(nil), (*Response_Plan)(nil), @@ -3987,7 +4396,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_provisionersdk_proto_provisioner_proto_rawDesc, NumEnums: 5, - NumMessages: 34, + NumMessages: 39, NumExtensions: 0, NumServices: 1, }, diff --git a/provisionersdk/proto/provisioner.proto b/provisionersdk/proto/provisioner.proto index 6e0c44800adcf..55d98e51fca7e 100644 --- a/provisionersdk/proto/provisioner.proto +++ b/provisionersdk/proto/provisioner.proto @@ -57,6 +57,17 @@ message RichParameterValue { string value = 2; } +// Preset represents a set of preset parameters for a template version. +message Preset { + string name = 1; + repeated PresetParameter parameters = 2; +} + +message PresetParameter { + string name = 1; + string value = 2; +} + // VariableValue holds the key/value mapping of a Terraform variable. message VariableValue { string name = 1; @@ -129,6 +140,7 @@ message Agent { repeated Script scripts = 21; repeated Env extra_envs = 22; int64 order = 23; + ResourcesMonitoring resources_monitoring = 24; } enum AppSharingLevel { @@ -137,6 +149,22 @@ enum AppSharingLevel { PUBLIC = 2; } +message ResourcesMonitoring { + MemoryResourceMonitor memory = 1; + repeated VolumeResourceMonitor volumes = 2; +} + +message MemoryResourceMonitor { + bool enabled = 1; + int32 threshold = 2; +} + +message VolumeResourceMonitor { + string path = 1; + bool enabled = 2; + int32 threshold = 3; +} + message DisplayApps { bool vscode = 1; bool vscode_insiders = 2; @@ -286,6 +314,7 @@ message PlanComplete { repeated ExternalAuthProviderResource external_auth_providers = 4; repeated Timing timings = 6; repeated Module modules = 7; + repeated Preset presets = 8; } // ApplyRequest asks the provisioner to apply the changes. Apply MUST be preceded by a successful plan request/response diff --git a/provisionersdk/serve.go b/provisionersdk/serve.go index baa3cc1412051..b91329d0665fe 100644 --- a/provisionersdk/serve.go +++ b/provisionersdk/serve.go @@ -25,9 +25,10 @@ type ServeOptions struct { // Listener serves multiple connections. Cannot be combined with Conn. Listener net.Listener // Conn is a single connection to serve. Cannot be combined with Listener. - Conn drpc.Transport - Logger slog.Logger - WorkDirectory string + Conn drpc.Transport + Logger slog.Logger + WorkDirectory string + ExternalProvisioner bool } type Server interface { diff --git a/pty/ptytest/ptytest.go b/pty/ptytest/ptytest.go index 1f0493dfa1a13..a871a0ddcafa0 100644 --- a/pty/ptytest/ptytest.go +++ b/pty/ptytest/ptytest.go @@ -252,6 +252,7 @@ func (e *outExpecter) Peek(ctx context.Context, n int) []byte { return slices.Clone(out) } +//nolint:govet // We don't care about conforming to ReadRune() (rune, int, error). func (e *outExpecter) ReadRune(ctx context.Context) rune { e.t.Helper() diff --git a/scratch/resourcepool-gcp-disk/main.tf b/scratch/resourcepool-gcp-disk/main.tf deleted file mode 100644 index 3b566e3221f4f..0000000000000 --- a/scratch/resourcepool-gcp-disk/main.tf +++ /dev/null @@ -1,42 +0,0 @@ -terraform { - required_providers { - coder = { - source = "coder/coder" - } - google = { - source = "hashicorp/google" - } - } -} - -locals { - name = "matifali" - project_id = "coder-dev-1" - zone = "asia-south1-a" -} - -provider "random" {} - -provider "google" { - zone = local.zone - project = local.project_id -} - -resource "random_string" "disk_name" { - length = 16 - special = false - upper = false - numeric = false -} - -resource "google_compute_disk" "example_disk" { - name = "${local.name}disk-${random_string.disk_name.result}" - type = "pd-standard" - size = 3 # Disk size in GB -} - -resource "coder_pool_resource_claimable" "prebuilt_disk" { - other { - instance_id = google_compute_disk.example_disk.id - } -} diff --git a/scripts/Dockerfile.base b/scripts/Dockerfile.base index 30ef6802ed716..f9d2bf6594b08 100644 --- a/scripts/Dockerfile.base +++ b/scripts/Dockerfile.base @@ -26,7 +26,7 @@ RUN apk add --no-cache \ # Terraform was disabled in the edge repo due to a build issue. # https://gitlab.alpinelinux.org/alpine/aports/-/commit/f3e263d94cfac02d594bef83790c280e045eba35 # Using wget for now. Note that busybox unzip doesn't support streaming. -RUN ARCH="$(arch)"; if [ "${ARCH}" == "x86_64" ]; then ARCH="amd64"; elif [ "${ARCH}" == "aarch64" ]; then ARCH="arm64"; fi; wget -O /tmp/terraform.zip "https://releases.hashicorp.com/terraform/1.9.8/terraform_1.9.8_linux_${ARCH}.zip" && \ +RUN ARCH="$(arch)"; if [ "${ARCH}" == "x86_64" ]; then ARCH="amd64"; elif [ "${ARCH}" == "aarch64" ]; then ARCH="arm64"; fi; wget -O /tmp/terraform.zip "https://releases.hashicorp.com/terraform/1.10.5/terraform_1.10.5_linux_${ARCH}.zip" && \ busybox unzip /tmp/terraform.zip -d /usr/local/bin && \ rm -f /tmp/terraform.zip && \ chmod +x /usr/local/bin/terraform && \ diff --git a/scripts/build_go.sh b/scripts/build_go.sh index 91fc3a1e4b3e3..3e23e15d8b962 100755 --- a/scripts/build_go.sh +++ b/scripts/build_go.sh @@ -36,17 +36,19 @@ source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" version="" os="${GOOS:-linux}" arch="${GOARCH:-amd64}" +output_path="" slim="${CODER_SLIM_BUILD:-0}" +agpl="${CODER_BUILD_AGPL:-0}" sign_darwin="${CODER_SIGN_DARWIN:-0}" sign_windows="${CODER_SIGN_WINDOWS:-0}" -bin_ident="com.coder.cli" -output_path="" -agpl="${CODER_BUILD_AGPL:-0}" boringcrypto=${CODER_BUILD_BORINGCRYPTO:-0} -debug=0 dylib=0 +windows_resources="${CODER_WINDOWS_RESOURCES:-0}" +debug=0 + +bin_ident="com.coder.cli" -args="$(getopt -o "" -l version:,os:,arch:,output:,slim,agpl,sign-darwin,boringcrypto,dylib,debug -- "$@")" +args="$(getopt -o "" -l version:,os:,arch:,output:,slim,agpl,sign-darwin,sign-windows,boringcrypto,dylib,windows-resources,debug -- "$@")" eval set -- "$args" while true; do case "$1" in @@ -79,6 +81,10 @@ while true; do sign_darwin=1 shift ;; + --sign-windows) + sign_windows=1 + shift + ;; --boringcrypto) boringcrypto=1 shift @@ -87,6 +93,10 @@ while true; do dylib=1 shift ;; + --windows-resources) + windows_resources=1 + shift + ;; --debug) debug=1 shift @@ -115,11 +125,13 @@ if [[ "$sign_darwin" == 1 ]]; then dependencies rcodesign requiredenvs AC_CERTIFICATE_FILE AC_CERTIFICATE_PASSWORD_FILE fi - if [[ "$sign_windows" == 1 ]]; then dependencies java requiredenvs JSIGN_PATH EV_KEYSTORE EV_KEY EV_CERTIFICATE_PATH EV_TSA_URL GCLOUD_ACCESS_TOKEN fi +if [[ "$windows_resources" == 1 ]]; then + dependencies go-winres +fi ldflags=( -X "'github.com/coder/coder/v2/buildinfo.tag=$version'" @@ -204,10 +216,100 @@ if [[ "$boringcrypto" == 1 ]]; then goexp="boringcrypto" fi +# On Windows, we use go-winres to embed the resources into the binary. +if [[ "$windows_resources" == 1 ]] && [[ "$os" == "windows" ]]; then + # Convert the version to a format that Windows understands. + # Remove any trailing data after a "+" or "-". + version_windows=$version + version_windows="${version_windows%+*}" + version_windows="${version_windows%-*}" + # If there wasn't any extra data, add a .0 to the version. Otherwise, add + # a .1 to the version to signify that this is not a release build so it can + # be distinguished from a release build. + non_release_build=0 + if [[ "$version_windows" == "$version" ]]; then + version_windows+=".0" + else + version_windows+=".1" + non_release_build=1 + fi + + if [[ ! "$version_windows" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-1]$ ]]; then + error "Computed invalid windows version format: $version_windows" + fi + + # File description changes based on slimness, AGPL status, and architecture. + file_description="Coder" + if [[ "$agpl" == 1 ]]; then + file_description+=" AGPL" + fi + if [[ "$slim" == 1 ]]; then + file_description+=" CLI" + fi + if [[ "$non_release_build" == 1 ]]; then + file_description+=" (development build)" + fi + + # Because this writes to a file with the OS and arch in the filename, we + # don't support concurrent builds for the same OS and arch (irregardless of + # slimness or AGPL status). + # + # This is fine since we only embed resources during dogfood and release + # builds, which use make (which will build all slim targets in parallel, + # then all non-slim targets in parallel). + expected_rsrc_file="./buildinfo/resources/resources_windows_${arch}.syso" + if [[ -f "$expected_rsrc_file" ]]; then + rm "$expected_rsrc_file" + fi + touch "$expected_rsrc_file" + + pushd ./buildinfo/resources + GOARCH="$arch" go-winres simply \ + --arch "$arch" \ + --out "resources" \ + --product-version "$version_windows" \ + --file-version "$version_windows" \ + --manifest "cli" \ + --file-description "$file_description" \ + --product-name "Coder" \ + --copyright "Copyright $(date +%Y) Coder Technologies Inc." \ + --original-filename "coder.exe" \ + --icon ../../scripts/win-installer/coder.ico + popd + + if [[ ! -f "$expected_rsrc_file" ]]; then + error "Failed to generate $expected_rsrc_file" + fi +fi + +set +e GOEXPERIMENT="$goexp" CGO_ENABLED="$cgo" GOOS="$os" GOARCH="$arch" GOARM="$arm_version" \ go build \ "${build_args[@]}" \ "$cmd_path" 1>&2 +exit_code=$? +set -e + +# Clean up the resources file if it was generated. +if [[ "$windows_resources" == 1 ]] && [[ "$os" == "windows" ]]; then + rm "$expected_rsrc_file" +fi + +if [[ "$exit_code" != 0 ]]; then + exit "$exit_code" +fi + +# If we did embed resources, verify that they were included. +if [[ "$windows_resources" == 1 ]] && [[ "$os" == "windows" ]]; then + winres_dir=$(mktemp -d) + if ! go-winres extract --dir "$winres_dir" "$output_path" 1>&2; then + rm -rf "$winres_dir" + error "Compiled binary does not contain embedded resources" + fi + # If go-winres didn't return an error, it means it did find embedded + # resources. + rm -rf "$winres_dir" +fi if [[ "$sign_darwin" == 1 ]] && [[ "$os" == "darwin" ]]; then execrelative ./sign_darwin.sh "$output_path" "$bin_ident" 1>&2 diff --git a/scripts/dev-oidc.sh b/scripts/dev-oidc.sh index 6a6d6e08ac705..cf5a7e3c6964c 100755 --- a/scripts/dev-oidc.sh +++ b/scripts/dev-oidc.sh @@ -49,6 +49,17 @@ cat </tmp/example-realm.json "baseUrl": "/coder", "redirectUris": ["*"], "secret": "coder" + }, + { + "clientId": "coder-public", + "publicClient": true, + "directAccessGrantsEnabled": true, + "enabled": true, + "fullScopeAllowed": true, + "baseUrl": "/coder", + "redirectUris": [ + "*" + ] } ] } @@ -79,6 +90,9 @@ hostname=$(hostname -f) export CODER_OIDC_ISSUER_URL="http://${hostname}:9080/realms/coder" export CODER_OIDC_CLIENT_ID=coder export CODER_OIDC_CLIENT_SECRET=coder +# Comment out the two lines above, and comment in the line below, +# to configure OIDC auth using a public client. +# export CODER_OIDC_CLIENT_ID=coder-public export CODER_DEV_ACCESS_URL="http://${hostname}:8080" exec "${SCRIPT_DIR}/develop.sh" "$@" diff --git a/scripts/release/check_commit_metadata.sh b/scripts/release/check_commit_metadata.sh index dff4cb1c738fc..f53de8e107430 100755 --- a/scripts/release/check_commit_metadata.sh +++ b/scripts/release/check_commit_metadata.sh @@ -143,7 +143,12 @@ main() { for commit in "${renamed_cherry_pick_commits_pending[@]}"; do log "Checking if pending commit ${commit} has a corresponding cherry-pick..." if [[ ! -v renamed_cherry_pick_commits[${commit}] ]]; then - error "Invariant failed, cherry-picked commit ${commit} has no corresponding original commit" + if [[ ${CODER_IGNORE_MISSING_COMMIT_METADATA:-0} == 1 ]]; then + log "WARNING: Missing original commit for cherry-picked commit ${commit}, but continuing due to CODER_IGNORE_MISSING_COMMIT_METADATA being set." + continue + else + error "Invariant failed, cherry-picked commit ${commit} has no corresponding original commit" + fi fi log "Found matching cherry-pick commit ${commit} -> ${renamed_cherry_pick_commits[${commit}]}" done diff --git a/scripts/testidp/main.go b/scripts/testidp/main.go index e1b7a17f347e2..52b10ab94e975 100644 --- a/scripts/testidp/main.go +++ b/scripts/testidp/main.go @@ -11,6 +11,7 @@ import ( "time" "github.com/golang-jwt/jwt/v4" + "github.com/google/uuid" "github.com/stretchr/testify/require" "cdr.dev/slog" @@ -88,6 +89,7 @@ func RunIDP() func(t *testing.T) { // This is a static set of auth fields. Might be beneficial to make flags // to allow different values here. This is only required for using the // testIDP as primary auth. External auth does not ever fetch these fields. + "sub": uuid.MustParse("26c6a19c-b9b8-493b-a991-88a4c3310314"), "email": "oidc_member@coder.com", "preferred_username": "oidc_member", "email_verified": true, diff --git a/site/e2e/constants.ts b/site/e2e/constants.ts index 4ec0048e691cb..4fcada0e6d15b 100644 --- a/site/e2e/constants.ts +++ b/site/e2e/constants.ts @@ -24,16 +24,22 @@ export const users = { password: defaultPassword, email: "admin@coder.com", }, + templateAdmin: { + username: "template-admin", + password: defaultPassword, + email: "templateadmin@coder.com", + roles: ["Template Admin"], + }, auditor: { username: "auditor", password: defaultPassword, email: "auditor@coder.com", roles: ["Template Admin", "Auditor"], }, - user: { - username: "user", + member: { + username: "member", password: defaultPassword, - email: "user@coder.com", + email: "member@coder.com", }, } satisfies Record< string, diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index 4bb1010f311e6..5692909355fca 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -150,7 +150,6 @@ export const createWorkspace = async ( await page.getByRole("button", { name: /create workspace/i }).click(); const user = currentUser(page); - await expectUrl(page).toHavePathName(`/@${user.username}/${name}`); await page.waitForSelector("[data-testid='build-status'] >> text=Running", { @@ -165,12 +164,10 @@ export const verifyParameters = async ( richParameters: RichParameter[], expectedBuildParameters: WorkspaceBuildParameter[], ) => { - await page.goto(`/@admin/${workspaceName}/settings/parameters`, { + const user = currentUser(page); + await page.goto(`/@${user.username}/${workspaceName}/settings/parameters`, { waitUntil: "domcontentloaded", }); - await expectUrl(page).toHavePathName( - `/@admin/${workspaceName}/settings/parameters`, - ); for (const buildParameter of expectedBuildParameters) { const richParameter = richParameters.find( @@ -292,16 +289,22 @@ export const createTemplate = async ( * createGroup navigates to the /groups/create page and creates a group with a * random name. */ -export const createGroup = async (page: Page): Promise => { - await page.goto("/deployment/groups/create", { +export const createGroup = async ( + page: Page, + organization?: string, +): Promise => { + const prefix = organization + ? `/organizations/${organization}` + : "/deployment"; + await page.goto(`${prefix}/groups/create`, { waitUntil: "domcontentloaded", }); - await expectUrl(page).toHavePathName("/deployment/groups/create"); + await expectUrl(page).toHavePathName(`${prefix}/groups/create`); const name = randomName(); await page.getByLabel("Name", { exact: true }).fill(name); await page.getByRole("button", { name: /save/i }).click(); - await expectUrl(page).toHavePathName(`/deployment/groups/${name}`); + await expectUrl(page).toHavePathName(`${prefix}/groups/${name}`); return name; }; @@ -350,10 +353,10 @@ export const sshIntoWorkspace = async ( }; export const stopWorkspace = async (page: Page, workspaceName: string) => { - await page.goto(`/@admin/${workspaceName}`, { + const user = currentUser(page); + await page.goto(`/@${user.username}/${workspaceName}`, { waitUntil: "domcontentloaded", }); - await expectUrl(page).toHavePathName(`/@admin/${workspaceName}`); await page.getByTestId("workspace-stop-button").click(); @@ -369,10 +372,10 @@ export const buildWorkspaceWithParameters = async ( buildParameters: WorkspaceBuildParameter[] = [], confirm = false, ) => { - await page.goto(`/@admin/${workspaceName}`, { + const user = currentUser(page); + await page.goto(`/@${user.username}/${workspaceName}`, { waitUntil: "domcontentloaded", }); - await expectUrl(page).toHavePathName(`/@admin/${workspaceName}`); await page.getByTestId("build-parameters-button").click(); @@ -573,6 +576,7 @@ const createTemplateVersionTar = async ( parameters: response.apply?.parameters ?? [], externalAuthProviders: response.apply?.externalAuthProviders ?? [], timings: response.apply?.timings ?? [], + presets: [], }, }; }); @@ -693,6 +697,7 @@ const createTemplateVersionTar = async ( externalAuthProviders: [], timings: [], modules: [], + presets: [], ...response.plan, } as PlanComplete; response.plan.resources = response.plan.resources?.map(fillResource); @@ -756,7 +761,7 @@ export const createServer = async ( async function waitForPort( port: number, host = "0.0.0.0", - timeout = 30000, + timeout = 60_000, ): Promise { const start = Date.now(); while (Date.now() - start < timeout) { @@ -985,10 +990,10 @@ export const updateWorkspace = async ( richParameters: RichParameter[] = [], buildParameters: WorkspaceBuildParameter[] = [], ) => { - await page.goto(`/@admin/${workspaceName}`, { + const user = currentUser(page); + await page.goto(`/@${user.username}/${workspaceName}`, { waitUntil: "domcontentloaded", }); - await expectUrl(page).toHavePathName(`/@admin/${workspaceName}`); await page.getByTestId("workspace-update-button").click(); await page.getByTestId("confirm-button").click(); @@ -1007,12 +1012,10 @@ export const updateWorkspaceParameters = async ( richParameters: RichParameter[] = [], buildParameters: WorkspaceBuildParameter[] = [], ) => { - await page.goto(`/@admin/${workspaceName}/settings/parameters`, { + const user = currentUser(page); + await page.goto(`/@${user.username}/${workspaceName}/settings/parameters`, { waitUntil: "domcontentloaded", }); - await expectUrl(page).toHavePathName( - `/@admin/${workspaceName}/settings/parameters`, - ); await fillParameters(page, richParameters, buildParameters); await page.getByRole("button", { name: /submit and restart/i }).click(); @@ -1036,11 +1039,14 @@ export async function openTerminalWindow( // Specify that the shell should be `bash`, to prevent inheriting a shell that // isn't POSIX compatible, such as Fish. + const user = currentUser(page); const commandQuery = `?command=${encodeURIComponent("/usr/bin/env bash")}`; await expectUrl(terminal).toHavePathName( - `/@admin/${workspaceName}.${agentName}/terminal`, + `/@${user.username}/${workspaceName}.${agentName}/terminal`, + ); + await terminal.goto( + `/@${user.username}/${workspaceName}.${agentName}/terminal${commandQuery}`, ); - await terminal.goto(`/@admin/${workspaceName}.dev/terminal${commandQuery}`); return terminal; } @@ -1092,7 +1098,7 @@ export async function createUser( // Give them a role await addedRow.getByLabel("Edit user roles").click(); for (const role of roles) { - await page.getByText(role, { exact: true }).click(); + await page.getByRole("group").getByText(role, { exact: true }).click(); } await page.mouse.click(10, 10); // close the popover by clicking outside of it diff --git a/site/e2e/provisionerGenerated.ts b/site/e2e/provisionerGenerated.ts index a56143f367c4a..6943c54a30dae 100644 --- a/site/e2e/provisionerGenerated.ts +++ b/site/e2e/provisionerGenerated.ts @@ -94,6 +94,17 @@ export interface RichParameterValue { value: string; } +/** Preset represents a set of preset parameters for a template version. */ +export interface Preset { + name: string; + parameters: PresetParameter[]; +} + +export interface PresetParameter { + name: string; + value: string; +} + /** VariableValue holds the key/value mapping of a Terraform variable. */ export interface VariableValue { name: string; @@ -146,6 +157,7 @@ export interface Agent { scripts: Script[]; extraEnvs: Env[]; order: number; + resourcesMonitoring: ResourcesMonitoring | undefined; } export interface Agent_Metadata { @@ -162,6 +174,22 @@ export interface Agent_EnvEntry { value: string; } +export interface ResourcesMonitoring { + memory: MemoryResourceMonitor | undefined; + volumes: VolumeResourceMonitor[]; +} + +export interface MemoryResourceMonitor { + enabled: boolean; + threshold: number; +} + +export interface VolumeResourceMonitor { + path: string; + enabled: boolean; + threshold: number; +} + export interface DisplayApps { vscode: boolean; vscodeInsiders: boolean; @@ -305,6 +333,7 @@ export interface PlanComplete { externalAuthProviders: ExternalAuthProviderResource[]; timings: Timing[]; modules: Module[]; + presets: Preset[]; } /** @@ -468,6 +497,30 @@ export const RichParameterValue = { }, }; +export const Preset = { + encode(message: Preset, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.name !== "") { + writer.uint32(10).string(message.name); + } + for (const v of message.parameters) { + PresetParameter.encode(v!, writer.uint32(18).fork()).ldelim(); + } + return writer; + }, +}; + +export const PresetParameter = { + encode(message: PresetParameter, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.name !== "") { + writer.uint32(10).string(message.name); + } + if (message.value !== "") { + writer.uint32(18).string(message.value); + } + return writer; + }, +}; + export const VariableValue = { encode(message: VariableValue, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { if (message.name !== "") { @@ -581,6 +634,9 @@ export const Agent = { if (message.order !== 0) { writer.uint32(184).int64(message.order); } + if (message.resourcesMonitoring !== undefined) { + ResourcesMonitoring.encode(message.resourcesMonitoring, writer.uint32(194).fork()).ldelim(); + } return writer; }, }; @@ -621,6 +677,45 @@ export const Agent_EnvEntry = { }, }; +export const ResourcesMonitoring = { + encode(message: ResourcesMonitoring, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.memory !== undefined) { + MemoryResourceMonitor.encode(message.memory, writer.uint32(10).fork()).ldelim(); + } + for (const v of message.volumes) { + VolumeResourceMonitor.encode(v!, writer.uint32(18).fork()).ldelim(); + } + return writer; + }, +}; + +export const MemoryResourceMonitor = { + encode(message: MemoryResourceMonitor, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.enabled === true) { + writer.uint32(8).bool(message.enabled); + } + if (message.threshold !== 0) { + writer.uint32(16).int32(message.threshold); + } + return writer; + }, +}; + +export const VolumeResourceMonitor = { + encode(message: VolumeResourceMonitor, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.path !== "") { + writer.uint32(10).string(message.path); + } + if (message.enabled === true) { + writer.uint32(16).bool(message.enabled); + } + if (message.threshold !== 0) { + writer.uint32(24).int32(message.threshold); + } + return writer; + }, +}; + export const DisplayApps = { encode(message: DisplayApps, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { if (message.vscode === true) { @@ -959,6 +1054,9 @@ export const PlanComplete = { for (const v of message.modules) { Module.encode(v!, writer.uint32(58).fork()).ldelim(); } + for (const v of message.presets) { + Preset.encode(v!, writer.uint32(66).fork()).ldelim(); + } return writer; }, }; diff --git a/site/e2e/setup/addUsersAndLicense.spec.ts b/site/e2e/setup/addUsersAndLicense.spec.ts index f6817e0fd423d..784db4812aaa1 100644 --- a/site/e2e/setup/addUsersAndLicense.spec.ts +++ b/site/e2e/setup/addUsersAndLicense.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from "@playwright/test"; import { API } from "api/api"; -import { Language } from "pages/CreateUserPage/CreateUserForm"; +import { Language } from "pages/CreateUserPage/Language"; import { coderPort, license, premiumTestsRequired, users } from "../constants"; import { expectUrl } from "../expectUrl"; import { createUser } from "../helpers"; @@ -16,7 +16,6 @@ test("setup deployment", async ({ page }) => { } // Setup first user - await page.getByLabel(Language.usernameLabel).fill(users.admin.username); await page.getByLabel(Language.emailLabel).fill(users.admin.email); await page.getByLabel(Language.passwordLabel).fill(users.admin.password); await page.getByTestId("create").click(); diff --git a/site/e2e/tests/deployment/idpOrgSync.spec.ts b/site/e2e/tests/deployment/idpOrgSync.spec.ts index c8e6b5a33b17f..d77ddb1593fd3 100644 --- a/site/e2e/tests/deployment/idpOrgSync.spec.ts +++ b/site/e2e/tests/deployment/idpOrgSync.spec.ts @@ -16,6 +16,8 @@ test.beforeEach(async ({ page }) => { }); test.describe("IdpOrgSyncPage", () => { + test.describe.configure({ retries: 1 }); + test("show empty table when no org mappings are present", async ({ page, }) => { @@ -148,19 +150,32 @@ test.describe("IdpOrgSyncPage", () => { waitUntil: "domcontentloaded", }); + const syncField = page.getByRole("textbox", { + name: "Organization sync field", + }); + await syncField.fill(""); + const idpOrgInput = page.getByLabel("IdP organization name"); - const orgSelector = page.getByPlaceholder("Select organization"); const addButton = page.getByRole("button", { name: /Add IdP organization/i, }); await expect(addButton).toBeDisabled(); - await idpOrgInput.fill("new-idp-org"); + const idpOrgName = randomName(); + await idpOrgInput.fill(idpOrgName); // Select Coder organization from combobox + const orgSelector = page.getByPlaceholder("Select organization"); + await expect(orgSelector).toBeAttached(); + await expect(orgSelector).toBeVisible(); await orgSelector.click(); - await page.getByRole("option", { name: orgName }).click(); + await page.waitForTimeout(1000); + + const option = await page.getByRole("option", { name: orgName }); + await expect(option).toBeAttached({ timeout: 30000 }); + await expect(option).toBeVisible(); + await option.click(); // Add button should now be enabled await expect(addButton).toBeEnabled(); @@ -168,11 +183,9 @@ test.describe("IdpOrgSyncPage", () => { await addButton.click(); // Verify new mapping appears in table - const newRow = page.getByTestId("idp-org-new-idp-org"); + const newRow = page.getByTestId(`idp-org-${idpOrgName}`); await expect(newRow).toBeVisible(); - await expect( - newRow.getByRole("cell", { name: "new-idp-org" }), - ).toBeVisible(); + await expect(newRow.getByRole("cell", { name: idpOrgName })).toBeVisible(); await expect(newRow.getByRole("cell", { name: orgName })).toBeVisible(); await expect( diff --git a/site/e2e/tests/groups/addMembers.spec.ts b/site/e2e/tests/groups/addMembers.spec.ts index 5b70e8910dc55..7f29f4a536385 100644 --- a/site/e2e/tests/groups/addMembers.spec.ts +++ b/site/e2e/tests/groups/addMembers.spec.ts @@ -5,6 +5,7 @@ import { getCurrentOrgId, setupApiCalls, } from "../../api"; +import { defaultOrganizationName } from "../../constants"; import { requiresLicense } from "../../helpers"; import { login } from "../../helpers"; import { beforeCoderTest } from "../../hooks"; @@ -18,6 +19,7 @@ test.beforeEach(async ({ page }) => { test("add members", async ({ page, baseURL }) => { requiresLicense(); + const orgName = defaultOrganizationName; const orgId = await getCurrentOrgId(); const group = await createGroup(orgId); const numberOfMembers = 3; @@ -25,7 +27,7 @@ test("add members", async ({ page, baseURL }) => { Array.from({ length: numberOfMembers }, () => createUser(orgId)), ); - await page.goto(`${baseURL}/groups/${group.name}`, { + await page.goto(`${baseURL}/organizations/${orgName}/groups/${group.name}`, { waitUntil: "domcontentloaded", }); await expect(page).toHaveTitle(`${group.display_name} - Coder`); diff --git a/site/e2e/tests/groups/addUsersToDefaultGroup.spec.ts b/site/e2e/tests/groups/addUsersToDefaultGroup.spec.ts index 049049265d5ae..b1ece8705e2c6 100644 --- a/site/e2e/tests/groups/addUsersToDefaultGroup.spec.ts +++ b/site/e2e/tests/groups/addUsersToDefaultGroup.spec.ts @@ -1,5 +1,6 @@ import { expect, test } from "@playwright/test"; import { createUser, getCurrentOrgId, setupApiCalls } from "../../api"; +import { defaultOrganizationName } from "../../constants"; import { requiresLicense } from "../../helpers"; import { login } from "../../helpers"; import { beforeCoderTest } from "../../hooks"; @@ -17,16 +18,20 @@ test(`Every user should be automatically added to the default '${DEFAULT_GROUP_N }) => { requiresLicense(); await setupApiCalls(page); + + const orgName = defaultOrganizationName; const orgId = await getCurrentOrgId(); const numberOfMembers = 3; const users = await Promise.all( Array.from({ length: numberOfMembers }, () => createUser(orgId)), ); - await page.goto(`${baseURL}/groups`, { waitUntil: "domcontentloaded" }); + await page.goto(`${baseURL}/organizations/${orgName}/groups`, { + waitUntil: "domcontentloaded", + }); await expect(page).toHaveTitle("Groups - Coder"); - const groupRow = page.getByRole("row", { name: DEFAULT_GROUP_NAME }); + const groupRow = page.getByText(DEFAULT_GROUP_NAME); await groupRow.click(); await expect(page).toHaveTitle(`${DEFAULT_GROUP_NAME} - Coder`); diff --git a/site/e2e/tests/groups/createGroup.spec.ts b/site/e2e/tests/groups/createGroup.spec.ts index 3ae7bbe2a317e..8df1cdbdcc9fb 100644 --- a/site/e2e/tests/groups/createGroup.spec.ts +++ b/site/e2e/tests/groups/createGroup.spec.ts @@ -1,4 +1,5 @@ import { expect, test } from "@playwright/test"; +import { defaultOrganizationName } from "../../constants"; import { randomName, requiresLicense } from "../../helpers"; import { login } from "../../helpers"; import { beforeCoderTest } from "../../hooks"; @@ -11,7 +12,11 @@ test.beforeEach(async ({ page }) => { test("create group", async ({ page, baseURL }) => { requiresLicense(); - await page.goto(`${baseURL}/groups`, { waitUntil: "domcontentloaded" }); + const orgName = defaultOrganizationName; + + await page.goto(`${baseURL}/organizations/${orgName}/groups`, { + waitUntil: "domcontentloaded", + }); await expect(page).toHaveTitle("Groups - Coder"); await page.getByText("Create group").click(); diff --git a/site/e2e/tests/groups/removeGroup.spec.ts b/site/e2e/tests/groups/removeGroup.spec.ts index 06d13fd0dfccf..736b86f7d386d 100644 --- a/site/e2e/tests/groups/removeGroup.spec.ts +++ b/site/e2e/tests/groups/removeGroup.spec.ts @@ -1,5 +1,6 @@ import { expect, test } from "@playwright/test"; import { createGroup, getCurrentOrgId, setupApiCalls } from "../../api"; +import { defaultOrganizationName } from "../../constants"; import { requiresLicense } from "../../helpers"; import { login } from "../../helpers"; import { beforeCoderTest } from "../../hooks"; @@ -13,10 +14,11 @@ test.beforeEach(async ({ page }) => { test("remove group", async ({ page, baseURL }) => { requiresLicense(); + const orgName = defaultOrganizationName; const orgId = await getCurrentOrgId(); const group = await createGroup(orgId); - await page.goto(`${baseURL}/groups/${group.name}`, { + await page.goto(`${baseURL}/organizations/${orgName}/groups/${group.name}`, { waitUntil: "domcontentloaded", }); await expect(page).toHaveTitle(`${group.display_name} - Coder`); diff --git a/site/e2e/tests/groups/removeMember.spec.ts b/site/e2e/tests/groups/removeMember.spec.ts index 3b5727cc42dba..81fb5ee4f4117 100644 --- a/site/e2e/tests/groups/removeMember.spec.ts +++ b/site/e2e/tests/groups/removeMember.spec.ts @@ -6,6 +6,7 @@ import { getCurrentOrgId, setupApiCalls, } from "../../api"; +import { defaultOrganizationName } from "../../constants"; import { requiresLicense } from "../../helpers"; import { login } from "../../helpers"; import { beforeCoderTest } from "../../hooks"; @@ -19,6 +20,7 @@ test.beforeEach(async ({ page }) => { test("remove member", async ({ page, baseURL }) => { requiresLicense(); + const orgName = defaultOrganizationName; const orgId = await getCurrentOrgId(); const [group, member] = await Promise.all([ createGroup(orgId), @@ -26,7 +28,7 @@ test("remove member", async ({ page, baseURL }) => { ]); await API.addMember(group.id, member.id); - await page.goto(`${baseURL}/groups/${group.name}`, { + await page.goto(`${baseURL}/organizations/${orgName}/groups/${group.name}`, { waitUntil: "domcontentloaded", }); await expect(page).toHaveTitle(`${group.display_name} - Coder`); diff --git a/site/e2e/tests/organizationGroups.spec.ts b/site/e2e/tests/organizationGroups.spec.ts index 2d0a41acafc02..dff12ab91c453 100644 --- a/site/e2e/tests/organizationGroups.spec.ts +++ b/site/e2e/tests/organizationGroups.spec.ts @@ -5,6 +5,7 @@ import { createUser, setupApiCalls, } from "../api"; +import { defaultOrganizationName } from "../constants"; import { expectUrl } from "../expectUrl"; import { login, randomName, requiresLicense } from "../helpers"; import { beforeCoderTest } from "../hooks"; @@ -15,6 +16,17 @@ test.beforeEach(async ({ page }) => { await setupApiCalls(page); }); +test("redirects", async ({ page }) => { + requiresLicense(); + + const orgName = defaultOrganizationName; + await page.goto("/groups"); + await expectUrl(page).toHavePathName(`/organizations/${orgName}/groups`); + + await page.goto("/deployment/groups"); + await expectUrl(page).toHavePathName(`/organizations/${orgName}/groups`); +}); + test("create group", async ({ page }) => { requiresLicense(); @@ -24,7 +36,7 @@ test("create group", async ({ page }) => { // Navigate to groups page await page.getByRole("link", { name: "Groups" }).click(); - await expect(page).toHaveTitle(`Groups - Org ${org.name} - Coder`); + await expect(page).toHaveTitle("Groups - Coder"); // Create a new group await page.getByText("Create group").click(); @@ -72,7 +84,7 @@ test("create group", async ({ page }) => { await expect(page.getByText("Group deleted successfully.")).toBeVisible(); await expectUrl(page).toHavePathName(`/organizations/${org.name}/groups`); - await expect(page).toHaveTitle(`Groups - Org ${org.name} - Coder`); + await expect(page).toHaveTitle("Groups - Coder"); }); test("change quota settings", async ({ page }) => { diff --git a/site/e2e/tests/organizations.spec.ts b/site/e2e/tests/organizations.spec.ts index 5a1cf4ba82c0c..ff4f5ad993f19 100644 --- a/site/e2e/tests/organizations.spec.ts +++ b/site/e2e/tests/organizations.spec.ts @@ -52,5 +52,6 @@ test("create and delete organization", async ({ page }) => { const dialog = page.getByTestId("dialog"); await dialog.getByLabel("Name").fill(newName); await dialog.getByRole("button", { name: "Delete" }).click(); - await expect(page.getByText("Organization deleted.")).toBeVisible(); + await page.waitForTimeout(1000); + await expect(page.getByText("Organization deleted")).toBeVisible(); }); diff --git a/site/e2e/tests/organizations/idpGroupSync.spec.ts b/site/e2e/tests/organizations/idpGroupSync.spec.ts index 2ea9d02388b72..a6128253346b7 100644 --- a/site/e2e/tests/organizations/idpGroupSync.spec.ts +++ b/site/e2e/tests/organizations/idpGroupSync.spec.ts @@ -16,6 +16,8 @@ test.beforeEach(async ({ page }) => { }); test.describe("IdpGroupSyncPage", () => { + test.describe.configure({ retries: 1 }); + test("show empty table when no group mappings are present", async ({ page, }) => { @@ -149,7 +151,6 @@ test.describe("IdpGroupSyncPage", () => { }); const idpOrgInput = page.getByLabel("IdP group name"); - const orgSelector = page.getByPlaceholder("Select group"); const addButton = page.getByRole("button", { name: /Add IdP group/i, }); @@ -159,8 +160,16 @@ test.describe("IdpGroupSyncPage", () => { await idpOrgInput.fill("new-idp-group"); // Select Coder organization from combobox - await orgSelector.click(); - await page.getByRole("option", { name: /Everyone/i }).click(); + const groupSelector = page.getByPlaceholder("Select group"); + await expect(groupSelector).toBeAttached(); + await expect(groupSelector).toBeVisible(); + await groupSelector.click(); + await page.waitForTimeout(1000); + + const option = await page.getByRole("option", { name: /Everyone/i }); + await expect(option).toBeAttached({ timeout: 30000 }); + await expect(option).toBeVisible(); + await option.click(); // Add button should now be enabled await expect(addButton).toBeEnabled(); diff --git a/site/e2e/tests/organizations/idpRoleSync.spec.ts b/site/e2e/tests/organizations/idpRoleSync.spec.ts index 3374151a85b56..a889591026dd9 100644 --- a/site/e2e/tests/organizations/idpRoleSync.spec.ts +++ b/site/e2e/tests/organizations/idpRoleSync.spec.ts @@ -10,16 +10,18 @@ import { login } from "../../helpers"; import { beforeCoderTest } from "../../hooks"; test.beforeEach(async ({ page }) => { + requiresLicense(); beforeCoderTest(page); await login(page); await setupApiCalls(page); }); test.describe("IdpRoleSyncPage", () => { + test.describe.configure({ retries: 1 }); + test("show empty table when no role mappings are present", async ({ page, }) => { - requiresLicense(); const org = await createOrganizationWithName(randomName()); await page.goto(`/organizations/${org.name}/idp-sync?tab=roles`, { waitUntil: "domcontentloaded", @@ -36,7 +38,6 @@ test.describe("IdpRoleSyncPage", () => { }); test("add new IdP role mapping with API", async ({ page }) => { - requiresLicense(); const org = await createOrganizationWithName(randomName()); await createRoleSyncSettings(org.id); @@ -58,7 +59,6 @@ test.describe("IdpRoleSyncPage", () => { }); test("delete a IdP role to coder role mapping row", async ({ page }) => { - requiresLicense(); const org = await createOrganizationWithName(randomName()); await createRoleSyncSettings(org.id); @@ -79,7 +79,6 @@ test.describe("IdpRoleSyncPage", () => { }); test("update sync field", async ({ page }) => { - requiresLicense(); const org = await createOrganizationWithName(randomName()); await page.goto(`/organizations/${org.name}/idp-sync?tab=roles`, { waitUntil: "domcontentloaded", @@ -107,7 +106,6 @@ test.describe("IdpRoleSyncPage", () => { test("export policy button is enabled when sync settings are present", async ({ page, }) => { - requiresLicense(); const org = await createOrganizationWithName(randomName()); await page.goto(`/organizations/${org.name}/idp-sync?tab=roles`, { waitUntil: "domcontentloaded", @@ -121,7 +119,6 @@ test.describe("IdpRoleSyncPage", () => { }); test("add new IdP role mapping with UI", async ({ page }) => { - requiresLicense(); const orgName = randomName(); await createOrganizationWithName(orgName); @@ -130,18 +127,31 @@ test.describe("IdpRoleSyncPage", () => { }); const idpOrgInput = page.getByLabel("IdP role name"); - const roleSelector = page.getByPlaceholder("Select role"); const addButton = page.getByRole("button", { name: /Add IdP role/i, }); await expect(addButton).toBeDisabled(); - await idpOrgInput.fill("new-idp-role"); + const idpRoleName = randomName(); + await idpOrgInput.fill(idpRoleName); // Select Coder role from combobox + const roleSelector = page.getByPlaceholder("Select role"); + await expect(roleSelector).toBeAttached(); + await expect(roleSelector).toBeVisible(); await roleSelector.click(); - await page.getByRole("option", { name: /Organization Admin/i }).click(); + + await page.getByRole("combobox").click(); + await page.waitForTimeout(1000); + + const option = await page.getByRole("option", { + name: /Organization Admin/i, + }); + + await expect(option).toBeAttached({ timeout: 30000 }); + await expect(option).toBeVisible(); + await option.click(); // Add button should now be enabled await expect(addButton).toBeEnabled(); @@ -149,11 +159,9 @@ test.describe("IdpRoleSyncPage", () => { await addButton.click(); // Verify new mapping appears in table - const newRow = page.getByTestId("role-new-idp-role"); + const newRow = page.getByTestId(`role-${idpRoleName}`); await expect(newRow).toBeVisible(); - await expect( - newRow.getByRole("cell", { name: "new-idp-role" }), - ).toBeVisible(); + await expect(newRow.getByRole("cell", { name: idpRoleName })).toBeVisible(); await expect( newRow.getByRole("cell", { name: "organization-admin" }), ).toBeVisible(); diff --git a/site/e2e/tests/updateTemplate.spec.ts b/site/e2e/tests/updateTemplate.spec.ts index b8f1192b461b5..33e85e40e3b6d 100644 --- a/site/e2e/tests/updateTemplate.spec.ts +++ b/site/e2e/tests/updateTemplate.spec.ts @@ -31,7 +31,7 @@ test("add and remove a group", async ({ page }) => { const orgName = defaultOrganizationName; const templateName = await createTemplate(page); - const groupName = await createGroup(page); + const groupName = await createGroup(page, orgName); await page.goto( `/templates/${orgName}/${templateName}/settings/permissions`, diff --git a/site/e2e/tests/workspaces/autoCreateWorkspace.spec.ts b/site/e2e/tests/workspaces/autoCreateWorkspace.spec.ts index 4bf9b26bb205e..a6ec00958ad78 100644 --- a/site/e2e/tests/workspaces/autoCreateWorkspace.spec.ts +++ b/site/e2e/tests/workspaces/autoCreateWorkspace.spec.ts @@ -16,7 +16,7 @@ let template!: string; test.beforeAll(async ({ browser }) => { const page = await (await browser.newContext()).newPage(); - await login(page); + await login(page, users.templateAdmin); const richParameters: RichParameter[] = [ { ...emptyParameter, name: "repo", type: "string" }, @@ -29,7 +29,7 @@ test.beforeAll(async ({ browser }) => { test.beforeEach(async ({ page }) => { beforeCoderTest(page); - await login(page, users.user); + await login(page, users.member); }); test("create workspace in auto mode", async ({ page }) => { @@ -40,7 +40,7 @@ test("create workspace in auto mode", async ({ page }) => { waitUntil: "domcontentloaded", }, ); - await expect(page).toHaveTitle(`${users.user.username}/${name} - Coder`); + await expect(page).toHaveTitle(`${users.member.username}/${name} - Coder`); }); test("use an existing workspace that matches the `match` parameter instead of creating a new one", async ({ @@ -54,7 +54,7 @@ test("use an existing workspace that matches the `match` parameter instead of cr }, ); await expect(page).toHaveTitle( - `${users.user.username}/${prevWorkspace} - Coder`, + `${users.member.username}/${prevWorkspace} - Coder`, ); }); diff --git a/site/e2e/tests/workspaces/createWorkspace.spec.ts b/site/e2e/tests/workspaces/createWorkspace.spec.ts index ce1898a31049a..49b832d285e0b 100644 --- a/site/e2e/tests/workspaces/createWorkspace.spec.ts +++ b/site/e2e/tests/workspaces/createWorkspace.spec.ts @@ -1,4 +1,5 @@ import { expect, test } from "@playwright/test"; +import { users } from "../../constants"; import { StarterTemplates, createTemplate, @@ -26,27 +27,20 @@ test.describe.configure({ mode: "parallel" }); test.beforeEach(async ({ page }) => { beforeCoderTest(page); - await login(page); }); test("create workspace", async ({ page }) => { + await login(page, users.templateAdmin); const template = await createTemplate(page, { - apply: [ - { - apply: { - resources: [ - { - name: "example", - }, - ], - }, - }, - ], + apply: [{ apply: { resources: [{ name: "example" }] } }], }); + + await login(page, users.member); await createWorkspace(page, template); }); test("create workspace with default immutable parameters", async ({ page }) => { + await login(page, users.templateAdmin); const richParameters: RichParameter[] = [ secondParameter, fourthParameter, @@ -56,6 +50,8 @@ test("create workspace with default immutable parameters", async ({ page }) => { page, echoResponsesWithParameters(richParameters), ); + + await login(page, users.member); const workspaceName = await createWorkspace(page, template); await verifyParameters(page, workspaceName, richParameters, [ { name: secondParameter.name, value: secondParameter.defaultValue }, @@ -65,11 +61,14 @@ test("create workspace with default immutable parameters", async ({ page }) => { }); test("create workspace with default mutable parameters", async ({ page }) => { + await login(page, users.templateAdmin); const richParameters: RichParameter[] = [firstParameter, thirdParameter]; const template = await createTemplate( page, echoResponsesWithParameters(richParameters), ); + + await login(page, users.member); const workspaceName = await createWorkspace(page, template); await verifyParameters(page, workspaceName, richParameters, [ { name: firstParameter.name, value: firstParameter.defaultValue }, @@ -80,6 +79,7 @@ test("create workspace with default mutable parameters", async ({ page }) => { test("create workspace with default and required parameters", async ({ page, }) => { + await login(page, users.templateAdmin); const richParameters: RichParameter[] = [ secondParameter, fourthParameter, @@ -94,6 +94,8 @@ test("create workspace with default and required parameters", async ({ page, echoResponsesWithParameters(richParameters), ); + + await login(page, users.member); const workspaceName = await createWorkspace(page, template, { richParameters, buildParameters, @@ -108,6 +110,7 @@ test("create workspace with default and required parameters", async ({ }); test("create workspace and overwrite default parameters", async ({ page }) => { + await login(page, users.templateAdmin); // We use randParamName to prevent the new values from corrupting user_history // and thus affecting other tests. const richParameters: RichParameter[] = [ @@ -124,6 +127,7 @@ test("create workspace and overwrite default parameters", async ({ page }) => { echoResponsesWithParameters(richParameters), ); + await login(page, users.member); const workspaceName = await createWorkspace(page, template, { richParameters, buildParameters, @@ -132,6 +136,7 @@ test("create workspace and overwrite default parameters", async ({ page }) => { }); test("create workspace with disable_param search params", async ({ page }) => { + await login(page, users.templateAdmin); const richParameters: RichParameter[] = [ firstParameter, // mutable secondParameter, //immutable @@ -142,6 +147,7 @@ test("create workspace with disable_param search params", async ({ page }) => { echoResponsesWithParameters(richParameters), ); + await login(page, users.member); await page.goto( `/templates/${templateName}/workspace?disable_params=first_parameter,second_parameter`, { @@ -157,8 +163,11 @@ test("create workspace with disable_param search params", async ({ page }) => { // the tests are over. test.skip("create docker workspace", async ({ context, page }) => { requireTerraformProvisioner(); + + await login(page, users.templateAdmin); const template = await createTemplate(page, StarterTemplates.STARTER_DOCKER); + await login(page, users.member); const workspaceName = await createWorkspace(page, template); // The workspace agents must be ready before we try to interact with the workspace. @@ -184,8 +193,6 @@ test.skip("create docker workspace", async ({ context, page }) => { ); await terminal.waitForSelector( `//textarea[contains(@class,"xterm-helper-textarea")]`, - { - state: "visible", - }, + { state: "visible" }, ); }); diff --git a/site/e2e/tests/workspaces/restartWorkspace.spec.ts b/site/e2e/tests/workspaces/restartWorkspace.spec.ts index b65fa95208239..444ff891f0fdc 100644 --- a/site/e2e/tests/workspaces/restartWorkspace.spec.ts +++ b/site/e2e/tests/workspaces/restartWorkspace.spec.ts @@ -1,4 +1,5 @@ import { test } from "@playwright/test"; +import { users } from "../../constants"; import { buildWorkspaceWithParameters, createTemplate, @@ -13,15 +14,17 @@ import type { RichParameter } from "../../provisionerGenerated"; test.beforeEach(async ({ page }) => { beforeCoderTest(page); - await login(page); }); test("restart workspace with ephemeral parameters", async ({ page }) => { + await login(page, users.templateAdmin); const richParameters: RichParameter[] = [firstBuildOption, secondBuildOption]; const template = await createTemplate( page, echoResponsesWithParameters(richParameters), ); + + await login(page, users.member); const workspaceName = await createWorkspace(page, template); // Verify that build options are default (not selected). diff --git a/site/e2e/tests/workspaces/startWorkspace.spec.ts b/site/e2e/tests/workspaces/startWorkspace.spec.ts index d22c8f4f3457e..90fac440046ea 100644 --- a/site/e2e/tests/workspaces/startWorkspace.spec.ts +++ b/site/e2e/tests/workspaces/startWorkspace.spec.ts @@ -1,4 +1,5 @@ import { test } from "@playwright/test"; +import { users } from "../../constants"; import { buildWorkspaceWithParameters, createTemplate, @@ -14,15 +15,17 @@ import type { RichParameter } from "../../provisionerGenerated"; test.beforeEach(async ({ page }) => { beforeCoderTest(page); - await login(page); }); test("start workspace with ephemeral parameters", async ({ page }) => { + await login(page, users.templateAdmin); const richParameters: RichParameter[] = [firstBuildOption, secondBuildOption]; const template = await createTemplate( page, echoResponsesWithParameters(richParameters), ); + + await login(page, users.member); const workspaceName = await createWorkspace(page, template); // Verify that build options are default (not selected). diff --git a/site/e2e/tests/workspaces/updateWorkspace.spec.ts b/site/e2e/tests/workspaces/updateWorkspace.spec.ts index 1db623164699c..48c341eb63956 100644 --- a/site/e2e/tests/workspaces/updateWorkspace.spec.ts +++ b/site/e2e/tests/workspaces/updateWorkspace.spec.ts @@ -1,4 +1,5 @@ import { test } from "@playwright/test"; +import { users } from "../../constants"; import { createTemplate, createWorkspace, @@ -21,18 +22,19 @@ import type { RichParameter } from "../../provisionerGenerated"; test.beforeEach(async ({ page }) => { beforeCoderTest(page); - await login(page); }); test("update workspace, new optional, immutable parameter added", async ({ page, }) => { + await login(page, users.templateAdmin); const richParameters: RichParameter[] = [firstParameter, secondParameter]; const template = await createTemplate( page, echoResponsesWithParameters(richParameters), ); + await login(page, users.member); const workspaceName = await createWorkspace(page, template); // Verify that parameter values are default. @@ -42,6 +44,7 @@ test("update workspace, new optional, immutable parameter added", async ({ ]); // Push updated template. + await login(page, users.templateAdmin); const updatedRichParameters = [...richParameters, fifthParameter]; await updateTemplate( page, @@ -51,6 +54,7 @@ test("update workspace, new optional, immutable parameter added", async ({ ); // Now, update the workspace, and select the value for immutable parameter. + await login(page, users.member); await updateWorkspace(page, workspaceName, updatedRichParameters, [ { name: fifthParameter.name, value: fifthParameter.options[0].value }, ]); @@ -66,12 +70,14 @@ test("update workspace, new optional, immutable parameter added", async ({ test("update workspace, new required, mutable parameter added", async ({ page, }) => { + await login(page, users.templateAdmin); const richParameters: RichParameter[] = [firstParameter, secondParameter]; const template = await createTemplate( page, echoResponsesWithParameters(richParameters), ); + await login(page, users.member); const workspaceName = await createWorkspace(page, template); // Verify that parameter values are default. @@ -81,6 +87,7 @@ test("update workspace, new required, mutable parameter added", async ({ ]); // Push updated template. + await login(page, users.templateAdmin); const updatedRichParameters = [...richParameters, sixthParameter]; await updateTemplate( page, @@ -90,6 +97,7 @@ test("update workspace, new required, mutable parameter added", async ({ ); // Now, update the workspace, and provide the parameter value. + await login(page, users.member); const buildParameters = [{ name: sixthParameter.name, value: "99" }]; await updateWorkspace( page, @@ -107,12 +115,14 @@ test("update workspace, new required, mutable parameter added", async ({ }); test("update workspace with ephemeral parameter enabled", async ({ page }) => { + await login(page, users.templateAdmin); const richParameters: RichParameter[] = [firstParameter, secondBuildOption]; const template = await createTemplate( page, echoResponsesWithParameters(richParameters), ); + await login(page, users.member); const workspaceName = await createWorkspace(page, template); // Verify that parameter values are default. diff --git a/site/package.json b/site/package.json index 68c42aaedfcf1..892e1d50a005f 100644 --- a/site/package.json +++ b/site/package.json @@ -41,25 +41,26 @@ "@emotion/react": "11.14.0", "@emotion/styled": "11.14.0", "@fastly/performance-observer-polyfill": "2.0.0", - "@fontsource-variable/inter": "5.0.15", - "@fontsource/ibm-plex-mono": "5.1.0", + "@fontsource-variable/inter": "5.1.1", + "@fontsource/ibm-plex-mono": "5.1.1", "@monaco-editor/react": "4.6.0", "@mui/icons-material": "5.16.14", "@mui/lab": "5.0.0-alpha.175", "@mui/material": "5.16.14", "@mui/system": "5.16.14", "@mui/utils": "5.16.14", - "@mui/x-tree-view": "7.24.1", + "@mui/x-tree-view": "7.25.0", "@radix-ui/react-avatar": "1.1.2", "@radix-ui/react-collapsible": "1.1.2", "@radix-ui/react-dialog": "1.1.4", "@radix-ui/react-dropdown-menu": "2.1.4", "@radix-ui/react-label": "2.1.0", - "@radix-ui/react-popover": "1.1.3", + "@radix-ui/react-popover": "1.1.5", "@radix-ui/react-select": "2.1.4", - "@radix-ui/react-slider": "1.2.1", + "@radix-ui/react-slider": "1.2.2", "@radix-ui/react-slot": "1.1.1", "@radix-ui/react-switch": "1.1.1", + "@radix-ui/react-tooltip": "1.1.7", "@radix-ui/react-visually-hidden": "1.1.0", "@tanstack/react-query-devtools": "4.35.3", "@xterm/addon-canvas": "0.7.0", @@ -70,7 +71,7 @@ "@xterm/xterm": "5.5.0", "ansi-to-html": "0.7.2", "axios": "1.7.9", - "canvas": "3.0.0-rc2", + "canvas": "3.1.0", "chart.js": "4.4.0", "chartjs-adapter-date-fns": "3.0.0", "chartjs-plugin-annotation": "3.0.1", @@ -94,7 +95,7 @@ "monaco-editor": "0.52.0", "pretty-bytes": "6.1.1", "react": "18.3.1", - "react-chartjs-2": "5.2.0", + "react-chartjs-2": "5.3.0", "react-color": "2.19.3", "react-confetti": "6.2.2", "react-date-range": "1.4.0", @@ -105,52 +106,52 @@ "react-router-dom": "6.26.2", "react-syntax-highlighter": "15.6.1", "react-virtualized-auto-sizer": "1.0.24", - "react-window": "1.8.10", + "react-window": "1.8.11", "recharts": "2.15.0", "remark-gfm": "4.0.0", "resize-observer-polyfill": "1.5.1", - "rollup-plugin-visualizer": "5.12.0", + "rollup-plugin-visualizer": "5.14.0", "semver": "7.6.2", - "tailwind-merge": "2.5.4", + "tailwind-merge": "2.6.0", "tailwindcss-animate": "1.0.7", "tzdata": "1.0.40", - "ua-parser-js": "1.0.33", + "ua-parser-js": "1.0.40", "ufuzzy": "npm:@leeoniya/ufuzzy@1.0.10", "undici": "6.21.1", "unique-names-generator": "4.7.1", "uuid": "9.0.1", - "yup": "1.4.0" + "yup": "1.6.1" }, "devDependencies": { "@biomejs/biome": "1.9.4", "@chromatic-com/storybook": "3.2.2", "@octokit/types": "12.3.0", "@playwright/test": "1.47.2", - "@storybook/addon-actions": "8.4.6", + "@storybook/addon-actions": "8.5.2", "@storybook/addon-essentials": "8.4.6", - "@storybook/addon-interactions": "8.4.6", - "@storybook/addon-links": "8.4.6", - "@storybook/addon-mdx-gfm": "8.4.6", + "@storybook/addon-interactions": "8.5.3", + "@storybook/addon-links": "8.5.2", + "@storybook/addon-mdx-gfm": "8.5.2", "@storybook/addon-themes": "8.4.6", - "@storybook/preview-api": "8.4.7", + "@storybook/preview-api": "8.5.3", "@storybook/react": "8.4.6", "@storybook/react-vite": "8.4.6", "@storybook/test": "8.4.6", "@swc/core": "1.3.38", "@swc/jest": "0.2.37", - "@testing-library/jest-dom": "6.4.6", + "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "14.3.1", "@testing-library/react-hooks": "8.0.1", - "@testing-library/user-event": "14.5.1", + "@testing-library/user-event": "14.6.1", "@types/chroma-js": "2.4.0", - "@types/color-convert": "2.0.0", + "@types/color-convert": "2.0.4", "@types/express": "4.17.17", "@types/file-saver": "2.0.7", "@types/jest": "29.5.14", - "@types/lodash": "4.17.14", + "@types/lodash": "4.17.15", "@types/node": "20.17.16", "@types/react": "18.3.12", - "@types/react-color": "3.0.12", + "@types/react-color": "3.0.13", "@types/react-date-range": "1.4.4", "@types/react-dom": "18.3.1", "@types/react-syntax-highlighter": "15.5.13", @@ -162,9 +163,9 @@ "@types/uuid": "9.0.2", "@vitejs/plugin-react": "4.3.4", "autoprefixer": "10.4.20", - "chromatic": "11.16.3", + "chromatic": "11.25.2", "eventsourcemock": "2.0.0", - "express": "4.21.0", + "express": "4.21.2", "jest": "29.7.0", "jest-canvas-mock": "2.5.2", "jest-environment-jsdom": "29.5.0", @@ -172,15 +173,15 @@ "jest-websocket-mock": "2.5.0", "jest_workaround": "0.1.14", "msw": "2.3.5", - "postcss": "8.4.47", + "postcss": "8.5.1", "protobufjs": "7.4.0", "rxjs": "7.8.1", "ssh2": "1.16.0", - "storybook": "8.5.2", + "storybook": "8.5.3", "storybook-addon-remix-react-router": "3.1.0", "storybook-react-context": "0.7.0", - "tailwindcss": "3.4.13", - "ts-node": "10.9.1", + "tailwindcss": "3.4.17", + "ts-node": "10.9.2", "ts-proto": "1.164.0", "ts-prune": "0.10.3", "typescript": "5.6.3", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index 13e9813fd44e6..62ae51082e96a 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -34,11 +34,11 @@ importers: specifier: 2.0.0 version: 2.0.0 '@fontsource-variable/inter': - specifier: 5.0.15 - version: 5.0.15 + specifier: 5.1.1 + version: 5.1.1 '@fontsource/ibm-plex-mono': - specifier: 5.1.0 - version: 5.1.0 + specifier: 5.1.1 + version: 5.1.1 '@monaco-editor/react': specifier: 4.6.0 version: 4.6.0(monaco-editor@0.52.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -58,8 +58,8 @@ importers: specifier: 5.16.14 version: 5.16.14(@types/react@18.3.12)(react@18.3.1) '@mui/x-tree-view': - specifier: 7.24.1 - version: 7.24.1(@emotion/react@11.14.0(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@mui/material@5.16.14(@emotion/react@11.14.0(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@5.16.14(@emotion/react@11.14.0(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 7.25.0 + version: 7.25.0(@emotion/react@11.14.0(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@mui/material@5.16.14(@emotion/react@11.14.0(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@5.16.14(@emotion/react@11.14.0(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-avatar': specifier: 1.1.2 version: 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -76,20 +76,23 @@ importers: specifier: 2.1.0 version: 2.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-popover': - specifier: 1.1.3 - version: 1.1.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 1.1.5 + version: 1.1.5(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-select': specifier: 2.1.4 version: 2.1.4(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slider': - specifier: 1.2.1 - version: 1.2.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 1.2.2 + version: 1.2.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': specifier: 1.1.1 version: 1.1.1(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-switch': specifier: 1.1.1 version: 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tooltip': + specifier: 1.1.7 + version: 1.1.7(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-visually-hidden': specifier: 1.1.0 version: 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -121,8 +124,8 @@ importers: specifier: 1.7.9 version: 1.7.9 canvas: - specifier: 3.0.0-rc2 - version: 3.0.0-rc2 + specifier: 3.1.0 + version: 3.1.0 chart.js: specifier: 4.4.0 version: 4.4.0 @@ -193,8 +196,8 @@ importers: specifier: 18.3.1 version: 18.3.1 react-chartjs-2: - specifier: 5.2.0 - version: 5.2.0(chart.js@4.4.0)(react@18.3.1) + specifier: 5.3.0 + version: 5.3.0(chart.js@4.4.0)(react@18.3.1) react-color: specifier: 2.19.3 version: 2.19.3(react@18.3.1) @@ -226,8 +229,8 @@ importers: specifier: 1.0.24 version: 1.0.24(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-window: - specifier: 1.8.10 - version: 1.8.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 1.8.11 + version: 1.8.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) recharts: specifier: 2.15.0 version: 2.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -238,23 +241,23 @@ importers: specifier: 1.5.1 version: 1.5.1 rollup-plugin-visualizer: - specifier: 5.12.0 - version: 5.12.0(rollup@4.32.0) + specifier: 5.14.0 + version: 5.14.0(rollup@4.32.0) semver: specifier: 7.6.2 version: 7.6.2 tailwind-merge: - specifier: 2.5.4 - version: 2.5.4 + specifier: 2.6.0 + version: 2.6.0 tailwindcss-animate: specifier: 1.0.7 - version: 1.0.7(tailwindcss@3.4.13(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3))) + version: 1.0.7(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3))) tzdata: specifier: 1.0.40 version: 1.0.40 ua-parser-js: - specifier: 1.0.33 - version: 1.0.33 + specifier: 1.0.40 + version: 1.0.40 ufuzzy: specifier: npm:@leeoniya/ufuzzy@1.0.10 version: '@leeoniya/ufuzzy@1.0.10' @@ -268,15 +271,15 @@ importers: specifier: 9.0.1 version: 9.0.1 yup: - specifier: 1.4.0 - version: 1.4.0 + specifier: 1.6.1 + version: 1.6.1 devDependencies: '@biomejs/biome': specifier: 1.9.4 version: 1.9.4 '@chromatic-com/storybook': specifier: 3.2.2 - version: 3.2.2(react@18.3.1)(storybook@8.5.2(prettier@3.4.1)) + version: 3.2.2(react@18.3.1)(storybook@8.5.3(prettier@3.4.1)) '@octokit/types': specifier: 12.3.0 version: 12.3.0 @@ -284,35 +287,35 @@ importers: specifier: 1.47.2 version: 1.47.2 '@storybook/addon-actions': - specifier: 8.4.6 - version: 8.4.6(storybook@8.5.2(prettier@3.4.1)) + specifier: 8.5.2 + version: 8.5.2(storybook@8.5.3(prettier@3.4.1)) '@storybook/addon-essentials': specifier: 8.4.6 - version: 8.4.6(@types/react@18.3.12)(storybook@8.5.2(prettier@3.4.1)) + version: 8.4.6(@types/react@18.3.12)(storybook@8.5.3(prettier@3.4.1)) '@storybook/addon-interactions': - specifier: 8.4.6 - version: 8.4.6(storybook@8.5.2(prettier@3.4.1)) + specifier: 8.5.3 + version: 8.5.3(storybook@8.5.3(prettier@3.4.1)) '@storybook/addon-links': - specifier: 8.4.6 - version: 8.4.6(react@18.3.1)(storybook@8.5.2(prettier@3.4.1)) + specifier: 8.5.2 + version: 8.5.2(react@18.3.1)(storybook@8.5.3(prettier@3.4.1)) '@storybook/addon-mdx-gfm': - specifier: 8.4.6 - version: 8.4.6(storybook@8.5.2(prettier@3.4.1)) + specifier: 8.5.2 + version: 8.5.2(storybook@8.5.3(prettier@3.4.1)) '@storybook/addon-themes': specifier: 8.4.6 - version: 8.4.6(storybook@8.5.2(prettier@3.4.1)) + version: 8.4.6(storybook@8.5.3(prettier@3.4.1)) '@storybook/preview-api': - specifier: 8.4.7 - version: 8.4.7(storybook@8.5.2(prettier@3.4.1)) + specifier: 8.5.3 + version: 8.5.3(storybook@8.5.3(prettier@3.4.1)) '@storybook/react': specifier: 8.4.6 - version: 8.4.6(@storybook/test@8.4.6(storybook@8.5.2(prettier@3.4.1)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.2(prettier@3.4.1))(typescript@5.6.3) + version: 8.4.6(@storybook/test@8.4.6(storybook@8.5.3(prettier@3.4.1)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.3(prettier@3.4.1))(typescript@5.6.3) '@storybook/react-vite': specifier: 8.4.6 - version: 8.4.6(@storybook/test@8.4.6(storybook@8.5.2(prettier@3.4.1)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.32.0)(storybook@8.5.2(prettier@3.4.1))(typescript@5.6.3)(vite@5.4.14(@types/node@20.17.16)) + version: 8.4.6(@storybook/test@8.4.6(storybook@8.5.3(prettier@3.4.1)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.32.0)(storybook@8.5.3(prettier@3.4.1))(typescript@5.6.3)(vite@5.4.14(@types/node@20.17.16)) '@storybook/test': specifier: 8.4.6 - version: 8.4.6(storybook@8.5.2(prettier@3.4.1)) + version: 8.4.6(storybook@8.5.3(prettier@3.4.1)) '@swc/core': specifier: 1.3.38 version: 1.3.38 @@ -320,8 +323,8 @@ importers: specifier: 0.2.37 version: 0.2.37(@swc/core@1.3.38) '@testing-library/jest-dom': - specifier: 6.4.6 - version: 6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.17.16)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3))) + specifier: 6.6.3 + version: 6.6.3 '@testing-library/react': specifier: 14.3.1 version: 14.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -329,14 +332,14 @@ importers: specifier: 8.0.1 version: 8.0.1(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@testing-library/user-event': - specifier: 14.5.1 - version: 14.5.1(@testing-library/dom@10.4.0) + specifier: 14.6.1 + version: 14.6.1(@testing-library/dom@10.4.0) '@types/chroma-js': specifier: 2.4.0 version: 2.4.0 '@types/color-convert': - specifier: 2.0.0 - version: 2.0.0 + specifier: 2.0.4 + version: 2.0.4 '@types/express': specifier: 4.17.17 version: 4.17.17 @@ -347,8 +350,8 @@ importers: specifier: 29.5.14 version: 29.5.14 '@types/lodash': - specifier: 4.17.14 - version: 4.17.14 + specifier: 4.17.15 + version: 4.17.15 '@types/node': specifier: 20.17.16 version: 20.17.16 @@ -356,8 +359,8 @@ importers: specifier: 18.3.12 version: 18.3.12 '@types/react-color': - specifier: 3.0.12 - version: 3.0.12 + specifier: 3.0.13 + version: 3.0.13(@types/react@18.3.12) '@types/react-date-range': specifier: 1.4.4 version: 1.4.4 @@ -390,25 +393,25 @@ importers: version: 4.3.4(vite@5.4.14(@types/node@20.17.16)) autoprefixer: specifier: 10.4.20 - version: 10.4.20(postcss@8.4.47) + version: 10.4.20(postcss@8.5.1) chromatic: - specifier: 11.16.3 - version: 11.16.3 + specifier: 11.25.2 + version: 11.25.2 eventsourcemock: specifier: 2.0.0 version: 2.0.0 express: - specifier: 4.21.0 - version: 4.21.0 + specifier: 4.21.2 + version: 4.21.2 jest: specifier: 29.7.0 - version: 29.7.0(@types/node@20.17.16)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)) + version: 29.7.0(@types/node@20.17.16)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)) jest-canvas-mock: specifier: 2.5.2 version: 2.5.2 jest-environment-jsdom: specifier: 29.5.0 - version: 29.5.0(canvas@3.0.0-rc2) + version: 29.5.0(canvas@3.1.0) jest-location-mock: specifier: 2.0.0 version: 2.0.0 @@ -422,8 +425,8 @@ importers: specifier: 2.3.5 version: 2.3.5(typescript@5.6.3) postcss: - specifier: 8.4.47 - version: 8.4.47 + specifier: 8.5.1 + version: 8.5.1 protobufjs: specifier: 7.4.0 version: 7.4.0 @@ -434,20 +437,20 @@ importers: specifier: 1.16.0 version: 1.16.0 storybook: - specifier: 8.5.2 - version: 8.5.2(prettier@3.4.1) + specifier: 8.5.3 + version: 8.5.3(prettier@3.4.1) storybook-addon-remix-react-router: specifier: 3.1.0 - version: 3.1.0(@storybook/blocks@8.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.2(prettier@3.4.1)))(@storybook/channels@8.1.11)(@storybook/components@8.4.6(storybook@8.5.2(prettier@3.4.1)))(@storybook/core-events@8.1.11)(@storybook/manager-api@8.4.6(storybook@8.5.2(prettier@3.4.1)))(@storybook/preview-api@8.4.7(storybook@8.5.2(prettier@3.4.1)))(@storybook/theming@8.4.6(storybook@8.5.2(prettier@3.4.1)))(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.26.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 3.1.0(@storybook/blocks@8.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.3(prettier@3.4.1)))(@storybook/channels@8.1.11)(@storybook/components@8.4.6(storybook@8.5.3(prettier@3.4.1)))(@storybook/core-events@8.1.11)(@storybook/manager-api@8.4.6(storybook@8.5.3(prettier@3.4.1)))(@storybook/preview-api@8.5.3(storybook@8.5.3(prettier@3.4.1)))(@storybook/theming@8.4.6(storybook@8.5.3(prettier@3.4.1)))(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.26.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) storybook-react-context: specifier: 0.7.0 - version: 0.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.2(prettier@3.4.1)) + version: 0.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.3(prettier@3.4.1)) tailwindcss: - specifier: 3.4.13 - version: 3.4.13(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)) + specifier: 3.4.17 + version: 3.4.17(ts-node@10.9.2(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)) ts-node: - specifier: 10.9.1 - version: 10.9.1(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3) + specifier: 10.9.2 + version: 10.9.2(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3) ts-proto: specifier: 1.164.0 version: 1.164.0 @@ -473,8 +476,8 @@ packages: resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==, tarball: https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz} engines: {node: '>=0.10.0'} - '@adobe/css-tools@4.4.0': - resolution: {integrity: sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==, tarball: https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz} + '@adobe/css-tools@4.4.1': + resolution: {integrity: sha512-12WGKBQzjUAI4ayyF4IAtfw2QR/IDoqk6jTddXDhtYTJF9ASmoE1zst7cVtP0aL/F1jUJL5r+JxKXKEgHNbEUQ==, tarball: https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.1.tgz} '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==, tarball: https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz} @@ -658,10 +661,6 @@ packages: resolution: {integrity: sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==, tarball: https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.6.tgz} engines: {node: '>=6.9.0'} - '@babel/runtime@7.24.7': - resolution: {integrity: sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==, tarball: https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz} - engines: {node: '>=6.9.0'} - '@babel/runtime@7.25.6': resolution: {integrity: sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==, tarball: https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz} engines: {node: '>=6.9.0'} @@ -1144,11 +1143,11 @@ packages: '@fastly/performance-observer-polyfill@2.0.0': resolution: {integrity: sha512-cQC4E6ReYY4Vud+eCJSCr1N0dSz+fk7xJlLiSgPFDHbnFLZo5DenazoersMt9D8JkEhl9Z5ZwJ/8apcjSrdb8Q==, tarball: https://registry.npmjs.org/@fastly/performance-observer-polyfill/-/performance-observer-polyfill-2.0.0.tgz} - '@floating-ui/core@1.6.8': - resolution: {integrity: sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==, tarball: https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz} + '@floating-ui/core@1.6.9': + resolution: {integrity: sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==, tarball: https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz} - '@floating-ui/dom@1.6.12': - resolution: {integrity: sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==, tarball: https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz} + '@floating-ui/dom@1.6.13': + resolution: {integrity: sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==, tarball: https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz} '@floating-ui/react-dom@2.1.2': resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==, tarball: https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz} @@ -1156,14 +1155,14 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' - '@floating-ui/utils@0.2.8': - resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==, tarball: https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz} + '@floating-ui/utils@0.2.9': + resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==, tarball: https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz} - '@fontsource-variable/inter@5.0.15': - resolution: {integrity: sha512-CdQPQQgOVxg6ifmbrqYZeUqtQf7p2wPn6EvJ4M+vdNnsmYZgYwPPPQDNlIOU7LCUlSGaN26v6H0uA030WKn61g==, tarball: https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.0.15.tgz} + '@fontsource-variable/inter@5.1.1': + resolution: {integrity: sha512-OpXFTmiH6tHkYijMvQTycFKBLK4X+SRV6tet1m4YOUH7SzIIlMqDja+ocDtiCA72UthBH/vF+3ZtlMr2rN/wIw==, tarball: https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.1.1.tgz} - '@fontsource/ibm-plex-mono@5.1.0': - resolution: {integrity: sha512-XKsZNyRCj6tz8zlatHmniSoLVephMD5GQG2sXgcaEb8DkUO+O61r28uTlIMEZuoZXtP4c4STvL+KUlJM5jZOEg==, tarball: https://registry.npmjs.org/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.1.0.tgz} + '@fontsource/ibm-plex-mono@5.1.1': + resolution: {integrity: sha512-1aayqPe/ZkD3MlvqpmOHecfA3f2B8g+fAEkgvcCd3lkPP0pS1T0xG5Zmn2EsJQqr1JURtugPUH+5NqvKyfFZMQ==, tarball: https://registry.npmjs.org/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.1.1.tgz} '@humanwhocodes/config-array@0.11.14': resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==, tarball: https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz} @@ -1301,10 +1300,6 @@ packages: typescript: optional: true - '@jridgewell/gen-mapping@0.3.5': - resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==, tarball: https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz} - engines: {node: '>=6.0.0'} - '@jridgewell/gen-mapping@0.3.8': resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==, tarball: https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz} engines: {node: '>=6.0.0'} @@ -1317,9 +1312,6 @@ packages: resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==, tarball: https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz} engines: {node: '>=6.0.0'} - '@jridgewell/sourcemap-codec@1.4.15': - resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==, tarball: https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz} - '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==, tarball: https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz} @@ -1483,14 +1475,14 @@ packages: '@types/react': optional: true - '@mui/x-internals@7.24.1': - resolution: {integrity: sha512-9BvJzpLJnS9BDphvkiv6v0QOLxbnu8jhwcexFjtCQ2ZyxtVuVsWzGZ2npT9sGOil7+eaFDmWnJtea/tgrPvSwQ==, tarball: https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.24.1.tgz} + '@mui/x-internals@7.25.0': + resolution: {integrity: sha512-tBUN54YznAkmtCIRAOl35Kgl0MjFDIjUbzIrbWRgVSIR3QJ8bXnVSkiRBi+P91SZEl9+ZW0rDj+osq7xFJV0kg==, tarball: https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.25.0.tgz} engines: {node: '>=14.0.0'} peerDependencies: react: ^17.0.0 || ^18.0.0 || ^19.0.0 - '@mui/x-tree-view@7.24.1': - resolution: {integrity: sha512-IR24GAw8e8NORlVxJzNf1RnJu/ThBLv6sNlHoh7sF82MQ89i3nUCErA2gqYnY4aZ4g3GfJSWnYikPP24OTEqRQ==, tarball: https://registry.npmjs.org/@mui/x-tree-view/-/x-tree-view-7.24.1.tgz} + '@mui/x-tree-view@7.25.0': + resolution: {integrity: sha512-DWBMWzfMtIBXMvGCb0WdEeo4H8TLleKeMExzX0L3zvo87Ootvmcin9d7x1q1ZABekT6wREVl3+pVTEoBzcFWug==, tarball: https://registry.npmjs.org/@mui/x-tree-view/-/x-tree-view-7.25.0.tgz} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.9.0 @@ -1622,19 +1614,6 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-collection@1.1.0': - resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==, tarball: https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - '@radix-ui/react-collection@1.1.1': resolution: {integrity: sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==, tarball: https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz} peerDependencies: @@ -1666,15 +1645,6 @@ packages: '@types/react': optional: true - '@radix-ui/react-context@1.1.0': - resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==, tarball: https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@radix-ui/react-context@1.1.1': resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==, tarball: https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz} peerDependencies: @@ -1706,8 +1676,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-dismissable-layer@1.1.2': - resolution: {integrity: sha512-kEHnlhv7wUggvhuJPkyw4qspXLJOdYoAP4dO2c8ngGuXTq1w/HZp1YeVB+NQ2KbH1iEG+pvOCGYSqh9HZOz6hg==, tarball: https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.2.tgz} + '@radix-ui/react-dismissable-layer@1.1.3': + resolution: {integrity: sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==, tarball: https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.3.tgz} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1719,8 +1689,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-dismissable-layer@1.1.3': - resolution: {integrity: sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==, tarball: https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.3.tgz} + '@radix-ui/react-dismissable-layer@1.1.4': + resolution: {integrity: sha512-XDUI0IVYVSwjMXxM6P4Dfti7AH+Y4oS/TB+sglZ/EXc7cqLwGAmp1NlMrcUjj7ks6R5WTZuWKv44FBbLpwU3sA==, tarball: https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.4.tgz} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1802,8 +1772,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-popover@1.1.3': - resolution: {integrity: sha512-MBDKFwRe6fi0LT8m/Jl4V8J3WbS/UfXJtsgg8Ym5w5AyPG3XfHH4zhBp1P8HmZK83T8J7UzVm6/JpDE3WMl1Dw==, tarball: https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.3.tgz} + '@radix-ui/react-popover@1.1.5': + resolution: {integrity: sha512-YXkTAftOIW2Bt3qKH8vYr6n9gCkVrvyvfiTObVjoHVTHnNj26rmvO87IKa3VgtgCjb8FAQ6qOjNViwl+9iIzlg==, tarball: https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.5.tgz} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1906,8 +1876,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-slider@1.2.1': - resolution: {integrity: sha512-bEzQoDW0XP+h/oGbutF5VMWJPAl/UU8IJjr7h02SOHDIIIxq+cep8nItVNoBV+OMmahCdqdF38FTpmXoqQUGvw==, tarball: https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.1.tgz} + '@radix-ui/react-slider@1.2.2': + resolution: {integrity: sha512-sNlU06ii1/ZcbHf8I9En54ZPW0Vil/yPVg4vQMcFNjrIx51jsHbFl1HYHQvCIWJSr1q0ZmA+iIs/ZTv8h7HHSA==, tarball: https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.2.tgz} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1950,6 +1920,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-tooltip@1.1.7': + resolution: {integrity: sha512-ss0s80BC0+g0+Zc53MvilcnTYSOi4mSuFWBPYPuTOFGjx+pUU+ZrmamMNwS56t8MTFlniA5ocjd4jYm/CdhbOg==, tarball: https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.7.tgz} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-use-callback-ref@1.1.0': resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==, tarball: https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz} peerDependencies: @@ -2164,6 +2147,11 @@ packages: peerDependencies: storybook: ^8.4.6 + '@storybook/addon-actions@8.5.2': + resolution: {integrity: sha512-g0gLesVSFgstUq5QphsLeC1vEdwNHgqo2TE0m+STM47832xbxBwmK6uvBeqi416xZvnt1TTKaaBr4uCRRQ64Ww==, tarball: https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-8.5.2.tgz} + peerDependencies: + storybook: ^8.5.2 + '@storybook/addon-backgrounds@8.4.6': resolution: {integrity: sha512-RSjJ3iElxlQXebZrz1s5LeoLpAXr9LAGifX7w0abMzN5sg6QSwNeUHko2eT3V57M3k1Fa/5Eelso/QBQifFEog==, tarball: https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-8.4.6.tgz} peerDependencies: @@ -2189,24 +2177,24 @@ packages: peerDependencies: storybook: ^8.4.6 - '@storybook/addon-interactions@8.4.6': - resolution: {integrity: sha512-sR2oUSYIGUoAdrHT+fM1zgykhad98bsJ11c79r7HfBMXEPWc1yRcjIMmz8Xz06FMROMfebqduYDf60V++/I0Jw==, tarball: https://registry.npmjs.org/@storybook/addon-interactions/-/addon-interactions-8.4.6.tgz} + '@storybook/addon-interactions@8.5.3': + resolution: {integrity: sha512-nQuP65iFGgqfVp/O8NxNDUwLTWmQBW4bofUFaT4wzYn7Jk9zobOZYtgQvdqBZtNzBDYmLrfrCutEBj5jVPRyuQ==, tarball: https://registry.npmjs.org/@storybook/addon-interactions/-/addon-interactions-8.5.3.tgz} peerDependencies: - storybook: ^8.4.6 + storybook: ^8.5.3 - '@storybook/addon-links@8.4.6': - resolution: {integrity: sha512-1KoG9ytEWWwdF/dheu1O0dayQTMsHw++Qk8afqw7bwW1Cxz5LuAJH5ZscFWMiE5f4Xq1NgaJdeAUaIavyoOcdg==, tarball: https://registry.npmjs.org/@storybook/addon-links/-/addon-links-8.4.6.tgz} + '@storybook/addon-links@8.5.2': + resolution: {integrity: sha512-eDKOQoAKKUQo0JqeLNzMLu6fm1s3oxwZ6O+rAWS6n5bsrjZS2Ul8esKkRriFVwHtDtqx99wneqOscS8IzE/ENw==, tarball: https://registry.npmjs.org/@storybook/addon-links/-/addon-links-8.5.2.tgz} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.4.6 + storybook: ^8.5.2 peerDependenciesMeta: react: optional: true - '@storybook/addon-mdx-gfm@8.4.6': - resolution: {integrity: sha512-wagsSBUN6pwcSZSWxp/aOhE16ZKI8ZW4XeRT6QivySmkJaLcbva+HNvQOijdXIM28W8PprKjqtyVa8nu4YQxsw==, tarball: https://registry.npmjs.org/@storybook/addon-mdx-gfm/-/addon-mdx-gfm-8.4.6.tgz} + '@storybook/addon-mdx-gfm@8.5.2': + resolution: {integrity: sha512-UuJDa2Asch8Z6H+vzLg+/VQQNbHhqmDtn8OSfNHo6Lr6a0uk6LofYKvP/nB7i6wMUvnaM+Qh/b5hAI/VCXitBQ==, tarball: https://registry.npmjs.org/@storybook/addon-mdx-gfm/-/addon-mdx-gfm-8.5.2.tgz} peerDependencies: - storybook: ^8.4.6 + storybook: ^8.5.2 '@storybook/addon-measure@8.4.6': resolution: {integrity: sha512-N2IRpr39g5KpexCAS1vIHJT+phc9Yilwm3PULds2rQ66VMTbkxobXJDdt0NS05g5n9/eDniroNQwdCeLg4tkpw==, tarball: https://registry.npmjs.org/@storybook/addon-measure/-/addon-measure-8.4.6.tgz} @@ -2265,8 +2253,8 @@ packages: '@storybook/core-events@8.1.11': resolution: {integrity: sha512-vXaNe2KEW9BGlLrg0lzmf5cJ0xt+suPjWmEODH5JqBbrdZ67X6ApA2nb6WcxDQhykesWCuFN5gp1l+JuDOBi7A==, tarball: https://registry.npmjs.org/@storybook/core-events/-/core-events-8.1.11.tgz} - '@storybook/core@8.5.2': - resolution: {integrity: sha512-rCOpXZo2XbdKVnZiv8oC9FId/gLkStpKGGL7hhdg/RyjcyUyTfhsvaf7LXKZH2A0n/UpwFxhF3idRfhgc1XiSg==, tarball: https://registry.npmjs.org/@storybook/core/-/core-8.5.2.tgz} + '@storybook/core@8.5.3': + resolution: {integrity: sha512-ZLlr2pltbj/hmC54lggJTnh09FCAJR62lIdiXNwa+V+/eJz0CfD8tfGmZGKPSmaQeZBpMwAOeRM97k2oLPF+0w==, tarball: https://registry.npmjs.org/@storybook/core/-/core-8.5.3.tgz} peerDependencies: prettier: ^2 || ^3 peerDependenciesMeta: @@ -2302,6 +2290,11 @@ packages: peerDependencies: storybook: ^8.4.6 + '@storybook/instrumenter@8.5.3': + resolution: {integrity: sha512-pxaTbGeju8MkwouIiaWX5DMWtpRruxqig8W3nZPOvzoSCCbQY+sLMQoyXxFlpGxLBjcvXivkL7AMVBKps5sFEQ==, tarball: https://registry.npmjs.org/@storybook/instrumenter/-/instrumenter-8.5.3.tgz} + peerDependencies: + storybook: ^8.5.3 + '@storybook/manager-api@8.4.6': resolution: {integrity: sha512-TsXlQ5m5rTl2KNT9icPFyy822AqXrx1QplZBt/L7cFn7SpqQKDeSta21FH7MG0piAvzOweXebVSqKngJ6cCWWQ==, tarball: https://registry.npmjs.org/@storybook/manager-api/-/manager-api-8.4.6.tgz} peerDependencies: @@ -2312,8 +2305,8 @@ packages: peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - '@storybook/preview-api@8.4.7': - resolution: {integrity: sha512-0QVQwHw+OyZGHAJEXo6Knx+6/4er7n2rTDE5RYJ9F2E2Lg42E19pfdLlq2Jhoods2Xrclo3wj6GWR//Ahi39Eg==, tarball: https://registry.npmjs.org/@storybook/preview-api/-/preview-api-8.4.7.tgz} + '@storybook/preview-api@8.5.3': + resolution: {integrity: sha512-dUsuXW+KgDg4tWXOB6dk5j5gwwRUzbPvicHAY9mzbpSVScbWXuE5T/S/9hHlbtfkhFroWQgPx2eB8z3rai+7RQ==, tarball: https://registry.npmjs.org/@storybook/preview-api/-/preview-api-8.5.3.tgz} peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 @@ -2353,6 +2346,11 @@ packages: peerDependencies: storybook: ^8.4.6 + '@storybook/test@8.5.3': + resolution: {integrity: sha512-2smoDbtU6Qh4yk0uD18qGfW6ll7lZBzKlF58Ha1CgWR4o+jpeeTQcfDLH9gG6sNrpojF7AVzMh/aN9BDHD+Chg==, tarball: https://registry.npmjs.org/@storybook/test/-/test-8.5.3.tgz} + peerDependencies: + storybook: ^8.5.3 + '@storybook/theming@8.4.6': resolution: {integrity: sha512-q7vDPN/mgj7cXIVQ9R1/V75hrzNgKkm2G0LjMo57//9/djQ+7LxvBsR1iScbFIRSEqppvMiBFzkts+2uXidySA==, tarball: https://registry.npmjs.org/@storybook/theming/-/theming-8.4.6.tgz} peerDependencies: @@ -2465,31 +2463,14 @@ packages: resolution: {integrity: sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==, tarball: https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.3.tgz} engines: {node: '>=14'} - '@testing-library/jest-dom@6.4.6': - resolution: {integrity: sha512-8qpnGVincVDLEcQXWaHOf6zmlbwTKc6Us6PPu4CRnPXCzo2OGBS5cwgMMOWdxDpEz1mkbvXHpEy99M5Yvt682w==, tarball: https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.6.tgz} - engines: {node: '>=14', npm: '>=6', yarn: '>=1'} - peerDependencies: - '@jest/globals': '>= 28' - '@types/bun': latest - '@types/jest': '>= 28' - jest: '>= 28' - vitest: '>= 0.32' - peerDependenciesMeta: - '@jest/globals': - optional: true - '@types/bun': - optional: true - '@types/jest': - optional: true - jest: - optional: true - vitest: - optional: true - '@testing-library/jest-dom@6.5.0': resolution: {integrity: sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==, tarball: https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.5.0.tgz} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + '@testing-library/jest-dom@6.6.3': + resolution: {integrity: sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==, tarball: https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + '@testing-library/react-hooks@8.0.1': resolution: {integrity: sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==, tarball: https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz} engines: {node: '>=12'} @@ -2513,14 +2494,14 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 - '@testing-library/user-event@14.5.1': - resolution: {integrity: sha512-UCcUKrUYGj7ClomOo2SpNVvx4/fkd/2BbIHDCle8A0ax+P3bU7yJwDBDrS6ZwdTMARWTGODX1hEsCcO+7beJjg==, tarball: https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.1.tgz} + '@testing-library/user-event@14.5.2': + resolution: {integrity: sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==, tarball: https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz} engines: {node: '>=12', npm: '>=6'} peerDependencies: '@testing-library/dom': '>=7.21.4' - '@testing-library/user-event@14.5.2': - resolution: {integrity: sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==, tarball: https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz} + '@testing-library/user-event@14.6.1': + resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==, tarball: https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz} engines: {node: '>=12', npm: '>=6'} peerDependencies: '@testing-library/dom': '>=7.21.4' @@ -2532,8 +2513,8 @@ packages: '@ts-morph/common@0.12.3': resolution: {integrity: sha512-4tUmeLyXJnJWvTFOKtcNJ1yh0a3SsTLi2MUoyj8iUNznFRN1ZquaNe7Oukqrnki2FzZkm0J9adCNLDZxUzvj+w==, tarball: https://registry.npmjs.org/@ts-morph/common/-/common-0.12.3.tgz} - '@tsconfig/node10@1.0.9': - resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==, tarball: https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz} + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==, tarball: https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz} '@tsconfig/node12@1.0.11': resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==, tarball: https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz} @@ -2565,11 +2546,11 @@ packages: '@types/chroma-js@2.4.0': resolution: {integrity: sha512-JklMxityrwjBTjGY2anH8JaTx3yjRU3/sEHSblLH1ba5lqcSh1LnImXJZO5peJfXyqKYWjHTGy4s5Wz++hARrw==, tarball: https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.4.0.tgz} - '@types/color-convert@2.0.0': - resolution: {integrity: sha512-m7GG7IKKGuJUXvkZ1qqG3ChccdIM/qBBo913z+Xft0nKCX4hAU/IxKwZBU4cpRZ7GS5kV4vOblUkILtSShCPXQ==, tarball: https://registry.npmjs.org/@types/color-convert/-/color-convert-2.0.0.tgz} + '@types/color-convert@2.0.4': + resolution: {integrity: sha512-Ub1MmDdyZ7mX//g25uBAoH/mWGd9swVbt8BseymnaE18SU4po/PjmCrHxqIIRjBo3hV/vh1KGr0eMxUhp+t+dQ==, tarball: https://registry.npmjs.org/@types/color-convert/-/color-convert-2.0.4.tgz} - '@types/color-name@1.1.1': - resolution: {integrity: sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==, tarball: https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz} + '@types/color-name@1.1.5': + resolution: {integrity: sha512-j2K5UJqGTxeesj6oQuGpMgifpT5k9HprgQd8D1Y0lOFqKHl3PJu5GMeS4Y5EgjS55AE6OQxf8mPED9uaGbf4Cg==, tarball: https://registry.npmjs.org/@types/color-name/-/color-name-1.1.5.tgz} '@types/connect@3.4.35': resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==, tarball: https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz} @@ -2664,8 +2645,8 @@ packages: '@types/jsdom@20.0.1': resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==, tarball: https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz} - '@types/lodash@4.17.14': - resolution: {integrity: sha512-jsxagdikDiDBeIRaPYtArcT8my4tN1og7MtMRquFT3XNA6axxyHDRUemqDz/taRDdOUn0GnGHRCuff4q48sW9A==, tarball: https://registry.npmjs.org/@types/lodash/-/lodash-4.17.14.tgz} + '@types/lodash@4.17.15': + resolution: {integrity: sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==, tarball: https://registry.npmjs.org/@types/lodash/-/lodash-4.17.15.tgz} '@types/mdast@4.0.3': resolution: {integrity: sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg==, tarball: https://registry.npmjs.org/@types/mdast/-/mdast-4.0.3.tgz} @@ -2709,8 +2690,10 @@ packages: '@types/range-parser@1.2.4': resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==, tarball: https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz} - '@types/react-color@3.0.12': - resolution: {integrity: sha512-pr3uKE3lSvf7GFo1Rn2K3QktiZQFFrSgSGJ/3iMvSOYWt2pPAJ97rVdVfhWxYJZ8prAEXzoP2XX//3qGSQgu7Q==, tarball: https://registry.npmjs.org/@types/react-color/-/react-color-3.0.12.tgz} + '@types/react-color@3.0.13': + resolution: {integrity: sha512-2c/9FZ4ixC5T3JzN0LP5Cke2Mf0MKOP2Eh0NPDPWmuVH3NjPyhEjqNMQpN1Phr5m74egAy+p2lYNAFrX1z9Yrg==, tarball: https://registry.npmjs.org/@types/react-color/-/react-color-3.0.13.tgz} + peerDependencies: + '@types/react': '*' '@types/react-date-range@1.4.4': resolution: {integrity: sha512-9Y9NyNgaCsEVN/+O4HKuxzPbVjRVBGdOKRxMDcsTRWVG62lpYgnxefNckTXDWup8FvczoqPW0+ESZR6R1yymDg==, tarball: https://registry.npmjs.org/@types/react-date-range/-/react-date-range-1.4.4.tgz} @@ -2735,8 +2718,10 @@ packages: '@types/react@18.3.12': resolution: {integrity: sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==, tarball: https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz} - '@types/reactcss@1.2.6': - resolution: {integrity: sha512-qaIzpCuXNWomGR1Xq8SCFTtF4v8V27Y6f+b9+bzHiv087MylI/nTCqqdChNeWS7tslgROmYB7yeiruWX7WnqNg==, tarball: https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.6.tgz} + '@types/reactcss@1.2.13': + resolution: {integrity: sha512-gi3S+aUi6kpkF5vdhUsnkwbiSEIU/BEJyD7kBy2SudWBUuKmJk8AQKE0OVcQQeEy40Azh0lV6uynxlikYIJuwg==, tarball: https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.13.tgz} + peerDependencies: + '@types/react': '*' '@types/resolve@1.20.4': resolution: {integrity: sha512-BKGK0T1VgB1zD+PwQR4RRf0ais3NyvH1qjLUrHI5SEiccYaJrhLstLuoXFWJ+2Op9whGizSPUMGPJY/Qtb/A2w==, tarball: https://registry.npmjs.org/@types/resolve/-/resolve-1.20.4.tgz} @@ -2869,23 +2854,9 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn-walk@8.2.0: - resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==, tarball: https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz} - engines: {node: '>=0.4.0'} - - acorn-walk@8.3.0: - resolution: {integrity: sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==, tarball: https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz} - engines: {node: '>=0.4.0'} - - acorn@8.10.0: - resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==, tarball: https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz} - engines: {node: '>=0.4.0'} - hasBin: true - - acorn@8.11.2: - resolution: {integrity: sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==, tarball: https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz} + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==, tarball: https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz} engines: {node: '>=0.4.0'} - hasBin: true acorn@8.14.0: resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==, tarball: https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz} @@ -2961,6 +2932,10 @@ packages: aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==, tarball: https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz} + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==, tarball: https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz} + engines: {node: '>= 0.4'} + array-buffer-byte-length@1.0.0: resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==, tarball: https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz} @@ -3129,8 +3104,8 @@ packages: caniuse-lite@1.0.30001690: resolution: {integrity: sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==, tarball: https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz} - canvas@3.0.0-rc2: - resolution: {integrity: sha512-esx4bYDznnqgRX4G8kaEaf0W3q8xIc51WpmrIitDzmcoEgwnv9wSKdzT6UxWZ4wkVu5+ileofppX0TpyviJRdQ==, tarball: https://registry.npmjs.org/canvas/-/canvas-3.0.0-rc2.tgz} + canvas@3.1.0: + resolution: {integrity: sha512-tTj3CqqukVJ9NgSahykNwtGda7V33VLObwrHfzT0vqJXu7J4d4C/7kQQW3fOEGDfZZoILPut5H00gOjyttPGyg==, tarball: https://registry.npmjs.org/canvas/-/canvas-3.1.0.tgz} engines: {node: ^18.12.0 || >= 20.9.0} case-anything@2.1.13: @@ -3210,8 +3185,8 @@ packages: chroma-js@2.4.2: resolution: {integrity: sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==, tarball: https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz} - chromatic@11.16.3: - resolution: {integrity: sha512-bckarRbZ3M1BvsmhLqEMschuQPk2FlSD9cvy8383JwoVvaIqLr0dv1tI/DPM4LMuXOjTjeBSZZINVH9r3RMiiA==, tarball: https://registry.npmjs.org/chromatic/-/chromatic-11.16.3.tgz} + chromatic@11.25.2: + resolution: {integrity: sha512-/9eQWn6BU1iFsop86t8Au21IksTRxwXAl7if8YHD05L2AbuMjClLWZo5cZojqrJHGKDhTqfrC2X2xE4uSm0iKw==, tarball: https://registry.npmjs.org/chromatic/-/chromatic-11.25.2.tgz} hasBin: true peerDependencies: '@chromatic-com/cypress': ^0.*.* || ^1.0.0 @@ -3329,8 +3304,8 @@ packages: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==, tarball: https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz} engines: {node: '>= 0.6'} - cookie@0.6.0: - resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==, tarball: https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz} + cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==, tarball: https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz} engines: {node: '>= 0.6'} copy-anything@3.0.5: @@ -3473,10 +3448,6 @@ packages: decode-named-character-reference@1.0.2: resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==, tarball: https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz} - decompress-response@4.2.1: - resolution: {integrity: sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==, tarball: https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz} - engines: {node: '>=8'} - decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==, tarball: https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz} engines: {node: '>=10'} @@ -3548,8 +3519,8 @@ packages: engines: {node: '>=0.10'} hasBin: true - detect-libc@2.0.2: - resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==, tarball: https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz} + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==, tarball: https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz} engines: {node: '>=8'} detect-newline@3.1.0: @@ -3684,10 +3655,6 @@ packages: engines: {node: '>=18'} hasBin: true - escalade@3.1.2: - resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==, tarball: https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz} - engines: {node: '>=6'} - escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==, tarball: https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz} engines: {node: '>=6'} @@ -3727,7 +3694,6 @@ packages: eslint@8.52.0: resolution: {integrity: sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==, tarball: https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true espree@9.6.1: @@ -3790,8 +3756,8 @@ packages: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==, tarball: https://registry.npmjs.org/expect/-/expect-29.7.0.tgz} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - express@4.21.0: - resolution: {integrity: sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==, tarball: https://registry.npmjs.org/express/-/express-4.21.0.tgz} + express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==, tarball: https://registry.npmjs.org/express/-/express-4.21.2.tgz} engines: {node: '>= 0.10.0'} extend@3.0.2: @@ -3808,14 +3774,18 @@ packages: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==, tarball: https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz} engines: {node: '>=8.6.0'} + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==, tarball: https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz} + engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==, tarball: https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz} fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==, tarball: https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz} - fastq@1.17.1: - resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==, tarball: https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz} + fastq@1.19.0: + resolution: {integrity: sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==, tarball: https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz} fault@1.0.4: resolution: {integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==, tarball: https://registry.npmjs.org/fault/-/fault-1.0.4.tgz} @@ -3877,8 +3847,8 @@ packages: resolution: {integrity: sha512-kKaIINnFpzW6ffJNDjjyjrk21BkDx38c0xa/klsT8VzLCaMEefv4ZTacrcVR4DmgTeBra++jMDAfS/tS799YDw==, tarball: https://registry.npmjs.org/for-each/-/for-each-0.3.4.tgz} engines: {node: '>= 0.4'} - foreground-child@3.1.1: - resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==, tarball: https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz} + foreground-child@3.3.0: + resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==, tarball: https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz} engines: {node: '>=14'} form-data@4.0.1: @@ -3977,9 +3947,8 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==, tarball: https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz} engines: {node: '>=10.13.0'} - glob@10.3.10: - resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==, tarball: https://registry.npmjs.org/glob/-/glob-10.3.10.tgz} - engines: {node: '>=16 || 14 >=14.17'} + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==, tarball: https://registry.npmjs.org/glob/-/glob-10.4.5.tgz} hasBin: true glob@7.2.3: @@ -4121,6 +4090,10 @@ packages: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==, tarball: https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz} engines: {node: '>=6'} + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==, tarball: https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz} + engines: {node: '>=6'} + import-local@3.2.0: resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==, tarball: https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz} engines: {node: '>=8'} @@ -4202,8 +4175,8 @@ packages: is-core-module@2.13.1: resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==, tarball: https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz} - is-core-module@2.16.0: - resolution: {integrity: sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g==, tarball: https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.0.tgz} + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==, tarball: https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz} engines: {node: '>= 0.4'} is-date-object@1.0.5: @@ -4349,9 +4322,8 @@ packages: resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==, tarball: https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz} engines: {node: '>=8'} - jackspeak@2.3.6: - resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==, tarball: https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz} - engines: {node: '>=14'} + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==, tarball: https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz} jest-canvas-mock@2.5.2: resolution: {integrity: sha512-vgnpPupjOL6+L5oJXzxTxFrlGEIbHdZqFU+LFNdtLxZ3lRDCl17FlTMM7IatoRQkrcyOTMlDinjUguqmQ6bR2A==, tarball: https://registry.npmjs.org/jest-canvas-mock/-/jest-canvas-mock-2.5.2.tgz} @@ -4527,8 +4499,8 @@ packages: '@swc/core': ^1.3.3 '@swc/jest': ^0.2.22 - jiti@1.21.6: - resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==, tarball: https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz} + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==, tarball: https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz} hasBin: true js-tokens@4.0.0: @@ -4604,12 +4576,8 @@ packages: lie@3.3.0: resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==, tarball: https://registry.npmjs.org/lie/-/lie-3.3.0.tgz} - lilconfig@2.1.0: - resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==, tarball: https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz} - engines: {node: '>=10'} - - lilconfig@3.1.2: - resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==, tarball: https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz} + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==, tarball: https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz} engines: {node: '>=14'} lines-and-columns@1.2.4: @@ -4645,6 +4613,9 @@ packages: loupe@3.1.2: resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==, tarball: https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz} + loupe@3.1.3: + resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==, tarball: https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz} + lowlight@1.20.0: resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==, tarball: https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz} @@ -4938,10 +4909,6 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==, tarball: https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz} engines: {node: '>=6'} - mimic-response@2.1.0: - resolution: {integrity: sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==, tarball: https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz} - engines: {node: '>=8'} - mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==, tarball: https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz} engines: {node: '>=10'} @@ -4960,8 +4927,8 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==, tarball: https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz} - minipass@7.0.4: - resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==, tarball: https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz} + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==, tarball: https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz} engines: {node: '>=16 || 14 >=14.17'} mkdirp-classic@0.5.3: @@ -5013,8 +4980,8 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - napi-build-utils@1.0.2: - resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==, tarball: https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz} + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==, tarball: https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz} natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==, tarball: https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz} @@ -5023,8 +4990,8 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==, tarball: https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz} engines: {node: '>= 0.6'} - node-abi@3.65.0: - resolution: {integrity: sha512-ThjYBfoDNr08AWx6hGaRbfPwxKV9kVzAzOzlLKbk2CuqXE2xnCh+cbAGnwM3t8Lq4v9rUB7VfondlkBckcJrVA==, tarball: https://registry.npmjs.org/node-abi/-/node-abi-3.65.0.tgz} + node-abi@3.74.0: + resolution: {integrity: sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==, tarball: https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz} engines: {node: '>=10'} node-addon-api@7.1.1: @@ -5062,8 +5029,9 @@ packages: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==, tarball: https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz} engines: {node: '>= 6'} - object-inspect@1.13.1: - resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==, tarball: https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz} + object-inspect@1.13.3: + resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==, tarball: https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz} + engines: {node: '>= 0.4'} object-is@1.1.5: resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==, tarball: https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz} @@ -5119,6 +5087,9 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==, tarball: https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz} engines: {node: '>=6'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==, tarball: https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz} + pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==, tarball: https://registry.npmjs.org/pako/-/pako-1.0.11.tgz} @@ -5161,12 +5132,12 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==, tarball: https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz} - path-scurry@1.10.1: - resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==, tarball: https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz} - engines: {node: '>=16 || 14 >=14.17'} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==, tarball: https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz} + engines: {node: '>=16 || 14 >=14.18'} - path-to-regexp@0.1.10: - resolution: {integrity: sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==, tarball: https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz} + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==, tarball: https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz} path-to-regexp@6.2.1: resolution: {integrity: sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==, tarball: https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz} @@ -5179,9 +5150,6 @@ packages: resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==, tarball: https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz} engines: {node: '>= 14.16'} - picocolors@1.1.0: - resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==, tarball: https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==, tarball: https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz} @@ -5189,6 +5157,10 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==, tarball: https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz} engines: {node: '>=8.6'} + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==, tarball: https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz} + engines: {node: '>=12'} + pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==, tarball: https://registry.npmjs.org/pify/-/pify-2.3.0.tgz} engines: {node: '>=0.10.0'} @@ -5211,8 +5183,8 @@ packages: engines: {node: '>=18'} hasBin: true - polished@4.2.2: - resolution: {integrity: sha512-Sz2Lkdxz6F2Pgnpi9U5Ng/WdWAUZxmHrNPoVlm3aAemxoy2Qy7LGjQg4uf8qKelDAUW94F4np3iH2YPf2qefcQ==, tarball: https://registry.npmjs.org/polished/-/polished-4.2.2.tgz} + polished@4.3.1: + resolution: {integrity: sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==, tarball: https://registry.npmjs.org/polished/-/polished-4.3.1.tgz} engines: {node: '>=10'} possible-typed-array-names@1.0.0: @@ -5256,12 +5228,12 @@ packages: postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==, tarball: https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz} - postcss@8.4.47: - resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==, tarball: https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz} + postcss@8.5.1: + resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==, tarball: https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz} engines: {node: ^10 || ^12 || >=14} - prebuild-install@7.1.2: - resolution: {integrity: sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==, tarball: https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz} + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==, tarball: https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz} engines: {node: '>=10'} hasBin: true @@ -5308,8 +5280,8 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==, tarball: https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz} - property-expr@2.0.5: - resolution: {integrity: sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==, tarball: https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz} + property-expr@2.0.6: + resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==, tarball: https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz} property-information@5.6.0: resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==, tarball: https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz} @@ -5331,8 +5303,8 @@ packages: psl@1.9.0: resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==, tarball: https://registry.npmjs.org/psl/-/psl-1.9.0.tgz} - pump@3.0.0: - resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==, tarball: https://registry.npmjs.org/pump/-/pump-3.0.0.tgz} + pump@3.0.2: + resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==, tarball: https://registry.npmjs.org/pump/-/pump-3.0.2.tgz} punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==, tarball: https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz} @@ -5363,11 +5335,11 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==, tarball: https://registry.npmjs.org/rc/-/rc-1.2.8.tgz} hasBin: true - react-chartjs-2@5.2.0: - resolution: {integrity: sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==, tarball: https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz} + react-chartjs-2@5.3.0: + resolution: {integrity: sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==, tarball: https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz} peerDependencies: chart.js: ^4.1.1 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-color@2.19.3: resolution: {integrity: sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==, tarball: https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz} @@ -5449,16 +5421,6 @@ packages: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==, tarball: https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz} engines: {node: '>=0.10.0'} - react-remove-scroll-bar@2.3.6: - resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==, tarball: https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz} - engines: {node: '>=10'} - peerDependencies: - '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==, tarball: https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz} engines: {node: '>=10'} @@ -5469,18 +5431,18 @@ packages: '@types/react': optional: true - react-remove-scroll@2.6.0: - resolution: {integrity: sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==, tarball: https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz} + react-remove-scroll@2.6.2: + resolution: {integrity: sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==, tarball: https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.2.tgz} engines: {node: '>=10'} peerDependencies: - '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': optional: true - react-remove-scroll@2.6.2: - resolution: {integrity: sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==, tarball: https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.2.tgz} + react-remove-scroll@2.6.3: + resolution: {integrity: sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==, tarball: https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz} engines: {node: '>=10'} peerDependencies: '@types/react': '*' @@ -5508,16 +5470,6 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-style-singleton@2.2.1: - resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==, tarball: https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz} - engines: {node: '>=10'} - peerDependencies: - '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - react-style-singleton@2.2.3: resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==, tarball: https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz} engines: {node: '>=10'} @@ -5545,12 +5497,12 @@ packages: react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 - react-window@1.8.10: - resolution: {integrity: sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==, tarball: https://registry.npmjs.org/react-window/-/react-window-1.8.10.tgz} + react-window@1.8.11: + resolution: {integrity: sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==, tarball: https://registry.npmjs.org/react-window/-/react-window-1.8.11.tgz} engines: {node: '>8.0.0'} peerDependencies: - react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 - react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==, tarball: https://registry.npmjs.org/react/-/react-18.3.1.tgz} @@ -5647,12 +5599,13 @@ packages: resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==, tarball: https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz} engines: {node: '>=10'} - resolve@1.22.8: - resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==, tarball: https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz} + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==, tarball: https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz} + engines: {node: '>= 0.4'} hasBin: true - resolve@1.22.9: - resolution: {integrity: sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A==, tarball: https://registry.npmjs.org/resolve/-/resolve-1.22.9.tgz} + resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==, tarball: https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz} hasBin: true reusify@1.0.4: @@ -5664,13 +5617,16 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rollup-plugin-visualizer@5.12.0: - resolution: {integrity: sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==, tarball: https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.12.0.tgz} - engines: {node: '>=14'} + rollup-plugin-visualizer@5.14.0: + resolution: {integrity: sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA==, tarball: https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.14.0.tgz} + engines: {node: '>=18'} hasBin: true peerDependencies: + rolldown: 1.x rollup: 2.x || 3.x || 4.x peerDependenciesMeta: + rolldown: + optional: true rollup: optional: true @@ -5750,8 +5706,20 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==, tarball: https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz} engines: {node: '>=8'} - side-channel@1.0.6: - resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==, tarball: https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz} + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==, tarball: https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==, tarball: https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==, tarball: https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==, tarball: https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz} engines: {node: '>= 0.4'} signal-exit@3.0.7: @@ -5764,9 +5732,6 @@ packages: simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==, tarball: https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz} - simple-get@3.1.1: - resolution: {integrity: sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==, tarball: https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz} - simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==, tarball: https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz} @@ -5849,8 +5814,8 @@ packages: react: '>=18' react-dom: '>=18' - storybook@8.5.2: - resolution: {integrity: sha512-pf84emQ7Pd5jBdT2gzlNs4kRaSI3pq0Lh8lSfV+YqIVXztXIHU+Lqyhek2Lhjb7btzA1tExrhJrgQUsIji7i7A==, tarball: https://registry.npmjs.org/storybook/-/storybook-8.5.2.tgz} + storybook@8.5.3: + resolution: {integrity: sha512-2WtNBZ45u1AhviRU+U+ld588tH8gDa702dNSq5C8UBaE9PlOsazGsyp90dw1s9YRvi+ejrjKAupQAU0GwwUiVg==, tarball: https://registry.npmjs.org/storybook/-/storybook-8.5.3.tgz} hasBin: true peerDependencies: prettier: ^2 || ^3 @@ -5952,21 +5917,21 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==, tarball: https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz} - tailwind-merge@2.5.4: - resolution: {integrity: sha512-0q8cfZHMu9nuYP/b5Shb7Y7Sh1B7Nnl5GqNr1U+n2p6+mybvRtayrQ+0042Z5byvTA8ihjlP8Odo8/VnHbZu4Q==, tarball: https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.4.tgz} + tailwind-merge@2.6.0: + resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==, tarball: https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz} tailwindcss-animate@1.0.7: resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==, tarball: https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz} peerDependencies: tailwindcss: '>=3.0.0 || insiders' - tailwindcss@3.4.13: - resolution: {integrity: sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==, tarball: https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.13.tgz} + tailwindcss@3.4.17: + resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==, tarball: https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz} engines: {node: '>=14.0.0'} hasBin: true - tar-fs@2.1.1: - resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==, tarball: https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz} + tar-fs@2.1.2: + resolution: {integrity: sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==, tarball: https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz} tar-stream@2.2.0: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==, tarball: https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz} @@ -6058,8 +6023,8 @@ packages: ts-morph@13.0.3: resolution: {integrity: sha512-pSOfUMx8Ld/WUreoSzvMFQG5i9uEiWIsBYjpU9+TTASOeUa89j5HykomeqVULm1oqWtBdleI3KEFRLrlA3zGIw==, tarball: https://registry.npmjs.org/ts-morph/-/ts-morph-13.0.3.tgz} - ts-node@10.9.1: - resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==, tarball: https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz} + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==, tarball: https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz} hasBin: true peerDependencies: '@swc/core': '>=1.2.50' @@ -6144,8 +6109,9 @@ packages: tzdata@1.0.40: resolution: {integrity: sha512-IsWNGfC5GrVPG4ejYJtf3tOlBdJYs0uNzv1a+vkdANHDq2kPg4oAN2UlCfpqrCwErPZVhI6MLA2gkeuXAVnpLg==, tarball: https://registry.npmjs.org/tzdata/-/tzdata-1.0.40.tgz} - ua-parser-js@1.0.33: - resolution: {integrity: sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ==, tarball: https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.33.tgz} + ua-parser-js@1.0.40: + resolution: {integrity: sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==, tarball: https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz} + hasBin: true undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==, tarball: https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz} @@ -6209,16 +6175,6 @@ packages: url-parse@1.5.10: resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==, tarball: https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz} - use-callback-ref@1.3.2: - resolution: {integrity: sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==, tarball: https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz} - engines: {node: '>=10'} - peerDependencies: - '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - use-callback-ref@1.3.3: resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==, tarball: https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz} engines: {node: '>=10'} @@ -6239,6 +6195,16 @@ packages: '@types/react': optional: true + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==, tarball: https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + use-sync-external-store@1.2.0: resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==, tarball: https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz} peerDependencies: @@ -6485,8 +6451,8 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==, tarball: https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz} engines: {node: '>= 6'} - yaml@2.6.0: - resolution: {integrity: sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==, tarball: https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz} + yaml@2.7.0: + resolution: {integrity: sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==, tarball: https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz} engines: {node: '>= 14'} hasBin: true @@ -6506,8 +6472,8 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==, tarball: https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz} engines: {node: '>=10'} - yup@1.4.0: - resolution: {integrity: sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg==, tarball: https://registry.npmjs.org/yup/-/yup-1.4.0.tgz} + yup@1.6.1: + resolution: {integrity: sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA==, tarball: https://registry.npmjs.org/yup/-/yup-1.6.1.tgz} zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==, tarball: https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz} @@ -6517,7 +6483,7 @@ snapshots: '@aashutoshrathi/word-wrap@1.2.6': optional: true - '@adobe/css-tools@4.4.0': {} + '@adobe/css-tools@4.4.1': {} '@alloc/quick-lru@5.2.0': {} @@ -6529,7 +6495,7 @@ snapshots: '@babel/code-frame@7.25.7': dependencies: '@babel/highlight': 7.25.7 - picocolors: 1.1.0 + picocolors: 1.1.1 '@babel/code-frame@7.26.2': dependencies: @@ -6611,7 +6577,7 @@ snapshots: '@babel/helper-validator-identifier': 7.25.7 chalk: 2.4.2 js-tokens: 4.0.0 - picocolors: 1.1.0 + picocolors: 1.1.1 '@babel/parser@7.26.3': dependencies: @@ -6716,10 +6682,6 @@ snapshots: dependencies: regenerator-runtime: 0.13.11 - '@babel/runtime@7.24.7': - dependencies: - regenerator-runtime: 0.14.1 - '@babel/runtime@7.25.6': dependencies: regenerator-runtime: 0.14.1 @@ -6822,13 +6784,13 @@ snapshots: '@types/tough-cookie': 4.0.5 tough-cookie: 4.1.4 - '@chromatic-com/storybook@3.2.2(react@18.3.1)(storybook@8.5.2(prettier@3.4.1))': + '@chromatic-com/storybook@3.2.2(react@18.3.1)(storybook@8.5.3(prettier@3.4.1))': dependencies: - chromatic: 11.16.3 + chromatic: 11.25.2 filesize: 10.1.2 jsonfile: 6.1.0 react-confetti: 6.2.2(react@18.3.1) - storybook: 8.5.2(prettier@3.4.1) + storybook: 8.5.3(prettier@3.4.1) strip-ansi: 7.1.0 transitivePeerDependencies: - '@chromatic-com/cypress' @@ -7099,7 +7061,7 @@ snapshots: espree: 9.6.1 globals: 13.24.0 ignore: 5.3.2 - import-fresh: 3.3.0 + import-fresh: 3.3.1 js-yaml: 4.1.0 minimatch: 3.1.2 strip-json-comments: 3.1.1 @@ -7114,26 +7076,26 @@ snapshots: dependencies: tslib: 2.6.1 - '@floating-ui/core@1.6.8': + '@floating-ui/core@1.6.9': dependencies: - '@floating-ui/utils': 0.2.8 + '@floating-ui/utils': 0.2.9 - '@floating-ui/dom@1.6.12': + '@floating-ui/dom@1.6.13': dependencies: - '@floating-ui/core': 1.6.8 - '@floating-ui/utils': 0.2.8 + '@floating-ui/core': 1.6.9 + '@floating-ui/utils': 0.2.9 '@floating-ui/react-dom@2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@floating-ui/dom': 1.6.12 + '@floating-ui/dom': 1.6.13 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@floating-ui/utils@0.2.8': {} + '@floating-ui/utils@0.2.9': {} - '@fontsource-variable/inter@5.0.15': {} + '@fontsource-variable/inter@5.1.1': {} - '@fontsource/ibm-plex-mono@5.1.0': {} + '@fontsource/ibm-plex-mono@5.1.1': {} '@humanwhocodes/config-array@0.11.14': dependencies: @@ -7208,7 +7170,7 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3))': + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -7222,7 +7184,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.17.16)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)) + jest-config: 29.7.0(@types/node@20.17.16)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -7398,12 +7360,6 @@ snapshots: optionalDependencies: typescript: 5.6.3 - '@jridgewell/gen-mapping@0.3.5': - dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping': 0.3.25 - '@jridgewell/gen-mapping@0.3.8': dependencies: '@jridgewell/set-array': 1.2.1 @@ -7414,8 +7370,6 @@ snapshots: '@jridgewell/set-array@1.2.1': {} - '@jridgewell/sourcemap-codec@1.4.15': {} - '@jridgewell/sourcemap-codec@1.5.0': {} '@jridgewell/trace-mapping@0.3.25': @@ -7426,7 +7380,7 @@ snapshots: '@jridgewell/trace-mapping@0.3.9': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/sourcemap-codec': 1.5.0 '@kurkle/color@0.3.2': {} @@ -7577,7 +7531,7 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 - '@mui/x-internals@7.24.1(@types/react@18.3.12)(react@18.3.1)': + '@mui/x-internals@7.25.0(@types/react@18.3.12)(react@18.3.1)': dependencies: '@babel/runtime': 7.26.7 '@mui/utils': 5.16.14(@types/react@18.3.12)(react@18.3.1) @@ -7585,13 +7539,13 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@mui/x-tree-view@7.24.1(@emotion/react@11.14.0(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@mui/material@5.16.14(@emotion/react@11.14.0(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@5.16.14(@emotion/react@11.14.0(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@mui/x-tree-view@7.25.0(@emotion/react@11.14.0(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@mui/material@5.16.14(@emotion/react@11.14.0(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@5.16.14(@emotion/react@11.14.0(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.26.7 '@mui/material': 5.16.14(@emotion/react@11.14.0(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mui/system': 5.16.14(@emotion/react@11.14.0(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1) '@mui/utils': 5.16.14(@types/react@18.3.12)(react@18.3.1) - '@mui/x-internals': 7.24.1(@types/react@18.3.12)(react@18.3.1) + '@mui/x-internals': 7.25.0(@types/react@18.3.12)(react@18.3.1) '@types/react-transition-group': 4.4.12(@types/react@18.3.12) clsx: 2.1.1 prop-types: 15.8.1 @@ -7614,7 +7568,7 @@ snapshots: '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.17.1 + fastq: 1.19.0 '@octokit/openapi-types@19.0.2': {} @@ -7706,18 +7660,6 @@ snapshots: '@types/react': 18.3.12 '@types/react-dom': 18.3.1 - '@radix-ui/react-collection@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) - '@radix-ui/react-context': 1.1.0(@types/react@18.3.12)(react@18.3.1) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.1.0(@types/react@18.3.12)(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - optionalDependencies: - '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 - '@radix-ui/react-collection@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.12)(react@18.3.1) @@ -7742,12 +7684,6 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 - '@radix-ui/react-context@1.1.0(@types/react@18.3.12)(react@18.3.1)': - dependencies: - react: 18.3.1 - optionalDependencies: - '@types/react': 18.3.12 - '@radix-ui/react-context@1.1.1(@types/react@18.3.12)(react@18.3.1)': dependencies: react: 18.3.1 @@ -7782,7 +7718,7 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 - '@radix-ui/react-dismissable-layer@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-dismissable-layer@1.1.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.1 '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.12)(react@18.3.1) @@ -7795,7 +7731,7 @@ snapshots: '@types/react': 18.3.12 '@types/react-dom': 18.3.1 - '@radix-ui/react-dismissable-layer@1.1.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-dismissable-layer@1.1.4(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.1 '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.12)(react@18.3.1) @@ -7877,17 +7813,17 @@ snapshots: aria-hidden: 1.2.4 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.6.2(@types/react@18.3.12)(react@18.3.1) + react-remove-scroll: 2.6.3(@types/react@18.3.12)(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 '@types/react-dom': 18.3.1 - '@radix-ui/react-popover@1.1.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-popover@1.1.5(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.1 '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.4(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-focus-scope': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-id': 1.1.0(@types/react@18.3.12)(react@18.3.1) @@ -7900,7 +7836,7 @@ snapshots: aria-hidden: 1.2.4 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.6.0(@types/react@18.3.12)(react@18.3.1) + react-remove-scroll: 2.6.3(@types/react@18.3.12)(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 '@types/react-dom': 18.3.1 @@ -8007,15 +7943,15 @@ snapshots: '@types/react': 18.3.12 '@types/react-dom': 18.3.1 - '@radix-ui/react-slider@1.2.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-slider@1.2.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/number': 1.1.0 - '@radix-ui/primitive': 1.1.0 - '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-collection': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-direction': 1.1.0(@types/react@18.3.12)(react@18.3.1) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.12)(react@18.3.1) @@ -8055,6 +7991,26 @@ snapshots: '@types/react': 18.3.12 '@types/react-dom': 18.3.1 + '@radix-ui/react-tooltip@1.1.7(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.4(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-popper': 1.2.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + '@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.12)(react@18.3.1)': dependencies: react: 18.3.1 @@ -8198,130 +8154,139 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.0 - '@storybook/addon-actions@8.4.6(storybook@8.5.2(prettier@3.4.1))': + '@storybook/addon-actions@8.4.6(storybook@8.5.3(prettier@3.4.1))': + dependencies: + '@storybook/global': 5.0.0 + '@types/uuid': 9.0.2 + dequal: 2.0.3 + polished: 4.3.1 + storybook: 8.5.3(prettier@3.4.1) + uuid: 9.0.1 + + '@storybook/addon-actions@8.5.2(storybook@8.5.3(prettier@3.4.1))': dependencies: '@storybook/global': 5.0.0 '@types/uuid': 9.0.2 dequal: 2.0.3 - polished: 4.2.2 - storybook: 8.5.2(prettier@3.4.1) + polished: 4.3.1 + storybook: 8.5.3(prettier@3.4.1) uuid: 9.0.1 - '@storybook/addon-backgrounds@8.4.6(storybook@8.5.2(prettier@3.4.1))': + '@storybook/addon-backgrounds@8.4.6(storybook@8.5.3(prettier@3.4.1))': dependencies: '@storybook/global': 5.0.0 memoizerific: 1.11.3 - storybook: 8.5.2(prettier@3.4.1) + storybook: 8.5.3(prettier@3.4.1) ts-dedent: 2.2.0 - '@storybook/addon-controls@8.4.6(storybook@8.5.2(prettier@3.4.1))': + '@storybook/addon-controls@8.4.6(storybook@8.5.3(prettier@3.4.1))': dependencies: '@storybook/global': 5.0.0 dequal: 2.0.3 - storybook: 8.5.2(prettier@3.4.1) + storybook: 8.5.3(prettier@3.4.1) ts-dedent: 2.2.0 - '@storybook/addon-docs@8.4.6(@types/react@18.3.12)(storybook@8.5.2(prettier@3.4.1))': + '@storybook/addon-docs@8.4.6(@types/react@18.3.12)(storybook@8.5.3(prettier@3.4.1))': dependencies: '@mdx-js/react': 3.0.1(@types/react@18.3.12)(react@18.3.1) - '@storybook/blocks': 8.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.2(prettier@3.4.1)) - '@storybook/csf-plugin': 8.4.6(storybook@8.5.2(prettier@3.4.1)) - '@storybook/react-dom-shim': 8.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.2(prettier@3.4.1)) + '@storybook/blocks': 8.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.3(prettier@3.4.1)) + '@storybook/csf-plugin': 8.4.6(storybook@8.5.3(prettier@3.4.1)) + '@storybook/react-dom-shim': 8.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.3(prettier@3.4.1)) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 8.5.2(prettier@3.4.1) + storybook: 8.5.3(prettier@3.4.1) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' - '@storybook/addon-essentials@8.4.6(@types/react@18.3.12)(storybook@8.5.2(prettier@3.4.1))': - dependencies: - '@storybook/addon-actions': 8.4.6(storybook@8.5.2(prettier@3.4.1)) - '@storybook/addon-backgrounds': 8.4.6(storybook@8.5.2(prettier@3.4.1)) - '@storybook/addon-controls': 8.4.6(storybook@8.5.2(prettier@3.4.1)) - '@storybook/addon-docs': 8.4.6(@types/react@18.3.12)(storybook@8.5.2(prettier@3.4.1)) - '@storybook/addon-highlight': 8.4.6(storybook@8.5.2(prettier@3.4.1)) - '@storybook/addon-measure': 8.4.6(storybook@8.5.2(prettier@3.4.1)) - '@storybook/addon-outline': 8.4.6(storybook@8.5.2(prettier@3.4.1)) - '@storybook/addon-toolbars': 8.4.6(storybook@8.5.2(prettier@3.4.1)) - '@storybook/addon-viewport': 8.4.6(storybook@8.5.2(prettier@3.4.1)) - storybook: 8.5.2(prettier@3.4.1) + '@storybook/addon-essentials@8.4.6(@types/react@18.3.12)(storybook@8.5.3(prettier@3.4.1))': + dependencies: + '@storybook/addon-actions': 8.4.6(storybook@8.5.3(prettier@3.4.1)) + '@storybook/addon-backgrounds': 8.4.6(storybook@8.5.3(prettier@3.4.1)) + '@storybook/addon-controls': 8.4.6(storybook@8.5.3(prettier@3.4.1)) + '@storybook/addon-docs': 8.4.6(@types/react@18.3.12)(storybook@8.5.3(prettier@3.4.1)) + '@storybook/addon-highlight': 8.4.6(storybook@8.5.3(prettier@3.4.1)) + '@storybook/addon-measure': 8.4.6(storybook@8.5.3(prettier@3.4.1)) + '@storybook/addon-outline': 8.4.6(storybook@8.5.3(prettier@3.4.1)) + '@storybook/addon-toolbars': 8.4.6(storybook@8.5.3(prettier@3.4.1)) + '@storybook/addon-viewport': 8.4.6(storybook@8.5.3(prettier@3.4.1)) + storybook: 8.5.3(prettier@3.4.1) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' - '@storybook/addon-highlight@8.4.6(storybook@8.5.2(prettier@3.4.1))': + '@storybook/addon-highlight@8.4.6(storybook@8.5.3(prettier@3.4.1))': dependencies: '@storybook/global': 5.0.0 - storybook: 8.5.2(prettier@3.4.1) + storybook: 8.5.3(prettier@3.4.1) - '@storybook/addon-interactions@8.4.6(storybook@8.5.2(prettier@3.4.1))': + '@storybook/addon-interactions@8.5.3(storybook@8.5.3(prettier@3.4.1))': dependencies: '@storybook/global': 5.0.0 - '@storybook/instrumenter': 8.4.6(storybook@8.5.2(prettier@3.4.1)) - '@storybook/test': 8.4.6(storybook@8.5.2(prettier@3.4.1)) - polished: 4.2.2 - storybook: 8.5.2(prettier@3.4.1) + '@storybook/instrumenter': 8.5.3(storybook@8.5.3(prettier@3.4.1)) + '@storybook/test': 8.5.3(storybook@8.5.3(prettier@3.4.1)) + polished: 4.3.1 + storybook: 8.5.3(prettier@3.4.1) ts-dedent: 2.2.0 - '@storybook/addon-links@8.4.6(react@18.3.1)(storybook@8.5.2(prettier@3.4.1))': + '@storybook/addon-links@8.5.2(react@18.3.1)(storybook@8.5.3(prettier@3.4.1))': dependencies: - '@storybook/csf': 0.1.11 + '@storybook/csf': 0.1.12 '@storybook/global': 5.0.0 - storybook: 8.5.2(prettier@3.4.1) + storybook: 8.5.3(prettier@3.4.1) ts-dedent: 2.2.0 optionalDependencies: react: 18.3.1 - '@storybook/addon-mdx-gfm@8.4.6(storybook@8.5.2(prettier@3.4.1))': + '@storybook/addon-mdx-gfm@8.5.2(storybook@8.5.3(prettier@3.4.1))': dependencies: remark-gfm: 4.0.0 - storybook: 8.5.2(prettier@3.4.1) + storybook: 8.5.3(prettier@3.4.1) ts-dedent: 2.2.0 transitivePeerDependencies: - supports-color - '@storybook/addon-measure@8.4.6(storybook@8.5.2(prettier@3.4.1))': + '@storybook/addon-measure@8.4.6(storybook@8.5.3(prettier@3.4.1))': dependencies: '@storybook/global': 5.0.0 - storybook: 8.5.2(prettier@3.4.1) + storybook: 8.5.3(prettier@3.4.1) tiny-invariant: 1.3.3 - '@storybook/addon-outline@8.4.6(storybook@8.5.2(prettier@3.4.1))': + '@storybook/addon-outline@8.4.6(storybook@8.5.3(prettier@3.4.1))': dependencies: '@storybook/global': 5.0.0 - storybook: 8.5.2(prettier@3.4.1) + storybook: 8.5.3(prettier@3.4.1) ts-dedent: 2.2.0 - '@storybook/addon-themes@8.4.6(storybook@8.5.2(prettier@3.4.1))': + '@storybook/addon-themes@8.4.6(storybook@8.5.3(prettier@3.4.1))': dependencies: - storybook: 8.5.2(prettier@3.4.1) + storybook: 8.5.3(prettier@3.4.1) ts-dedent: 2.2.0 - '@storybook/addon-toolbars@8.4.6(storybook@8.5.2(prettier@3.4.1))': + '@storybook/addon-toolbars@8.4.6(storybook@8.5.3(prettier@3.4.1))': dependencies: - storybook: 8.5.2(prettier@3.4.1) + storybook: 8.5.3(prettier@3.4.1) - '@storybook/addon-viewport@8.4.6(storybook@8.5.2(prettier@3.4.1))': + '@storybook/addon-viewport@8.4.6(storybook@8.5.3(prettier@3.4.1))': dependencies: memoizerific: 1.11.3 - storybook: 8.5.2(prettier@3.4.1) + storybook: 8.5.3(prettier@3.4.1) - '@storybook/blocks@8.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.2(prettier@3.4.1))': + '@storybook/blocks@8.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.3(prettier@3.4.1))': dependencies: '@storybook/csf': 0.1.13 '@storybook/icons': 1.2.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - storybook: 8.5.2(prettier@3.4.1) + storybook: 8.5.3(prettier@3.4.1) ts-dedent: 2.2.0 optionalDependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/builder-vite@8.4.6(storybook@8.5.2(prettier@3.4.1))(vite@5.4.14(@types/node@20.17.16))': + '@storybook/builder-vite@8.4.6(storybook@8.5.3(prettier@3.4.1))(vite@5.4.14(@types/node@20.17.16))': dependencies: - '@storybook/csf-plugin': 8.4.6(storybook@8.5.2(prettier@3.4.1)) + '@storybook/csf-plugin': 8.4.6(storybook@8.5.3(prettier@3.4.1)) browser-assert: 1.2.1 - storybook: 8.5.2(prettier@3.4.1) + storybook: 8.5.3(prettier@3.4.1) ts-dedent: 2.2.0 vite: 5.4.14(@types/node@20.17.16) @@ -8337,16 +8302,16 @@ snapshots: dependencies: '@storybook/global': 5.0.0 - '@storybook/components@8.4.6(storybook@8.5.2(prettier@3.4.1))': + '@storybook/components@8.4.6(storybook@8.5.3(prettier@3.4.1))': dependencies: - storybook: 8.5.2(prettier@3.4.1) + storybook: 8.5.3(prettier@3.4.1) '@storybook/core-events@8.1.11': dependencies: '@storybook/csf': 0.1.13 ts-dedent: 2.2.0 - '@storybook/core@8.5.2(prettier@3.4.1)': + '@storybook/core@8.5.3(prettier@3.4.1)': dependencies: '@storybook/csf': 0.1.12 better-opn: 3.0.2 @@ -8366,9 +8331,9 @@ snapshots: - supports-color - utf-8-validate - '@storybook/csf-plugin@8.4.6(storybook@8.5.2(prettier@3.4.1))': + '@storybook/csf-plugin@8.4.6(storybook@8.5.3(prettier@3.4.1))': dependencies: - storybook: 8.5.2(prettier@3.4.1) + storybook: 8.5.3(prettier@3.4.1) unplugin: 1.5.0 '@storybook/csf@0.1.11': @@ -8390,43 +8355,49 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/instrumenter@8.4.6(storybook@8.5.2(prettier@3.4.1))': + '@storybook/instrumenter@8.4.6(storybook@8.5.3(prettier@3.4.1))': + dependencies: + '@storybook/global': 5.0.0 + '@vitest/utils': 2.1.8 + storybook: 8.5.3(prettier@3.4.1) + + '@storybook/instrumenter@8.5.3(storybook@8.5.3(prettier@3.4.1))': dependencies: '@storybook/global': 5.0.0 '@vitest/utils': 2.1.8 - storybook: 8.5.2(prettier@3.4.1) + storybook: 8.5.3(prettier@3.4.1) - '@storybook/manager-api@8.4.6(storybook@8.5.2(prettier@3.4.1))': + '@storybook/manager-api@8.4.6(storybook@8.5.3(prettier@3.4.1))': dependencies: - storybook: 8.5.2(prettier@3.4.1) + storybook: 8.5.3(prettier@3.4.1) - '@storybook/preview-api@8.4.6(storybook@8.5.2(prettier@3.4.1))': + '@storybook/preview-api@8.4.6(storybook@8.5.3(prettier@3.4.1))': dependencies: - storybook: 8.5.2(prettier@3.4.1) + storybook: 8.5.3(prettier@3.4.1) - '@storybook/preview-api@8.4.7(storybook@8.5.2(prettier@3.4.1))': + '@storybook/preview-api@8.5.3(storybook@8.5.3(prettier@3.4.1))': dependencies: - storybook: 8.5.2(prettier@3.4.1) + storybook: 8.5.3(prettier@3.4.1) - '@storybook/react-dom-shim@8.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.2(prettier@3.4.1))': + '@storybook/react-dom-shim@8.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.3(prettier@3.4.1))': dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 8.5.2(prettier@3.4.1) + storybook: 8.5.3(prettier@3.4.1) - '@storybook/react-vite@8.4.6(@storybook/test@8.4.6(storybook@8.5.2(prettier@3.4.1)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.32.0)(storybook@8.5.2(prettier@3.4.1))(typescript@5.6.3)(vite@5.4.14(@types/node@20.17.16))': + '@storybook/react-vite@8.4.6(@storybook/test@8.4.6(storybook@8.5.3(prettier@3.4.1)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.32.0)(storybook@8.5.3(prettier@3.4.1))(typescript@5.6.3)(vite@5.4.14(@types/node@20.17.16))': dependencies: '@joshwooding/vite-plugin-react-docgen-typescript': 0.4.2(typescript@5.6.3)(vite@5.4.14(@types/node@20.17.16)) '@rollup/pluginutils': 5.0.5(rollup@4.32.0) - '@storybook/builder-vite': 8.4.6(storybook@8.5.2(prettier@3.4.1))(vite@5.4.14(@types/node@20.17.16)) - '@storybook/react': 8.4.6(@storybook/test@8.4.6(storybook@8.5.2(prettier@3.4.1)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.2(prettier@3.4.1))(typescript@5.6.3) + '@storybook/builder-vite': 8.4.6(storybook@8.5.3(prettier@3.4.1))(vite@5.4.14(@types/node@20.17.16)) + '@storybook/react': 8.4.6(@storybook/test@8.4.6(storybook@8.5.3(prettier@3.4.1)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.3(prettier@3.4.1))(typescript@5.6.3) find-up: 5.0.0 magic-string: 0.30.5 react: 18.3.1 react-docgen: 7.0.3 react-dom: 18.3.1(react@18.3.1) resolve: 1.22.8 - storybook: 8.5.2(prettier@3.4.1) + storybook: 8.5.3(prettier@3.4.1) tsconfig-paths: 4.2.0 vite: 5.4.14(@types/node@20.17.16) transitivePeerDependencies: @@ -8435,36 +8406,48 @@ snapshots: - supports-color - typescript - '@storybook/react@8.4.6(@storybook/test@8.4.6(storybook@8.5.2(prettier@3.4.1)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.2(prettier@3.4.1))(typescript@5.6.3)': + '@storybook/react@8.4.6(@storybook/test@8.4.6(storybook@8.5.3(prettier@3.4.1)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.3(prettier@3.4.1))(typescript@5.6.3)': dependencies: - '@storybook/components': 8.4.6(storybook@8.5.2(prettier@3.4.1)) + '@storybook/components': 8.4.6(storybook@8.5.3(prettier@3.4.1)) '@storybook/global': 5.0.0 - '@storybook/manager-api': 8.4.6(storybook@8.5.2(prettier@3.4.1)) - '@storybook/preview-api': 8.4.6(storybook@8.5.2(prettier@3.4.1)) - '@storybook/react-dom-shim': 8.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.2(prettier@3.4.1)) - '@storybook/theming': 8.4.6(storybook@8.5.2(prettier@3.4.1)) + '@storybook/manager-api': 8.4.6(storybook@8.5.3(prettier@3.4.1)) + '@storybook/preview-api': 8.4.6(storybook@8.5.3(prettier@3.4.1)) + '@storybook/react-dom-shim': 8.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.3(prettier@3.4.1)) + '@storybook/theming': 8.4.6(storybook@8.5.3(prettier@3.4.1)) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 8.5.2(prettier@3.4.1) + storybook: 8.5.3(prettier@3.4.1) optionalDependencies: - '@storybook/test': 8.4.6(storybook@8.5.2(prettier@3.4.1)) + '@storybook/test': 8.4.6(storybook@8.5.3(prettier@3.4.1)) typescript: 5.6.3 - '@storybook/test@8.4.6(storybook@8.5.2(prettier@3.4.1))': + '@storybook/test@8.4.6(storybook@8.5.3(prettier@3.4.1))': dependencies: '@storybook/csf': 0.1.11 '@storybook/global': 5.0.0 - '@storybook/instrumenter': 8.4.6(storybook@8.5.2(prettier@3.4.1)) + '@storybook/instrumenter': 8.4.6(storybook@8.5.3(prettier@3.4.1)) '@testing-library/dom': 10.4.0 '@testing-library/jest-dom': 6.5.0 '@testing-library/user-event': 14.5.2(@testing-library/dom@10.4.0) '@vitest/expect': 2.0.5 '@vitest/spy': 2.0.5 - storybook: 8.5.2(prettier@3.4.1) + storybook: 8.5.3(prettier@3.4.1) - '@storybook/theming@8.4.6(storybook@8.5.2(prettier@3.4.1))': + '@storybook/test@8.5.3(storybook@8.5.3(prettier@3.4.1))': dependencies: - storybook: 8.5.2(prettier@3.4.1) + '@storybook/csf': 0.1.12 + '@storybook/global': 5.0.0 + '@storybook/instrumenter': 8.5.3(storybook@8.5.3(prettier@3.4.1)) + '@testing-library/dom': 10.4.0 + '@testing-library/jest-dom': 6.5.0 + '@testing-library/user-event': 14.5.2(@testing-library/dom@10.4.0) + '@vitest/expect': 2.0.5 + '@vitest/spy': 2.0.5 + storybook: 8.5.3(prettier@3.4.1) + + '@storybook/theming@8.4.6(storybook@8.5.3(prettier@3.4.1))': + dependencies: + storybook: 8.5.3(prettier@3.4.1) '@swc/core-darwin-arm64@1.3.38': optional: true @@ -8563,25 +8546,20 @@ snapshots: lz-string: 1.5.0 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.17.16)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)))': + '@testing-library/jest-dom@6.5.0': dependencies: - '@adobe/css-tools': 4.4.0 - '@babel/runtime': 7.24.7 - aria-query: 5.3.0 + '@adobe/css-tools': 4.4.1 + aria-query: 5.3.2 chalk: 3.0.0 css.escape: 1.5.1 dom-accessibility-api: 0.6.3 lodash: 4.17.21 redent: 3.0.0 - optionalDependencies: - '@jest/globals': 29.7.0 - '@types/jest': 29.5.14 - jest: 29.7.0(@types/node@20.17.16)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)) - '@testing-library/jest-dom@6.5.0': + '@testing-library/jest-dom@6.6.3': dependencies: - '@adobe/css-tools': 4.4.0 - aria-query: 5.3.0 + '@adobe/css-tools': 4.4.1 + aria-query: 5.3.2 chalk: 3.0.0 css.escape: 1.5.1 dom-accessibility-api: 0.6.3 @@ -8605,11 +8583,11 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@testing-library/user-event@14.5.1(@testing-library/dom@10.4.0)': + '@testing-library/user-event@14.5.2(@testing-library/dom@10.4.0)': dependencies: '@testing-library/dom': 10.4.0 - '@testing-library/user-event@14.5.2(@testing-library/dom@10.4.0)': + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.0)': dependencies: '@testing-library/dom': 10.4.0 @@ -8617,12 +8595,12 @@ snapshots: '@ts-morph/common@0.12.3': dependencies: - fast-glob: 3.3.2 + fast-glob: 3.3.3 minimatch: 3.1.2 mkdirp: 1.0.4 path-browserify: 1.0.1 - '@tsconfig/node10@1.0.9': {} + '@tsconfig/node10@1.0.11': {} '@tsconfig/node12@1.0.11': {} @@ -8660,11 +8638,11 @@ snapshots: '@types/chroma-js@2.4.0': {} - '@types/color-convert@2.0.0': + '@types/color-convert@2.0.4': dependencies: - '@types/color-name': 1.1.1 + '@types/color-name': 1.1.5 - '@types/color-name@1.1.1': {} + '@types/color-name@1.1.5': {} '@types/connect@3.4.35': dependencies: @@ -8774,7 +8752,7 @@ snapshots: '@types/tough-cookie': 4.0.2 parse5: 7.1.2 - '@types/lodash@4.17.14': {} + '@types/lodash@4.17.15': {} '@types/mdast@4.0.3': dependencies: @@ -8814,10 +8792,10 @@ snapshots: '@types/range-parser@1.2.4': {} - '@types/react-color@3.0.12': + '@types/react-color@3.0.13(@types/react@18.3.12)': dependencies: '@types/react': 18.3.12 - '@types/reactcss': 1.2.6 + '@types/reactcss': 1.2.13(@types/react@18.3.12) '@types/react-date-range@1.4.4': dependencies: @@ -8849,7 +8827,7 @@ snapshots: '@types/prop-types': 15.7.13 csstype: 3.1.3 - '@types/reactcss@1.2.6': + '@types/reactcss@1.2.13(@types/react@18.3.12)': dependencies: '@types/react': 18.3.12 @@ -8948,7 +8926,7 @@ snapshots: '@vitest/utils@2.1.8': dependencies: '@vitest/pretty-format': 2.1.8 - loupe: 3.1.2 + loupe: 3.1.3 tinyrainbow: 1.2.0 '@xterm/addon-canvas@0.7.0(@xterm/xterm@5.5.0)': @@ -8982,21 +8960,17 @@ snapshots: acorn-globals@7.0.1: dependencies: - acorn: 8.11.2 - acorn-walk: 8.3.0 + acorn: 8.14.0 + acorn-walk: 8.3.4 acorn-jsx@5.3.2(acorn@8.14.0): dependencies: acorn: 8.14.0 optional: true - acorn-walk@8.2.0: {} - - acorn-walk@8.3.0: {} - - acorn@8.10.0: {} - - acorn@8.11.2: {} + acorn-walk@8.3.4: + dependencies: + acorn: 8.14.0 acorn@8.14.0: {} @@ -9058,7 +9032,7 @@ snapshots: aria-hidden@1.2.4: dependencies: - tslib: 2.6.2 + tslib: 2.8.1 aria-query@5.1.3: dependencies: @@ -9068,6 +9042,8 @@ snapshots: dependencies: dequal: 2.0.3 + aria-query@5.3.2: {} + array-buffer-byte-length@1.0.0: dependencies: call-bind: 1.0.7 @@ -9087,14 +9063,14 @@ snapshots: asynckit@0.4.0: {} - autoprefixer@10.4.20(postcss@8.4.47): + autoprefixer@10.4.20(postcss@8.5.1): dependencies: browserslist: 4.24.2 caniuse-lite: 1.0.30001677 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.1.1 - postcss: 8.4.47 + postcss: 8.5.1 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.7: @@ -9143,7 +9119,7 @@ snapshots: dependencies: '@babel/runtime': 7.26.7 cosmiconfig: 7.1.0 - resolve: 1.22.9 + resolve: 1.22.10 babel-preset-current-node-syntax@1.1.0(@babel/core@7.26.0): dependencies: @@ -9291,11 +9267,10 @@ snapshots: caniuse-lite@1.0.30001690: {} - canvas@3.0.0-rc2: + canvas@3.1.0: dependencies: node-addon-api: 7.1.1 - prebuild-install: 7.1.2 - simple-get: 3.1.1 + prebuild-install: 7.1.3 case-anything@2.1.13: {} @@ -9372,7 +9347,7 @@ snapshots: chroma-js@2.4.2: {} - chromatic@11.16.3: {} + chromatic@11.25.2: {} ci-info@3.9.0: {} @@ -9458,7 +9433,7 @@ snapshots: cookie@0.5.0: {} - cookie@0.6.0: {} + cookie@0.7.1: {} copy-anything@3.0.5: dependencies: @@ -9480,13 +9455,13 @@ snapshots: nan: 2.20.0 optional: true - create-jest@29.7.0(@types/node@20.17.16)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)): + create-jest@29.7.0(@types/node@20.17.16)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.17.16)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)) + jest-config: 29.7.0(@types/node@20.17.16)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -9591,10 +9566,6 @@ snapshots: dependencies: character-entities: 2.0.2 - decompress-response@4.2.1: - dependencies: - mimic-response: 2.1.0 - decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 @@ -9621,7 +9592,7 @@ snapshots: object-keys: 1.1.1 object.assign: 4.1.4 regexp.prototype.flags: 1.5.1 - side-channel: 1.0.6 + side-channel: 1.1.0 which-boxed-primitive: 1.0.2 which-collection: 1.0.1 which-typed-array: 1.1.18 @@ -9643,7 +9614,7 @@ snapshots: define-data-property@1.1.4: dependencies: - es-define-property: 1.0.0 + es-define-property: 1.0.1 es-errors: 1.3.0 gopd: 1.2.0 @@ -9665,7 +9636,7 @@ snapshots: detect-libc@1.0.3: {} - detect-libc@2.0.2: {} + detect-libc@2.0.3: {} detect-newline@3.1.0: {} @@ -9829,8 +9800,6 @@ snapshots: '@esbuild/win32-ia32': 0.24.2 '@esbuild/win32-x64': 0.24.2 - escalade@3.1.2: {} - escalade@3.2.0: {} escape-html@1.0.3: {} @@ -9965,14 +9934,14 @@ snapshots: jest-message-util: 29.7.0 jest-util: 29.7.0 - express@4.21.0: + express@4.21.2: dependencies: accepts: 1.3.8 array-flatten: 1.1.1 body-parser: 1.20.3 content-disposition: 0.5.4 content-type: 1.0.5 - cookie: 0.6.0 + cookie: 0.7.1 cookie-signature: 1.0.6 debug: 2.6.9 depd: 2.0.0 @@ -9986,7 +9955,7 @@ snapshots: methods: 1.1.2 on-finished: 2.4.1 parseurl: 1.3.3 - path-to-regexp: 0.1.10 + path-to-regexp: 0.1.12 proxy-addr: 2.0.7 qs: 6.13.0 range-parser: 1.2.1 @@ -10016,12 +9985,20 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: optional: true - fastq@1.17.1: + fastq@1.19.0: dependencies: reusify: 1.0.4 @@ -10090,7 +10067,7 @@ snapshots: dependencies: is-callable: 1.2.7 - foreground-child@3.1.1: + foreground-child@3.3.0: dependencies: cross-spawn: 7.0.6 signal-exit: 4.1.0 @@ -10155,7 +10132,7 @@ snapshots: function-bind: 1.1.2 has-proto: 1.0.1 has-symbols: 1.0.3 - hasown: 2.0.0 + hasown: 2.0.2 get-intrinsic@1.2.7: dependencies: @@ -10191,13 +10168,14 @@ snapshots: dependencies: is-glob: 4.0.3 - glob@10.3.10: + glob@10.4.5: dependencies: - foreground-child: 3.1.1 - jackspeak: 2.3.6 + foreground-child: 3.3.0 + jackspeak: 3.4.3 minimatch: 9.0.5 - minipass: 7.0.4 - path-scurry: 1.10.1 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 glob@7.2.3: dependencies: @@ -10236,7 +10214,7 @@ snapshots: has-property-descriptors@1.0.2: dependencies: - es-define-property: 1.0.0 + es-define-property: 1.0.1 has-proto@1.0.1: {} @@ -10353,6 +10331,12 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + optional: true + import-local@3.2.0: dependencies: pkg-dir: 4.2.0 @@ -10377,7 +10361,7 @@ snapshots: dependencies: get-intrinsic: 1.2.7 hasown: 2.0.2 - side-channel: 1.0.6 + side-channel: 1.1.0 internmap@2.0.3: {} @@ -10433,7 +10417,7 @@ snapshots: dependencies: hasown: 2.0.0 - is-core-module@2.16.0: + is-core-module@2.16.1: dependencies: hasown: 2.0.2 @@ -10577,7 +10561,7 @@ snapshots: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 - jackspeak@2.3.6: + jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 optionalDependencies: @@ -10620,16 +10604,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.17.16)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)): + jest-cli@29.7.0(@types/node@20.17.16)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.17.16)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)) + create-jest: 29.7.0(@types/node@20.17.16)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@20.17.16)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)) + jest-config: 29.7.0(@types/node@20.17.16)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -10639,7 +10623,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@20.17.16)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)): + jest-config@29.7.0(@types/node@20.17.16)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)): dependencies: '@babel/core': 7.26.0 '@jest/test-sequencer': 29.7.0 @@ -10665,7 +10649,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 20.17.16 - ts-node: 10.9.1(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3) + ts-node: 10.9.2(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -10696,7 +10680,7 @@ snapshots: jest-util: 29.7.0 pretty-format: 29.7.0 - jest-environment-jsdom@29.5.0(canvas@3.0.0-rc2): + jest-environment-jsdom@29.5.0(canvas@3.1.0): dependencies: '@jest/environment': 29.6.2 '@jest/fake-timers': 29.6.2 @@ -10705,9 +10689,9 @@ snapshots: '@types/node': 20.17.16 jest-mock: 29.6.2 jest-util: 29.6.2 - jsdom: 20.0.3(canvas@3.0.0-rc2) + jsdom: 20.0.3(canvas@3.1.0) optionalDependencies: - canvas: 3.0.0-rc2 + canvas: 3.1.0 transitivePeerDependencies: - bufferutil - supports-color @@ -10816,7 +10800,7 @@ snapshots: jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) jest-util: 29.7.0 jest-validate: 29.7.0 - resolve: 1.22.8 + resolve: 1.22.10 resolve.exports: 2.0.2 slash: 3.0.0 @@ -10948,12 +10932,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@20.17.16)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)): + jest@29.7.0(@types/node@20.17.16)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@20.17.16)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)) + jest-cli: 29.7.0(@types/node@20.17.16)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -10965,7 +10949,7 @@ snapshots: '@swc/core': 1.3.38 '@swc/jest': 0.2.37(@swc/core@1.3.38) - jiti@1.21.6: {} + jiti@1.21.7: {} js-tokens@4.0.0: {} @@ -10981,10 +10965,10 @@ snapshots: jsdoc-type-pratt-parser@4.1.0: {} - jsdom@20.0.3(canvas@3.0.0-rc2): + jsdom@20.0.3(canvas@3.1.0): dependencies: abab: 2.0.6 - acorn: 8.11.2 + acorn: 8.14.0 acorn-globals: 7.0.1 cssom: 0.5.0 cssstyle: 2.3.0 @@ -11010,7 +10994,7 @@ snapshots: ws: 8.17.1 xml-name-validator: 4.0.0 optionalDependencies: - canvas: 3.0.0-rc2 + canvas: 3.1.0 transitivePeerDependencies: - bufferutil - supports-color @@ -11065,9 +11049,7 @@ snapshots: dependencies: immediate: 3.0.6 - lilconfig@2.1.0: {} - - lilconfig@3.1.2: {} + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -11096,6 +11078,8 @@ snapshots: loupe@3.1.2: {} + loupe@3.1.3: {} + lowlight@1.20.0: dependencies: fault: 1.0.4 @@ -11674,8 +11658,6 @@ snapshots: mimic-fn@2.1.0: {} - mimic-response@2.1.0: {} - mimic-response@3.1.0: {} min-indent@1.0.1: {} @@ -11690,7 +11672,7 @@ snapshots: minimist@1.2.8: {} - minipass@7.0.4: {} + minipass@7.1.2: {} mkdirp-classic@0.5.3: {} @@ -11743,13 +11725,13 @@ snapshots: nanoid@3.3.8: {} - napi-build-utils@1.0.2: {} + napi-build-utils@2.0.0: {} natural-compare@1.4.0: {} negotiator@0.6.3: {} - node-abi@3.65.0: + node-abi@3.74.0: dependencies: semver: 7.6.2 @@ -11775,7 +11757,7 @@ snapshots: object-hash@3.0.0: {} - object-inspect@1.13.1: {} + object-inspect@1.13.3: {} object-is@1.1.5: dependencies: @@ -11839,6 +11821,8 @@ snapshots: p-try@2.2.0: {} + package-json-from-dist@1.0.1: {} + pako@1.0.11: {} parent-module@1.0.1: @@ -11887,12 +11871,12 @@ snapshots: path-parse@1.0.7: {} - path-scurry@1.10.1: + path-scurry@1.11.1: dependencies: lru-cache: 10.4.3 - minipass: 7.0.4 + minipass: 7.1.2 - path-to-regexp@0.1.10: {} + path-to-regexp@0.1.12: {} path-to-regexp@6.2.1: {} @@ -11900,12 +11884,12 @@ snapshots: pathval@2.0.0: {} - picocolors@1.1.0: {} - picocolors@1.1.1: {} picomatch@2.3.1: {} + picomatch@4.0.2: {} + pify@2.3.0: {} pirates@4.0.6: {} @@ -11922,35 +11906,35 @@ snapshots: optionalDependencies: fsevents: 2.3.2 - polished@4.2.2: + polished@4.3.1: dependencies: '@babel/runtime': 7.26.7 possible-typed-array-names@1.0.0: {} - postcss-import@15.1.0(postcss@8.4.47): + postcss-import@15.1.0(postcss@8.5.1): dependencies: - postcss: 8.4.47 + postcss: 8.5.1 postcss-value-parser: 4.2.0 read-cache: 1.0.0 - resolve: 1.22.8 + resolve: 1.22.10 - postcss-js@4.0.1(postcss@8.4.47): + postcss-js@4.0.1(postcss@8.5.1): dependencies: camelcase-css: 2.0.1 - postcss: 8.4.47 + postcss: 8.5.1 - postcss-load-config@4.0.2(postcss@8.4.47)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)): + postcss-load-config@4.0.2(postcss@8.5.1)(ts-node@10.9.2(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)): dependencies: - lilconfig: 3.1.2 - yaml: 2.6.0 + lilconfig: 3.1.3 + yaml: 2.7.0 optionalDependencies: - postcss: 8.4.47 - ts-node: 10.9.1(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3) + postcss: 8.5.1 + ts-node: 10.9.2(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3) - postcss-nested@6.2.0(postcss@8.4.47): + postcss-nested@6.2.0(postcss@8.5.1): dependencies: - postcss: 8.4.47 + postcss: 8.5.1 postcss-selector-parser: 6.1.2 postcss-selector-parser@6.1.2: @@ -11960,25 +11944,25 @@ snapshots: postcss-value-parser@4.2.0: {} - postcss@8.4.47: + postcss@8.5.1: dependencies: nanoid: 3.3.8 picocolors: 1.1.1 source-map-js: 1.2.1 - prebuild-install@7.1.2: + prebuild-install@7.1.3: dependencies: - detect-libc: 2.0.2 + detect-libc: 2.0.3 expand-template: 2.0.3 github-from-package: 0.0.0 minimist: 1.2.8 mkdirp-classic: 0.5.3 - napi-build-utils: 1.0.2 - node-abi: 3.65.0 - pump: 3.0.0 + napi-build-utils: 2.0.0 + node-abi: 3.74.0 + pump: 3.0.2 rc: 1.2.8 simple-get: 4.0.1 - tar-fs: 2.1.1 + tar-fs: 2.1.2 tunnel-agent: 0.6.0 prelude-ls@1.2.1: @@ -12020,7 +12004,7 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 - property-expr@2.0.5: {} + property-expr@2.0.6: {} property-information@5.6.0: dependencies: @@ -12052,7 +12036,7 @@ snapshots: psl@1.9.0: {} - pump@3.0.0: + pump@3.0.2: dependencies: end-of-stream: 1.4.4 once: 1.4.0 @@ -12063,7 +12047,7 @@ snapshots: qs@6.13.0: dependencies: - side-channel: 1.0.6 + side-channel: 1.1.0 querystringify@2.2.0: {} @@ -12085,7 +12069,7 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 - react-chartjs-2@5.2.0(chart.js@4.4.0)(react@18.3.1): + react-chartjs-2@5.3.0(chart.js@4.4.0)(react@18.3.1): dependencies: chart.js: 4.4.0 react: 18.3.1 @@ -12192,41 +12176,33 @@ snapshots: react-refresh@0.14.2: {} - react-remove-scroll-bar@2.3.6(@types/react@18.3.12)(react@18.3.1): - dependencies: - react: 18.3.1 - react-style-singleton: 2.2.3(@types/react@18.3.12)(react@18.3.1) - tslib: 2.6.2 - optionalDependencies: - '@types/react': 18.3.12 - react-remove-scroll-bar@2.3.8(@types/react@18.3.12)(react@18.3.1): dependencies: react: 18.3.1 react-style-singleton: 2.2.3(@types/react@18.3.12)(react@18.3.1) - tslib: 2.6.2 + tslib: 2.8.1 optionalDependencies: '@types/react': 18.3.12 - react-remove-scroll@2.6.0(@types/react@18.3.12)(react@18.3.1): + react-remove-scroll@2.6.2(@types/react@18.3.12)(react@18.3.1): dependencies: react: 18.3.1 - react-remove-scroll-bar: 2.3.6(@types/react@18.3.12)(react@18.3.1) - react-style-singleton: 2.2.1(@types/react@18.3.12)(react@18.3.1) + react-remove-scroll-bar: 2.3.8(@types/react@18.3.12)(react@18.3.1) + react-style-singleton: 2.2.3(@types/react@18.3.12)(react@18.3.1) tslib: 2.6.2 - use-callback-ref: 1.3.2(@types/react@18.3.12)(react@18.3.1) + use-callback-ref: 1.3.3(@types/react@18.3.12)(react@18.3.1) use-sidecar: 1.1.2(@types/react@18.3.12)(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - react-remove-scroll@2.6.2(@types/react@18.3.12)(react@18.3.1): + react-remove-scroll@2.6.3(@types/react@18.3.12)(react@18.3.1): dependencies: react: 18.3.1 react-remove-scroll-bar: 2.3.8(@types/react@18.3.12)(react@18.3.1) react-style-singleton: 2.2.3(@types/react@18.3.12)(react@18.3.1) - tslib: 2.6.2 + tslib: 2.8.1 use-callback-ref: 1.3.3(@types/react@18.3.12)(react@18.3.1) - use-sidecar: 1.1.2(@types/react@18.3.12)(react@18.3.1) + use-sidecar: 1.1.3(@types/react@18.3.12)(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 @@ -12250,20 +12226,11 @@ snapshots: react-dom: 18.3.1(react@18.3.1) react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-style-singleton@2.2.1(@types/react@18.3.12)(react@18.3.1): - dependencies: - get-nonce: 1.0.1 - invariant: 2.2.4 - react: 18.3.1 - tslib: 2.6.2 - optionalDependencies: - '@types/react': 18.3.12 - react-style-singleton@2.2.3(@types/react@18.3.12)(react@18.3.1): dependencies: get-nonce: 1.0.1 react: 18.3.1 - tslib: 2.6.2 + tslib: 2.8.1 optionalDependencies: '@types/react': 18.3.12 @@ -12291,9 +12258,9 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-window@1.8.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-window@1.8.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.26.7 memoize-one: 5.2.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -12429,15 +12396,15 @@ snapshots: resolve.exports@2.0.2: {} - resolve@1.22.8: + resolve@1.22.10: dependencies: - is-core-module: 2.13.1 + is-core-module: 2.16.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - resolve@1.22.9: + resolve@1.22.8: dependencies: - is-core-module: 2.16.0 + is-core-module: 2.13.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 @@ -12448,10 +12415,10 @@ snapshots: glob: 7.2.3 optional: true - rollup-plugin-visualizer@5.12.0(rollup@4.32.0): + rollup-plugin-visualizer@5.14.0(rollup@4.32.0): dependencies: open: 8.4.2 - picomatch: 2.3.1 + picomatch: 4.0.2 source-map: 0.7.4 yargs: 17.7.2 optionalDependencies: @@ -12570,12 +12537,33 @@ snapshots: shebang-regex@3.0.0: {} - side-channel@1.0.6: + side-channel-list@1.0.0: dependencies: - call-bind: 1.0.7 es-errors: 1.3.0 - get-intrinsic: 1.2.4 - object-inspect: 1.13.1 + object-inspect: 1.13.3 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.7 + object-inspect: 1.13.3 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.7 + object-inspect: 1.13.3 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.3 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 signal-exit@3.0.7: {} @@ -12583,12 +12571,6 @@ snapshots: simple-concat@1.0.1: {} - simple-get@3.1.1: - dependencies: - decompress-response: 4.2.1 - once: 1.4.0 - simple-concat: 1.0.1 - simple-get@4.0.1: dependencies: decompress-response: 6.0.0 @@ -12638,15 +12620,15 @@ snapshots: dependencies: internal-slot: 1.0.6 - storybook-addon-remix-react-router@3.1.0(@storybook/blocks@8.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.2(prettier@3.4.1)))(@storybook/channels@8.1.11)(@storybook/components@8.4.6(storybook@8.5.2(prettier@3.4.1)))(@storybook/core-events@8.1.11)(@storybook/manager-api@8.4.6(storybook@8.5.2(prettier@3.4.1)))(@storybook/preview-api@8.4.7(storybook@8.5.2(prettier@3.4.1)))(@storybook/theming@8.4.6(storybook@8.5.2(prettier@3.4.1)))(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.26.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): + storybook-addon-remix-react-router@3.1.0(@storybook/blocks@8.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.3(prettier@3.4.1)))(@storybook/channels@8.1.11)(@storybook/components@8.4.6(storybook@8.5.3(prettier@3.4.1)))(@storybook/core-events@8.1.11)(@storybook/manager-api@8.4.6(storybook@8.5.3(prettier@3.4.1)))(@storybook/preview-api@8.5.3(storybook@8.5.3(prettier@3.4.1)))(@storybook/theming@8.4.6(storybook@8.5.3(prettier@3.4.1)))(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.26.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): dependencies: - '@storybook/blocks': 8.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.2(prettier@3.4.1)) + '@storybook/blocks': 8.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.3(prettier@3.4.1)) '@storybook/channels': 8.1.11 - '@storybook/components': 8.4.6(storybook@8.5.2(prettier@3.4.1)) + '@storybook/components': 8.4.6(storybook@8.5.3(prettier@3.4.1)) '@storybook/core-events': 8.1.11 - '@storybook/manager-api': 8.4.6(storybook@8.5.2(prettier@3.4.1)) - '@storybook/preview-api': 8.4.7(storybook@8.5.2(prettier@3.4.1)) - '@storybook/theming': 8.4.6(storybook@8.5.2(prettier@3.4.1)) + '@storybook/manager-api': 8.4.6(storybook@8.5.3(prettier@3.4.1)) + '@storybook/preview-api': 8.5.3(storybook@8.5.3(prettier@3.4.1)) + '@storybook/theming': 8.4.6(storybook@8.5.3(prettier@3.4.1)) compare-versions: 6.1.0 react-inspector: 6.0.2(react@18.3.1) react-router-dom: 6.26.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -12654,17 +12636,17 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook-react-context@0.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.2(prettier@3.4.1)): + storybook-react-context@0.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.3(prettier@3.4.1)): dependencies: - '@storybook/preview-api': 8.4.7(storybook@8.5.2(prettier@3.4.1)) + '@storybook/preview-api': 8.5.3(storybook@8.5.3(prettier@3.4.1)) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) transitivePeerDependencies: - storybook - storybook@8.5.2(prettier@3.4.1): + storybook@8.5.3(prettier@3.4.1): dependencies: - '@storybook/core': 8.5.2(prettier@3.4.1) + '@storybook/core': 8.5.3(prettier@3.4.1) optionalDependencies: prettier: 3.4.1 transitivePeerDependencies: @@ -12738,9 +12720,9 @@ snapshots: sucrase@3.35.0: dependencies: - '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/gen-mapping': 0.3.8 commander: 4.1.1 - glob: 10.3.10 + glob: 10.4.5 lines-and-columns: 1.2.4 mz: 2.7.0 pirates: 4.0.6 @@ -12766,44 +12748,44 @@ snapshots: symbol-tree@3.2.4: {} - tailwind-merge@2.5.4: {} + tailwind-merge@2.6.0: {} - tailwindcss-animate@1.0.7(tailwindcss@3.4.13(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3))): + tailwindcss-animate@1.0.7(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3))): dependencies: - tailwindcss: 3.4.13(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)) + tailwindcss: 3.4.17(ts-node@10.9.2(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)) - tailwindcss@3.4.13(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)): + tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 chokidar: 3.6.0 didyoumean: 1.2.2 dlv: 1.1.3 - fast-glob: 3.3.2 + fast-glob: 3.3.3 glob-parent: 6.0.2 is-glob: 4.0.3 - jiti: 1.21.6 - lilconfig: 2.1.0 + jiti: 1.21.7 + lilconfig: 3.1.3 micromatch: 4.0.8 normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.1.1 - postcss: 8.4.47 - postcss-import: 15.1.0(postcss@8.4.47) - postcss-js: 4.0.1(postcss@8.4.47) - postcss-load-config: 4.0.2(postcss@8.4.47)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)) - postcss-nested: 6.2.0(postcss@8.4.47) + postcss: 8.5.1 + postcss-import: 15.1.0(postcss@8.5.1) + postcss-js: 4.0.1(postcss@8.5.1) + postcss-load-config: 4.0.2(postcss@8.5.1)(ts-node@10.9.2(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3)) + postcss-nested: 6.2.0(postcss@8.5.1) postcss-selector-parser: 6.1.2 - resolve: 1.22.8 + resolve: 1.22.10 sucrase: 3.35.0 transitivePeerDependencies: - ts-node - tar-fs@2.1.1: + tar-fs@2.1.2: dependencies: chownr: 1.1.4 mkdirp-classic: 0.5.3 - pump: 3.0.0 + pump: 3.0.2 tar-stream: 2.2.0 tar-stream@2.2.0: @@ -12892,16 +12874,16 @@ snapshots: '@ts-morph/common': 0.12.3 code-block-writer: 11.0.3 - ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3): + ts-node@10.9.2(@swc/core@1.3.38)(@types/node@20.17.16)(typescript@5.6.3): dependencies: '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.9 + '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 '@types/node': 20.17.16 - acorn: 8.10.0 - acorn-walk: 8.2.0 + acorn: 8.14.0 + acorn-walk: 8.3.4 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 @@ -12982,7 +12964,7 @@ snapshots: tzdata@1.0.40: {} - ua-parser-js@1.0.33: {} + ua-parser-js@1.0.40: {} undici-types@5.26.5: {} @@ -13070,25 +13052,26 @@ snapshots: querystringify: 2.2.0 requires-port: 1.0.0 - use-callback-ref@1.3.2(@types/react@18.3.12)(react@18.3.1): + use-callback-ref@1.3.3(@types/react@18.3.12)(react@18.3.1): dependencies: react: 18.3.1 - tslib: 2.6.2 + tslib: 2.8.1 optionalDependencies: '@types/react': 18.3.12 - use-callback-ref@1.3.3(@types/react@18.3.12)(react@18.3.1): + use-sidecar@1.1.2(@types/react@18.3.12)(react@18.3.1): dependencies: + detect-node-es: 1.1.0 react: 18.3.1 tslib: 2.6.2 optionalDependencies: '@types/react': 18.3.12 - use-sidecar@1.1.2(@types/react@18.3.12)(react@18.3.1): + use-sidecar@1.1.3(@types/react@18.3.12)(react@18.3.1): dependencies: detect-node-es: 1.1.0 react: 18.3.1 - tslib: 2.6.2 + tslib: 2.8.1 optionalDependencies: '@types/react': 18.3.12 @@ -13179,7 +13162,7 @@ snapshots: vite@5.4.14(@types/node@20.17.16): dependencies: esbuild: 0.21.5 - postcss: 8.4.47 + postcss: 8.5.1 rollup: 4.32.0 optionalDependencies: '@types/node': 20.17.16 @@ -13302,14 +13285,14 @@ snapshots: yaml@1.10.2: {} - yaml@2.6.0: {} + yaml@2.7.0: {} yargs-parser@21.1.1: {} yargs@17.7.2: dependencies: cliui: 8.0.1 - escalade: 3.1.2 + escalade: 3.2.0 get-caller-file: 2.0.5 require-directory: 2.1.1 string-width: 4.2.3 @@ -13320,9 +13303,9 @@ snapshots: yocto-queue@0.1.0: {} - yup@1.4.0: + yup@1.6.1: dependencies: - property-expr: 2.0.5 + property-expr: 2.0.6 tiny-case: 1.0.3 toposort: 2.0.2 type-fest: 2.19.0 diff --git a/site/site.go b/site/site.go index af66c01c6f896..e2209b4052929 100644 --- a/site/site.go +++ b/site/site.go @@ -34,6 +34,7 @@ import ( "golang.org/x/sync/singleflight" "golang.org/x/xerrors" + "cdr.dev/slog" "github.com/coder/coder/v2/coderd/appearance" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/db2sdk" @@ -41,6 +42,7 @@ import ( "github.com/coder/coder/v2/coderd/entitlements" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/telemetry" "github.com/coder/coder/v2/codersdk" ) @@ -81,6 +83,8 @@ type Options struct { BuildInfo codersdk.BuildInfoResponse AppearanceFetcher *atomic.Pointer[appearance.Fetcher] Entitlements *entitlements.Set + Telemetry telemetry.Reporter + Logger slog.Logger } func New(opts *Options) *Handler { @@ -162,7 +166,7 @@ func New(opts *Options) *Handler { handler.installScript, err = parseInstallScript(opts.SiteFS, opts.BuildInfo) if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "install.sh will be unavailable: %v", err.Error()) + opts.Logger.Warn(context.Background(), "could not parse install.sh, it will be unavailable", slog.Error(err)) } return handler @@ -183,6 +187,8 @@ type Handler struct { Entitlements *entitlements.Set Experiments atomic.Pointer[codersdk.Experiments] + + telemetryHTMLServedOnce sync.Once } func (h *Handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { @@ -321,12 +327,51 @@ func ShouldCacheFile(reqFile string) bool { return true } +// reportHTMLFirstServedAt sends a telemetry report when the first HTML is ever served. +// The purpose is to track the first time the first user opens the site. +func (h *Handler) reportHTMLFirstServedAt() { + // nolint:gocritic // Manipulating telemetry items is system-restricted. + // TODO(hugodutka): Add a telemetry context in RBAC. + ctx := dbauthz.AsSystemRestricted(context.Background()) + itemKey := string(telemetry.TelemetryItemKeyHTMLFirstServedAt) + _, err := h.opts.Database.GetTelemetryItem(ctx, itemKey) + if err == nil { + // If the value is already set, then we reported it before. + // We don't need to report it again. + return + } + if !errors.Is(err, sql.ErrNoRows) { + h.opts.Logger.Debug(ctx, "failed to get telemetry html first served at", slog.Error(err)) + return + } + if err := h.opts.Database.InsertTelemetryItemIfNotExists(ctx, database.InsertTelemetryItemIfNotExistsParams{ + Key: string(telemetry.TelemetryItemKeyHTMLFirstServedAt), + Value: time.Now().Format(time.RFC3339), + }); err != nil { + h.opts.Logger.Debug(ctx, "failed to set telemetry html first served at", slog.Error(err)) + return + } + item, err := h.opts.Database.GetTelemetryItem(ctx, itemKey) + if err != nil { + h.opts.Logger.Debug(ctx, "failed to get telemetry html first served at", slog.Error(err)) + return + } + h.opts.Telemetry.Report(&telemetry.Snapshot{ + TelemetryItems: []telemetry.TelemetryItem{telemetry.ConvertTelemetryItem(item)}, + }) +} + func (h *Handler) serveHTML(resp http.ResponseWriter, request *http.Request, reqPath string, state htmlState) bool { if data, err := h.renderHTMLWithState(request, reqPath, state); err == nil { if reqPath == "" { // Pass "index.html" to the ServeContent so the ServeContent sets the right content headers. reqPath = "index.html" } + // `Once` is used to reduce the volume of db calls and telemetry reports. + // It's fine to run the enclosed function multiple times, but it's unnecessary. + h.telemetryHTMLServedOnce.Do(func() { + go h.reportHTMLFirstServedAt() + }) http.ServeContent(resp, request, reqPath, time.Time{}, bytes.NewReader(data)) return true } diff --git a/site/site_test.go b/site/site_test.go index 8bee665a56ae3..63f3f9aa17226 100644 --- a/site/site_test.go +++ b/site/site_test.go @@ -27,8 +27,10 @@ import ( "github.com/coder/coder/v2/coderd/database/db2sdk" "github.com/coder/coder/v2/coderd/database/dbgen" "github.com/coder/coder/v2/coderd/database/dbmem" + "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/telemetry" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/site" "github.com/coder/coder/v2/testutil" @@ -45,9 +47,10 @@ func TestInjection(t *testing.T) { binFs := http.FS(fstest.MapFS{}) db := dbmem.New() handler := site.New(&site.Options{ - BinFS: binFs, - Database: db, - SiteFS: siteFS, + Telemetry: telemetry.NewNoop(), + BinFS: binFs, + Database: db, + SiteFS: siteFS, }) user := dbgen.User(t, db, database.User{}) @@ -101,9 +104,10 @@ func TestInjectionFailureProducesCleanHTML(t *testing.T) { }, } handler := site.New(&site.Options{ - BinFS: binFs, - Database: db, - SiteFS: siteFS, + Telemetry: telemetry.NewNoop(), + BinFS: binFs, + Database: db, + SiteFS: siteFS, // No OAuth2 configs, refresh will fail. OAuth2Configs: &httpmw.OAuth2Configs{ @@ -147,9 +151,12 @@ func TestCaching(t *testing.T) { } binFS := http.FS(fstest.MapFS{}) + db, _ := dbtestutil.NewDB(t) srv := httptest.NewServer(site.New(&site.Options{ - BinFS: binFS, - SiteFS: rootFS, + Telemetry: telemetry.NewNoop(), + BinFS: binFS, + SiteFS: rootFS, + Database: db, })) defer srv.Close() @@ -213,9 +220,12 @@ func TestServingFiles(t *testing.T) { } binFS := http.FS(fstest.MapFS{}) + db, _ := dbtestutil.NewDB(t) srv := httptest.NewServer(site.New(&site.Options{ - BinFS: binFS, - SiteFS: rootFS, + Telemetry: telemetry.NewNoop(), + BinFS: binFS, + SiteFS: rootFS, + Database: db, })) defer srv.Close() @@ -473,6 +483,7 @@ func TestServingBin(t *testing.T) { } srv := httptest.NewServer(site.New(&site.Options{ + Telemetry: telemetry.NewNoop(), BinFS: binFS, BinHashes: binHashes, SiteFS: rootFS, diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 26491efb10565..a1aeeca8a9e59 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -698,7 +698,7 @@ class ApiMethods { } const response = await this.axios.get( - `/api/v2/organizations/${organization}/provisionerdaemons?${params.toString()}`, + `/api/v2/organizations/${organization}/provisionerdaemons?${params}`, ); return response.data; }; @@ -787,6 +787,29 @@ class ApiMethods { return response.data; }; + getDeploymentIdpSyncFieldValues = async ( + field: string, + ): Promise => { + const params = new URLSearchParams(); + params.set("claimField", field); + const response = await this.axios.get( + `/api/v2/settings/idpsync/field-values?${params}`, + ); + return response.data; + }; + + getOrganizationIdpSyncClaimFieldValues = async ( + organization: string, + field: string, + ) => { + const params = new URLSearchParams(); + params.set("claimField", field); + const response = await this.axios.get( + `/api/v2/organizations/${organization}/settings/idpsync/field-values?${params}`, + ); + return response.data; + }; + getTemplate = async (templateId: string): Promise => { const response = await this.axios.get( `/api/v2/templates/${templateId}`, @@ -1122,6 +1145,15 @@ class ApiMethods { return response.data; }; + getTemplateVersionPresets = async ( + templateVersionId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/templateversions/${templateVersionId}/presets`, + ); + return response.data; + }; + startWorkspace = ( workspaceId: string, templateVersionId: string, @@ -1215,7 +1247,7 @@ class ApiMethods { }; cancelTemplateVersionBuild = async ( - templateVersionId: TypesGen.TemplateVersion["id"], + templateVersionId: string, ): Promise => { const response = await this.axios.patch( `/api/v2/templateversions/${templateVersionId}/cancel`, @@ -1224,6 +1256,17 @@ class ApiMethods { return response.data; }; + cancelTemplateVersionDryRun = async ( + templateVersionId: string, + jobId: string, + ): Promise => { + const response = await this.axios.patch( + `/api/v2/templateversions/${templateVersionId}/dry-run/${jobId}/cancel`, + ); + + return response.data; + }; + createUser = async ( user: TypesGen.CreateUserRequestWithOrgs, ): Promise => { @@ -1562,6 +1605,29 @@ class ApiMethods { return resp.data; }; + getOAuth2GitHubDeviceFlowCallback = async ( + code: string, + state: string, + ): Promise => { + const resp = await this.axios.get( + `/api/v2/users/oauth2/github/callback?code=${code}&state=${state}`, + ); + // sanity check + if ( + typeof resp.data !== "object" || + typeof resp.data.redirect_url !== "string" + ) { + console.error("Invalid response from OAuth2 GitHub callback", resp); + throw new Error("Invalid response from OAuth2 GitHub callback"); + } + return resp.data; + }; + + getOAuth2GitHubDevice = async (): Promise => { + const resp = await this.axios.get("/api/v2/users/oauth2/github/device"); + return resp.data; + }; + getOAuth2ProviderApps = async ( filter?: TypesGen.OAuth2ProviderAppFilter, ): Promise => { @@ -2254,6 +2320,10 @@ class ApiMethods { return res.data; }; + postTestNotification = async () => { + await this.axios.post("/api/v2/notifications/test"); + }; + requestOneTimePassword = async ( req: TypesGen.RequestOneTimePasscodeRequest, ) => { @@ -2272,6 +2342,38 @@ class ApiMethods { ); return res.data; }; + + getProvisionerJobs = async (orgId: string) => { + const res = await this.axios.get( + `/api/v2/organizations/${orgId}/provisionerjobs`, + ); + return res.data; + }; + + cancelProvisionerJob = async (job: TypesGen.ProvisionerJob) => { + switch (job.type) { + case "workspace_build": + if (!job.input.workspace_build_id) { + throw new Error("Workspace build ID is required to cancel this job"); + } + return this.cancelWorkspaceBuild(job.input.workspace_build_id); + + case "template_version_import": + if (!job.input.template_version_id) { + throw new Error("Template version ID is required to cancel this job"); + } + return this.cancelTemplateVersionBuild(job.input.template_version_id); + + case "template_version_dry_run": + if (!job.input.template_version_id) { + throw new Error("Template version ID is required to cancel this job"); + } + return this.cancelTemplateVersionDryRun( + job.input.template_version_id, + job.id, + ); + } + }; } // This is a hard coded CSRF token/cookie pair for local development. In prod, diff --git a/site/src/api/errors.ts b/site/src/api/errors.ts index f1e63d1e39caf..873163e11a68d 100644 --- a/site/src/api/errors.ts +++ b/site/src/api/errors.ts @@ -133,6 +133,14 @@ export const getErrorDetail = (error: unknown): string | undefined => { return undefined; }; +export const getErrorStatus = (error: unknown): number | undefined => { + if (isApiError(error)) { + return error.status; + } + + return undefined; +}; + export class DetailedError extends Error { constructor( message: string, diff --git a/site/src/api/queries/deployment.ts b/site/src/api/queries/deployment.ts index 62449af12fccf..999dd2ee4cbd5 100644 --- a/site/src/api/queries/deployment.ts +++ b/site/src/api/queries/deployment.ts @@ -29,3 +29,10 @@ export const deploymentSSHConfig = () => { queryFn: API.getDeploymentSSHConfig, }; }; + +export const deploymentIdpSyncFieldValues = (field: string) => { + return { + queryKey: ["deployment", "idpSync", "fieldValues", field], + queryFn: () => API.getDeploymentIdpSyncFieldValues(field), + }; +}; diff --git a/site/src/api/queries/oauth2.ts b/site/src/api/queries/oauth2.ts index 66547418c8f73..a124dbd032480 100644 --- a/site/src/api/queries/oauth2.ts +++ b/site/src/api/queries/oauth2.ts @@ -7,6 +7,20 @@ const userAppsKey = (userId: string) => appsKey.concat(userId); const appKey = (appId: string) => appsKey.concat(appId); const appSecretsKey = (appId: string) => appKey(appId).concat("secrets"); +export const getGitHubDevice = () => { + return { + queryKey: ["oauth2-provider", "github", "device"], + queryFn: () => API.getOAuth2GitHubDevice(), + }; +}; + +export const getGitHubDeviceFlowCallback = (code: string, state: string) => { + return { + queryKey: ["oauth2-provider", "github", "callback", code, state], + queryFn: () => API.getOAuth2GitHubDeviceFlowCallback(code, state), + }; +}; + export const getApps = (userId?: string) => { return { queryKey: userId ? appsKey.concat(userId) : appsKey, diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts index 0cc8168243c16..a27514a03c161 100644 --- a/site/src/api/queries/organizations.ts +++ b/site/src/api/queries/organizations.ts @@ -1,11 +1,17 @@ import { API } from "api/api"; import type { - AuthorizationResponse, CreateOrganizationRequest, GroupSyncSettings, RoleSyncSettings, UpdateOrganizationRequest, } from "api/typesGenerated"; +import { + type AnyOrganizationPermissions, + type OrganizationPermissionName, + type OrganizationPermissions, + anyOrganizationPermissionChecks, + organizationPermissionChecks, +} from "modules/management/organizationPermissions"; import type { QueryClient } from "react-query"; import { meKey } from "./users"; @@ -197,50 +203,16 @@ export const patchRoleSyncSettings = ( }; }; -/** - * Fetch permissions for a single organization. - * - * If the ID is undefined, return a disabled query. - */ -export const organizationPermissions = (organizationId: string | undefined) => { - if (!organizationId) { - return { enabled: false }; - } +export const provisionerJobQueryKey = (orgId: string) => [ + "organization", + orgId, + "provisionerjobs", +]; + +export const provisionerJobs = (orgId: string) => { return { - queryKey: ["organization", organizationId, "permissions"], - queryFn: () => - // Only request what we use on individual org settings, members, and group - // pages, which at the moment is whether you can edit the members on the - // members page, create roles on the roles page, and create groups on the - // groups page. The edit organization check for the settings page is - // covered by the multi-org query at the moment, and the edit group check - // on the group page is done on the group itself, not the org, so neither - // show up here. - API.checkAuthorization({ - checks: { - editMembers: { - object: { - resource_type: "organization_member", - organization_id: organizationId, - }, - action: "update", - }, - createGroup: { - object: { - resource_type: "group", - organization_id: organizationId, - }, - action: "create", - }, - assignOrgRole: { - object: { - resource_type: "assign_org_role", - organization_id: organizationId, - }, - action: "create", - }, - }, - }), + queryKey: provisionerJobQueryKey(orgId), + queryFn: () => API.getProvisionerJobs(orgId), }; }; @@ -263,58 +235,13 @@ export const organizationsPermissions = ( // per sub-link (settings, groups, roles, and members pages) that tells us // whether to show that page, since we only show them if you can edit (and // not, at the moment if you can only view). - const checks = (organizationId: string) => ({ - editMembers: { - object: { - resource_type: "organization_member", - organization_id: organizationId, - }, - action: "update", - }, - editGroups: { - object: { - resource_type: "group", - organization_id: organizationId, - }, - action: "update", - }, - editOrganization: { - object: { - resource_type: "organization", - organization_id: organizationId, - }, - action: "update", - }, - assignOrgRole: { - object: { - resource_type: "assign_org_role", - organization_id: organizationId, - }, - action: "create", - }, - viewProvisioners: { - object: { - resource_type: "provisioner_daemon", - organization_id: organizationId, - }, - action: "read", - }, - viewIdpSyncSettings: { - object: { - resource_type: "idpsync_settings", - organization_id: organizationId, - }, - action: "read", - }, - }); // The endpoint takes a flat array, so to avoid collisions prepend each // check with the org ID (the key can be anything we want). const prefixedChecks = organizationIds.flatMap((orgId) => - Object.entries(checks(orgId)).map(([key, val]) => [ - `${orgId}.${key}`, - val, - ]), + Object.entries(organizationPermissionChecks(orgId)).map( + ([key, val]) => [`${orgId}.${key}`, val], + ), ); const response = await API.checkAuthorization({ @@ -330,11 +257,42 @@ export const organizationsPermissions = ( if (!acc[orgId]) { acc[orgId] = {}; } - acc[orgId][perm] = value; + acc[orgId][perm as OrganizationPermissionName] = value; return acc; }, - {} as Record, - ); + {} as Record>, + ) as Record; }, }; }; + +export const anyOrganizationPermissionsKey = [ + "authorization", + "anyOrganization", +]; + +export const anyOrganizationPermissions = () => { + return { + queryKey: anyOrganizationPermissionsKey, + queryFn: () => + API.checkAuthorization({ + checks: anyOrganizationPermissionChecks, + }) as Promise, + }; +}; + +export const getOrganizationIdpSyncClaimFieldValuesKey = ( + organization: string, + field: string, +) => [organization, "idpSync", "fieldValues", field]; + +export const organizationIdpSyncClaimFieldValues = ( + organization: string, + field: string, +) => { + return { + queryKey: getOrganizationIdpSyncClaimFieldValuesKey(organization, field), + queryFn: () => + API.getOrganizationIdpSyncClaimFieldValues(organization, field), + }; +}; diff --git a/site/src/api/queries/templates.ts b/site/src/api/queries/templates.ts index 8f6399cc4b354..2cd2d7693cfda 100644 --- a/site/src/api/queries/templates.ts +++ b/site/src/api/queries/templates.ts @@ -2,6 +2,7 @@ import { API, type GetTemplatesOptions, type GetTemplatesQuery } from "api/api"; import type { CreateTemplateRequest, CreateTemplateVersionRequest, + Preset, ProvisionerJob, ProvisionerJobStatus, Template, @@ -305,6 +306,13 @@ export const previousTemplateVersion = ( }; }; +export const templateVersionPresets = (versionId: string) => { + return { + queryKey: ["templateVersion", versionId, "presets"], + queryFn: () => API.getTemplateVersionPresets(versionId), + }; +}; + const waitBuildToBeFinished = async ( version: TemplateVersion, onRequest?: (data: TemplateVersion) => void, diff --git a/site/src/api/rbacresourcesGenerated.ts b/site/src/api/rbacresourcesGenerated.ts index 886f1b0239694..bfd1a46861090 100644 --- a/site/src/api/rbacresourcesGenerated.ts +++ b/site/src/api/rbacresourcesGenerated.ts @@ -15,18 +15,17 @@ export const RBACResourceActions: Partial< update: "update an api key, eg expires", }, assign_org_role: { - assign: "ability to assign org scoped roles", - create: "ability to create/delete custom roles within an organization", - delete: "ability to delete org scoped roles", - read: "view what roles are assignable", - update: "ability to edit custom roles within an organization", + assign: "assign org scoped roles", + create: "create/delete custom roles within an organization", + delete: "delete roles within an organization", + read: "view what roles are assignable within an organization", + unassign: "unassign org scoped roles", + update: "edit custom roles within an organization", }, assign_role: { - assign: "ability to assign roles", - create: "ability to create/delete/edit custom roles", - delete: "ability to unassign roles", + assign: "assign user roles", read: "view what roles are assignable", - update: "ability to edit custom roles", + unassign: "unassign user roles", }, audit_log: { create: "create new audit log entries", @@ -114,19 +113,14 @@ export const RBACResourceActions: Partial< update: "update an organization member", }, provisioner_daemon: { - create: "create a provisioner daemon", - delete: "delete a provisioner daemon", + create: "create a provisioner daemon/key", + delete: "delete a provisioner daemon/key", read: "read provisioner daemon", update: "update a provisioner daemon", }, provisioner_jobs: { read: "read provisioner jobs", }, - provisioner_keys: { - create: "create a provisioner key", - delete: "delete a provisioner key", - read: "read provisioner keys", - }, replicas: { read: "read replicas", }, @@ -168,6 +162,11 @@ export const RBACResourceActions: Partial< stop: "allows stopping a workspace", update: "edit workspace settings (scheduling, permissions, parameters)", }, + workspace_agent_resource_monitor: { + create: "create workspace agent resource monitor", + read: "read workspace agent resource monitor", + update: "update workspace agent resource monitor", + }, workspace_dormant: { application_connect: "connect to workspace apps via browser", create: "create a new workspace", diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index d5093587ad527..8c350d8f5bc31 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -115,10 +115,14 @@ export interface AssignableRoles extends Role { // From codersdk/audit.go export type AuditAction = + | "close" + | "connect" | "create" | "delete" + | "disconnect" | "login" | "logout" + | "open" | "register" | "request_password_reset" | "start" @@ -126,10 +130,14 @@ export type AuditAction = | "write"; export const AuditActions: AuditAction[] = [ + "close", + "connect", "create", "delete", + "disconnect", "login", "logout", + "open", "register", "request_password_reset", "start", @@ -192,7 +200,7 @@ export interface AuthMethod { export interface AuthMethods { readonly terms_of_service_url?: string; readonly password: AuthMethod; - readonly github: AuthMethod; + readonly github: GithubAuthMethod; readonly oidc: OIDCAuthMethod; } @@ -405,6 +413,7 @@ export interface CreateTestAuditLogRequest { readonly time?: string; readonly build_reason?: BuildReason; readonly organization_id?: string; + readonly request_id?: string; } // From codersdk/apikey.go @@ -907,6 +916,12 @@ export interface GitSSHKey { readonly public_key: string; } +// From codersdk/users.go +export interface GithubAuthMethod { + readonly enabled: boolean; + readonly default_provider_configured: boolean; +} + // From codersdk/groups.go export interface Group { readonly id: string; @@ -1055,6 +1070,12 @@ export interface HealthcheckReport { readonly coder_version: string; } +// From codersdk/idpsync.go +export interface IDPSyncMapping { + readonly Given: string; + readonly Gets: ResourceIdType; +} + // From codersdk/insights.go export type InsightsReportInterval = "day" | "week"; @@ -1101,6 +1122,10 @@ export interface License { // From codersdk/licenses.go export const LicenseExpiryClaim = "license_expires"; +// From codersdk/licenses.go +export const LicenseTelemetryRequiredErrorText = + "License requires telemetry but telemetry is disabled"; + // From codersdk/deployment.go export interface LinkConfig { readonly name: string; @@ -1297,10 +1322,17 @@ export interface OAuth2Config { readonly github: OAuth2GithubConfig; } +// From codersdk/oauth2.go +export interface OAuth2DeviceFlowCallbackResponse { + readonly redirect_url: string; +} + // From codersdk/deployment.go export interface OAuth2GithubConfig { readonly client_id: string; readonly client_secret: string; + readonly device_flow: boolean; + readonly default_provider_enable: boolean; readonly allowed_orgs: string; readonly allowed_teams: string; readonly allow_signups: boolean; @@ -1386,6 +1418,7 @@ export interface OIDCConfig { readonly email_field: string; readonly auth_url_params: SerpentStruct>; readonly ignore_user_info: boolean; + readonly source_user_info_from_access_token: boolean; readonly organization_field: string; readonly organization_mapping: SerpentStruct>; readonly organization_assign_default: boolean; @@ -1429,10 +1462,19 @@ export interface OrganizationMemberWithUserData extends OrganizationMember { readonly global_roles: readonly SlimRole[]; } +// From codersdk/organizations.go +export interface OrganizationProvisionerDaemonsOptions { + readonly Limit: number; + readonly IDs: readonly string[]; + readonly Tags: Record; +} + // From codersdk/organizations.go export interface OrganizationProvisionerJobsOptions { readonly Limit: number; + readonly IDs: readonly string[]; readonly Status: readonly ProvisionerJobStatus[]; + readonly Tags: Record; } // From codersdk/idpsync.go @@ -1449,6 +1491,19 @@ export interface Pagination { readonly offset?: number; } +// From codersdk/idpsync.go +export interface PatchGroupIDPSyncConfigRequest { + readonly field: string; + readonly regex_filter: string | null; + readonly auto_create_missing_groups: boolean; +} + +// From codersdk/idpsync.go +export interface PatchGroupIDPSyncMappingRequest { + readonly Add: readonly IDPSyncMapping[]; + readonly Remove: readonly IDPSyncMapping[]; +} + // From codersdk/groups.go export interface PatchGroupRequest { readonly add_users: readonly string[]; @@ -1459,6 +1514,29 @@ export interface PatchGroupRequest { readonly quota_allowance: number | null; } +// From codersdk/idpsync.go +export interface PatchOrganizationIDPSyncConfigRequest { + readonly field: string; + readonly assign_default: boolean; +} + +// From codersdk/idpsync.go +export interface PatchOrganizationIDPSyncMappingRequest { + readonly Add: readonly IDPSyncMapping[]; + readonly Remove: readonly IDPSyncMapping[]; +} + +// From codersdk/idpsync.go +export interface PatchRoleIDPSyncConfigRequest { + readonly field: string; +} + +// From codersdk/idpsync.go +export interface PatchRoleIDPSyncMappingRequest { + readonly Add: readonly IDPSyncMapping[]; + readonly Remove: readonly IDPSyncMapping[]; +} + // From codersdk/templateversions.go export interface PatchTemplateVersionRequest { readonly name: string; @@ -1508,6 +1586,19 @@ export interface PprofConfig { readonly address: string; } +// From codersdk/presets.go +export interface Preset { + readonly ID: string; + readonly Name: string; + readonly Parameters: readonly PresetParameter[]; +} + +// From codersdk/presets.go +export interface PresetParameter { + readonly Name: string; + readonly Value: string; +} + // From codersdk/deployment.go export interface PrometheusConfig { readonly enable: boolean; @@ -1549,6 +1640,9 @@ export interface ProvisionerDaemon { export interface ProvisionerDaemonJob { readonly id: string; readonly status: ProvisionerJobStatus; + readonly template_name: string; + readonly template_icon: string; + readonly template_display_name: string; } // From codersdk/client.go @@ -1596,6 +1690,7 @@ export interface ProvisionerJob { readonly input: ProvisionerJobInput; readonly type: ProvisionerJobType; readonly available_workers?: readonly string[]; + readonly metadata: ProvisionerJobMetadata; } // From codersdk/provisionerdaemons.go @@ -1615,6 +1710,17 @@ export interface ProvisionerJobLog { readonly output: string; } +// From codersdk/provisionerdaemons.go +export interface ProvisionerJobMetadata { + readonly template_version_name: string; + readonly template_id: string; + readonly template_name: string; + readonly template_display_name: string; + readonly template_icon: string; + readonly workspace_id?: string; + readonly workspace_name?: string; +} + // From codersdk/provisionerdaemons.go export type ProvisionerJobStatus = | "canceled" @@ -1750,6 +1856,7 @@ export type RBACAction = | "read" | "read_personal" | "ssh" + | "unassign" | "update" | "update_personal" | "use" @@ -1765,6 +1872,7 @@ export const RBACActions: RBACAction[] = [ "read", "read_personal", "ssh", + "unassign", "update", "update_personal", "use", @@ -1798,7 +1906,6 @@ export type RBACResource = | "organization_member" | "provisioner_daemon" | "provisioner_jobs" - | "provisioner_keys" | "replicas" | "system" | "tailnet_coordinator" @@ -1806,6 +1913,7 @@ export type RBACResource = | "user" | "*" | "workspace" + | "workspace_agent_resource_monitor" | "workspace_dormant" | "workspace_proxy"; @@ -1833,7 +1941,6 @@ export const RBACResources: RBACResource[] = [ "organization_member", "provisioner_daemon", "provisioner_jobs", - "provisioner_keys", "replicas", "system", "tailnet_coordinator", @@ -1841,6 +1948,7 @@ export const RBACResources: RBACResource[] = [ "user", "*", "workspace", + "workspace_agent_resource_monitor", "workspace_dormant", "workspace_proxy", ]; @@ -1925,6 +2033,8 @@ export type ResourceType = | "template_version" | "user" | "workspace" + | "workspace_agent" + | "workspace_app" | "workspace_build" | "workspace_proxy"; @@ -1949,6 +2059,8 @@ export const ResourceTypes: ResourceType[] = [ "template_version", "user", "workspace", + "workspace_agent", + "workspace_app", "workspace_build", "workspace_proxy", ]; @@ -1991,6 +2103,10 @@ export const RoleOrganizationTemplateAdmin = "organization-template-admin"; // From codersdk/rbacroles.go export const RoleOrganizationUserAdmin = "organization-user-admin"; +// From codersdk/rbacroles.go +export const RoleOrganizationWorkspaceCreationBan = + "organization-workspace-creation-ban"; + // From codersdk/rbacroles.go export const RoleOwner = "owner"; @@ -2871,6 +2987,19 @@ export interface WorkspaceAgent { readonly startup_script_behavior: WorkspaceAgentStartupScriptBehavior; } +// From codersdk/workspaceagents.go +export interface WorkspaceAgentDevcontainer { + readonly created_at: string; + readonly id: string; + readonly name: string; + readonly image: string; + readonly labels: Record; + readonly running: boolean; + readonly ports: readonly WorkspaceAgentListeningPort[]; + readonly status: string; + readonly volumes: Record; +} + // From codersdk/workspaceagents.go export interface WorkspaceAgentHealth { readonly healthy: boolean; @@ -2901,6 +3030,12 @@ export const WorkspaceAgentLifecycles: WorkspaceAgentLifecycle[] = [ "starting", ]; +// From codersdk/workspaceagents.go +export interface WorkspaceAgentListContainersResponse { + readonly containers: readonly WorkspaceAgentDevcontainer[]; + readonly warnings?: readonly string[]; +} + // From codersdk/workspaceagents.go export interface WorkspaceAgentListeningPort { readonly process_name: string; diff --git a/site/src/components/Alert/ErrorAlert.tsx b/site/src/components/Alert/ErrorAlert.tsx index 73d9c62480ab8..0198ea4e99540 100644 --- a/site/src/components/Alert/ErrorAlert.tsx +++ b/site/src/components/Alert/ErrorAlert.tsx @@ -1,6 +1,7 @@ import AlertTitle from "@mui/material/AlertTitle"; -import { getErrorDetail, getErrorMessage } from "api/errors"; +import { getErrorDetail, getErrorMessage, getErrorStatus } from "api/errors"; import type { FC } from "react"; +import { Link } from "../Link/Link"; import { Alert, AlertDetail, type AlertProps } from "./Alert"; export const ErrorAlert: FC< @@ -8,6 +9,7 @@ export const ErrorAlert: FC< > = ({ error, ...alertProps }) => { const message = getErrorMessage(error, "Something went wrong."); const detail = getErrorDetail(error); + const status = getErrorStatus(error); // For some reason, the message and detail can be the same on the BE, but does // not make sense in the FE to showing them duplicated @@ -15,14 +17,28 @@ export const ErrorAlert: FC< return ( - {detail ? ( - <> - {message} - {shouldDisplayDetail && {detail}} - - ) : ( - message - )} + { + // When the error is a Forbidden response we include a link for the user to + // go back to a known viewable page. + status === 403 ? ( + <> + {message} + + {detail}{" "} + + Go to workspaces + + + + ) : detail ? ( + <> + {message} + {shouldDisplayDetail && {detail}} + + ) : ( + message + ) + } ); }; diff --git a/site/src/components/Avatar/Avatar.tsx b/site/src/components/Avatar/Avatar.tsx index c09bfaddddf10..f5492158b4aad 100644 --- a/site/src/components/Avatar/Avatar.tsx +++ b/site/src/components/Avatar/Avatar.tsx @@ -57,7 +57,6 @@ const avatarVariants = cva( export type AvatarProps = AvatarPrimitive.AvatarProps & VariantProps & { src?: string; - fallback?: string; }; diff --git a/site/src/components/Badge/Badge.tsx b/site/src/components/Badge/Badge.tsx index 94d0fa9052340..2044db6d20614 100644 --- a/site/src/components/Badge/Badge.tsx +++ b/site/src/components/Badge/Badge.tsx @@ -7,16 +7,21 @@ import type { FC } from "react"; import { cn } from "utils/cn"; export const badgeVariants = cva( - "inline-flex items-center rounded-md border px-2.5 py-1 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + "inline-flex items-center rounded-md border px-2 py-1 transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", { variants: { variant: { default: "border-transparent bg-surface-secondary text-content-secondary shadow hover:bg-surface-tertiary", }, + size: { + sm: "text-2xs font-regular", + md: "text-xs font-medium", + }, }, defaultVariants: { variant: "default", + size: "md", }, }, ); @@ -25,8 +30,16 @@ export interface BadgeProps extends React.HTMLAttributes, VariantProps {} -export const Badge: FC = ({ className, variant, ...props }) => { +export const Badge: FC = ({ + className, + variant, + size, + ...props +}) => { return ( -
+
); }; diff --git a/site/src/components/Button/Button.stories.tsx b/site/src/components/Button/Button.stories.tsx index 3dc5001064f44..ceeb395cf8006 100644 --- a/site/src/components/Button/Button.stories.tsx +++ b/site/src/components/Button/Button.stories.tsx @@ -96,6 +96,7 @@ export const DestructiveSmall: Story = { export const IconButtonDefault: Story = { args: { variant: "default", + size: "icon", children: , }, }; @@ -103,6 +104,7 @@ export const IconButtonDefault: Story = { export const IconButtonOutline: Story = { args: { variant: "outline", + size: "icon", children: , }, }; @@ -110,6 +112,7 @@ export const IconButtonOutline: Story = { export const IconButtonSubtle: Story = { args: { variant: "subtle", + size: "icon", children: , }, }; diff --git a/site/src/components/Button/Button.tsx b/site/src/components/Button/Button.tsx index 93e1a479aa6cc..23803b89add15 100644 --- a/site/src/components/Button/Button.tsx +++ b/site/src/components/Button/Button.tsx @@ -9,7 +9,7 @@ import { cn } from "utils/cn"; export const buttonVariants = cva( `inline-flex items-center justify-center gap-1 whitespace-nowrap - border-solid rounded-md transition-colors min-w-20 + border-solid rounded-md transition-colors text-sm font-semibold font-medium cursor-pointer no-underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-content-link disabled:pointer-events-none disabled:text-content-disabled @@ -28,9 +28,9 @@ export const buttonVariants = cva( }, size: { - lg: "h-10 px-3 py-2 [&_svg]:size-icon-lg", - sm: "h-[30px] px-2 py-1.5 text-xs [&_svg]:size-icon-sm", - icon: "h-[30px] min-w-[30px] px-1 py-1.5 [&_svg]:size-icon-sm", + lg: "min-w-20 h-10 px-3 py-2 [&_svg]:size-icon-lg", + sm: "min-w-20 h-8 px-2 py-1.5 text-xs [&_svg]:size-icon-sm", + icon: "size-8 px-1.5 [&_svg]:size-icon-sm", }, }, defaultVariants: { diff --git a/site/src/components/CollapsibleSummary/CollapsibleSummary.stories.tsx b/site/src/components/CollapsibleSummary/CollapsibleSummary.stories.tsx new file mode 100644 index 0000000000000..98f63c24ccbc7 --- /dev/null +++ b/site/src/components/CollapsibleSummary/CollapsibleSummary.stories.tsx @@ -0,0 +1,120 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Button } from "../Button/Button"; +import { CollapsibleSummary } from "./CollapsibleSummary"; + +const meta: Meta = { + title: "components/CollapsibleSummary", + component: CollapsibleSummary, + args: { + label: "Advanced options", + children: ( + <> +
+ Option 1 +
+
+ Option 2 +
+
+ Option 3 +
+ + ), + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const DefaultOpen: Story = { + args: { + defaultOpen: true, + }, +}; + +export const MediumSize: Story = { + args: { + size: "md", + }, +}; + +export const SmallSize: Story = { + args: { + size: "sm", + }, +}; + +export const CustomClassName: Story = { + args: { + className: "text-blue-500 font-bold", + }, +}; + +export const ManyChildren: Story = { + args: { + defaultOpen: true, + children: ( + <> + {Array.from({ length: 10 }).map((_, i) => ( +
+ Option {i + 1} +
+ ))} + + ), + }, +}; + +export const NestedCollapsible: Story = { + args: { + defaultOpen: true, + children: ( + <> +
+ Option 1 +
+ +
+ Nested Option 1 +
+
+ Nested Option 2 +
+
+
+ Option 3 +
+ + ), + }, +}; + +export const ComplexContent: Story = { + args: { + defaultOpen: true, + children: ( +
+

Complex Content

+

+ This is a more complex content example with various elements. +

+
+ + +
+
+ ), + }, +}; + +export const LongLabel: Story = { + args: { + label: + "This is a very long label that might wrap or cause layout issues if not handled properly", + }, +}; diff --git a/site/src/components/CollapsibleSummary/CollapsibleSummary.tsx b/site/src/components/CollapsibleSummary/CollapsibleSummary.tsx new file mode 100644 index 0000000000000..675500685adf3 --- /dev/null +++ b/site/src/components/CollapsibleSummary/CollapsibleSummary.tsx @@ -0,0 +1,91 @@ +import { type VariantProps, cva } from "class-variance-authority"; +import { ChevronRightIcon } from "lucide-react"; +import { type FC, type ReactNode, useState } from "react"; +import { cn } from "utils/cn"; + +const collapsibleSummaryVariants = cva( + `flex items-center gap-1 p-0 bg-transparent border-0 text-inherit cursor-pointer + transition-colors text-content-secondary hover:text-content-primary font-medium + whitespace-nowrap`, + { + variants: { + size: { + md: "text-sm", + sm: "text-xs", + }, + }, + defaultVariants: { + size: "md", + }, + }, +); + +export interface CollapsibleSummaryProps + extends VariantProps { + /** + * The label to display for the collapsible section + */ + label: string; + /** + * The content to show when expanded + */ + children: ReactNode; + /** + * Whether the section is initially expanded + */ + defaultOpen?: boolean; + /** + * Optional className for the button + */ + className?: string; + /** + * The size of the component + */ + size?: "md" | "sm"; +} + +export const CollapsibleSummary: FC = ({ + label, + children, + defaultOpen = false, + className, + size, +}) => { + const [isOpen, setIsOpen] = useState(defaultOpen); + + return ( +
+ + + {isOpen &&
{children}
} +
+ ); +}; diff --git a/site/src/components/Combobox/Combobox.stories.tsx b/site/src/components/Combobox/Combobox.stories.tsx new file mode 100644 index 0000000000000..2786f35b0bf5e --- /dev/null +++ b/site/src/components/Combobox/Combobox.stories.tsx @@ -0,0 +1,132 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { expect, screen, userEvent, waitFor, within } from "@storybook/test"; +import { useState } from "react"; +import { Combobox } from "./Combobox"; + +const options = ["Option 1", "Option 2", "Option 3", "Another Option"]; + +const ComboboxWithHooks = () => { + const [value, setValue] = useState(""); + const [open, setOpen] = useState(false); + const [inputValue, setInputValue] = useState(""); + + return ( + { + if (e.key === "Enter" && inputValue && !options.includes(inputValue)) { + setValue(inputValue); + setInputValue(""); + setOpen(false); + } + }} + /> + ); +}; + +const meta: Meta = { + title: "components/Combobox", + component: Combobox, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: () => , +}; + +export const OpenCombobox: Story = { + render: () => , + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click(canvas.getByRole("button")); + + await waitFor(() => expect(screen.getByRole("dialog")).toBeInTheDocument()); + }, +}; + +export const SelectOption: Story = { + render: () => , + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click(canvas.getByRole("button")); + await userEvent.click(screen.getByText("Option 1")); + + await waitFor(() => + expect(canvas.getByRole("button")).toHaveTextContent("Option 1"), + ); + }, +}; + +export const SearchAndFilter: Story = { + render: () => , + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click(canvas.getByRole("button")); + await userEvent.type(screen.getByRole("combobox"), "Another"); + await userEvent.click( + screen.getByRole("option", { name: "Another Option" }), + ); + + await waitFor(() => { + expect( + screen.getByRole("option", { name: "Another Option" }), + ).toBeInTheDocument(); + expect( + screen.queryByRole("option", { name: "Option 1" }), + ).not.toBeInTheDocument(); + }); + }, +}; + +export const EnterCustomValue: Story = { + render: () => , + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click(canvas.getByRole("button")); + await userEvent.type(screen.getByRole("combobox"), "Custom Value{enter}"); + + await waitFor(() => + expect(canvas.getByRole("button")).toHaveTextContent("Custom Value"), + ); + }, +}; + +export const NoResults: Story = { + render: () => , + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click(canvas.getByRole("button")); + await userEvent.type(screen.getByRole("combobox"), "xyz"); + + await waitFor(() => { + expect(screen.getByText("No results found")).toBeInTheDocument(); + expect(screen.getByText("Enter custom value")).toBeInTheDocument(); + }); + }, +}; + +export const ClearSelectedOption: Story = { + render: () => , + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await userEvent.click(canvas.getByRole("button")); + // First select an option + await userEvent.click(screen.getByRole("option", { name: "Option 1" })); + // Then clear it by selecting it again + await userEvent.click(screen.getByRole("option", { name: "Option 1" })); + + await waitFor(() => + expect(canvas.getByRole("button")).toHaveTextContent("Select option"), + ); + }, +}; diff --git a/site/src/components/Combobox/Combobox.tsx b/site/src/components/Combobox/Combobox.tsx new file mode 100644 index 0000000000000..fa15b6808a05e --- /dev/null +++ b/site/src/components/Combobox/Combobox.tsx @@ -0,0 +1,93 @@ +import { Button } from "components/Button/Button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "components/Command/Command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "components/Popover/Popover"; +import { Check, ChevronDown, CornerDownLeft } from "lucide-react"; +import type { FC, KeyboardEventHandler } from "react"; +import { cn } from "utils/cn"; + +interface ComboboxProps { + value: string; + options?: readonly string[]; + placeholder?: string; + open: boolean; + onOpenChange: (open: boolean) => void; + inputValue: string; + onInputChange: (value: string) => void; + onKeyDown?: KeyboardEventHandler; + onSelect: (value: string) => void; +} + +export const Combobox: FC = ({ + value, + options = [], + placeholder = "Select option", + open, + onOpenChange, + inputValue, + onInputChange, + onKeyDown, + onSelect, +}) => { + return ( + + + + + + + + + +

No results found

+ + Enter custom value + + +
+ + {options.map((option) => ( + { + onSelect(currentValue === value ? "" : currentValue); + }} + > + {option} + {value === option && ( + + )} + + ))} + +
+
+
+
+ ); +}; diff --git a/site/src/components/Command/Command.tsx b/site/src/components/Command/Command.tsx index bbdc5684cb19d..018f3da237e48 100644 --- a/site/src/components/Command/Command.tsx +++ b/site/src/components/Command/Command.tsx @@ -53,7 +53,7 @@ export const CommandInput = forwardRef< (({ className, ...props }, ref) => ( )); diff --git a/site/src/components/Dialog/Dialog.tsx b/site/src/components/Dialog/Dialog.tsx index 0770ec62ae735..dde7dcae3b291 100644 --- a/site/src/components/Dialog/Dialog.tsx +++ b/site/src/components/Dialog/Dialog.tsx @@ -27,7 +27,7 @@ export const DialogOverlay = forwardRef< ({ query = "", values, menus, + used = false, }: { query?: string; values: Record; menus: Record; + used?: boolean; }) => ({ filter: { query, update: () => action("update"), debounceUpdate: action("debounce") as UseFilterResult["debounceUpdate"], - used: false, + used: used, values, }, menus, diff --git a/site/src/components/GitDeviceAuth/GitDeviceAuth.tsx b/site/src/components/GitDeviceAuth/GitDeviceAuth.tsx new file mode 100644 index 0000000000000..a8391de36622c --- /dev/null +++ b/site/src/components/GitDeviceAuth/GitDeviceAuth.tsx @@ -0,0 +1,136 @@ +import type { Interpolation, Theme } from "@emotion/react"; +import OpenInNewIcon from "@mui/icons-material/OpenInNew"; +import AlertTitle from "@mui/material/AlertTitle"; +import CircularProgress from "@mui/material/CircularProgress"; +import Link from "@mui/material/Link"; +import type { ApiErrorResponse } from "api/errors"; +import type { ExternalAuthDevice } from "api/typesGenerated"; +import { Alert, AlertDetail } from "components/Alert/Alert"; +import { CopyButton } from "components/CopyButton/CopyButton"; +import type { FC } from "react"; + +interface GitDeviceAuthProps { + externalAuthDevice?: ExternalAuthDevice; + deviceExchangeError?: ApiErrorResponse; +} + +export const GitDeviceAuth: FC = ({ + externalAuthDevice, + deviceExchangeError, +}) => { + let status = ( +

+ + Checking for authentication... +

+ ); + if (deviceExchangeError) { + // See https://datatracker.ietf.org/doc/html/rfc8628#section-3.5 + switch (deviceExchangeError.detail) { + case "authorization_pending": + break; + case "expired_token": + status = ( + + The one-time code has expired. Refresh to get a new one! + + ); + break; + case "access_denied": + status = ( + Access to the Git provider was denied. + ); + break; + default: + status = ( + + {deviceExchangeError.message} + {deviceExchangeError.detail && ( + {deviceExchangeError.detail} + )} + + ); + break; + } + } + + // If the error comes from the `externalAuthDevice` query, + // we cannot even display the user_code. + if (deviceExchangeError && !externalAuthDevice) { + return
{status}
; + } + + if (!externalAuthDevice) { + return ; + } + + return ( +
+

+ Copy your one-time code:  +

+ {externalAuthDevice.user_code} +   +
+
+ Then open the link below and paste it: +

+
+ + + Open and Paste + +
+ + {status} +
+ ); +}; + +const styles = { + text: (theme) => ({ + fontSize: 16, + color: theme.palette.text.secondary, + textAlign: "center", + lineHeight: "160%", + margin: 0, + }), + + copyCode: { + display: "inline-flex", + alignItems: "center", + }, + + code: (theme) => ({ + fontWeight: "bold", + color: theme.palette.text.primary, + }), + + links: { + display: "flex", + gap: 4, + margin: 16, + flexDirection: "column", + }, + + link: { + display: "flex", + alignItems: "center", + justifyContent: "center", + fontSize: 16, + gap: 8, + }, + + status: (theme) => ({ + display: "flex", + alignItems: "center", + justifyContent: "center", + gap: 8, + color: theme.palette.text.disabled, + }), +} satisfies Record>; diff --git a/site/src/components/IconField/EmojiPicker.tsx b/site/src/components/IconField/EmojiPicker.tsx index 291a2dc357723..476e24f293756 100644 --- a/site/src/components/IconField/EmojiPicker.tsx +++ b/site/src/components/IconField/EmojiPicker.tsx @@ -1,6 +1,11 @@ import data from "@emoji-mart/data/sets/15/apple.json"; import EmojiMart from "@emoji-mart/react"; -import type { ComponentProps, FC } from "react"; +import { + type ComponentProps, + type FC, + useEffect, + useLayoutEffect, +} from "react"; import icons from "theme/icons.json"; const custom = [ @@ -26,6 +31,23 @@ type EmojiPickerProps = Omit< >; const EmojiPicker: FC = (props) => { + /** + * Workaround for a bug in the emoji-mart library where custom emoji images render improperly. + * Setting the image width to 100% ensures they display correctly. + * + * Issue: https://github.com/missive/emoji-mart/issues/805 + * Open PR: https://github.com/missive/emoji-mart/pull/806 + */ + useEffect(() => { + const picker = document.querySelector("em-emoji-picker")?.shadowRoot; + if (!picker) { + return; + } + const css = document.createElement("style"); + css.textContent = ".emoji-mart-emoji img { width: 100% }"; + picker.appendChild(css); + }, []); + return ( = ({ className, ...props }) => ( xmlns="http://www.w3.org/2000/svg" > Coder logo - + diff --git a/site/src/components/Input/Input.tsx b/site/src/components/Input/Input.tsx index b50d6415a8983..9f3896a1f4f6d 100644 --- a/site/src/components/Input/Input.tsx +++ b/site/src/components/Input/Input.tsx @@ -18,7 +18,7 @@ export const Input = forwardRef< file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-content-primary placeholder:text-content-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-content-link - disabled:cursor-not-allowed disabled:opacity-50 md:text-sm`, + disabled:cursor-not-allowed disabled:opacity-50 md:text-sm text-inherit`, className, )} ref={ref} diff --git a/site/src/components/Latency/Latency.tsx b/site/src/components/Latency/Latency.tsx index 16e3199b331c3..706bf106876b5 100644 --- a/site/src/components/Latency/Latency.tsx +++ b/site/src/components/Latency/Latency.tsx @@ -10,9 +10,14 @@ import { getLatencyColor } from "utils/latency"; interface LatencyProps { latency?: number; isLoading?: boolean; + size?: number; } -export const Latency: FC = ({ latency, isLoading }) => { +export const Latency: FC = ({ + latency, + isLoading, + size = 14, +}) => { const theme = useTheme(); // Always use the no latency color for loading. const color = getLatencyColor(theme, isLoading ? undefined : latency); @@ -21,7 +26,7 @@ export const Latency: FC = ({ latency, isLoading }) => { return ( diff --git a/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx b/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx index 702be6a64d582..83f2aeed41cd4 100644 --- a/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx +++ b/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx @@ -572,7 +572,7 @@ export const MultiSelectCombobox = forwardRef< > - +
diff --git a/site/src/components/Select/Select.tsx b/site/src/components/Select/Select.tsx index a0da638c907a2..ececcc2fc9950 100644 --- a/site/src/components/Select/Select.tsx +++ b/site/src/components/Select/Select.tsx @@ -20,17 +20,18 @@ export const SelectTrigger = React.forwardRef< span]:line-clamp-1", + `flex h-10 w-full font-medium items-center justify-between whitespace-nowrap rounded-md + border border-border border-solid bg-transparent px-3 py-2 text-sm shadow-sm + ring-offset-background text-content-secondary placeholder:text-content-secondary focus:outline-none, + focus:ring-2 focus:ring-content-link disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 + focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-content-link`, className, )} {...props} > {children} - + )); @@ -65,7 +66,7 @@ export const SelectScrollDownButton = React.forwardRef< )} {...props} > - + )); SelectScrollDownButton.displayName = diff --git a/site/src/components/Sidebar/Sidebar.tsx b/site/src/components/Sidebar/Sidebar.tsx index 880ceecec2265..7e3b09d811b1b 100644 --- a/site/src/components/Sidebar/Sidebar.tsx +++ b/site/src/components/Sidebar/Sidebar.tsx @@ -3,7 +3,7 @@ import type { CSSObject, Interpolation, Theme } from "@emotion/react"; import { Stack } from "components/Stack/Stack"; import { type ClassName, useClassName } from "hooks/useClassName"; import type { ElementType, FC, ReactNode } from "react"; -import { Link, NavLink, useMatch } from "react-router-dom"; +import { Link, NavLink } from "react-router-dom"; import { cn } from "utils/cn"; interface SidebarProps { @@ -61,21 +61,16 @@ export const SettingsSidebarNavItem: FC = ({ href, end, }) => { - // 2025-01-10: useMatch is a workaround for a bug we encountered when you - // pass a render function to NavLink's className prop, and try to access - // NavLinks's isActive state value for the conditional styling. isActive - // wasn't always evaluating to true when it should be, but useMatch worked - const matchResult = useMatch(href); return ( + cn( + "relative text-sm text-content-secondary no-underline font-medium py-2 px-3 hover:bg-surface-secondary rounded-md transition ease-in-out duration-150", + isActive && "font-semibold text-content-primary", + ) + } > {children} diff --git a/site/src/components/StatusIndicator/StatusIndicator.stories.tsx b/site/src/components/StatusIndicator/StatusIndicator.stories.tsx index f3da964dbd00f..f291089916060 100644 --- a/site/src/components/StatusIndicator/StatusIndicator.stories.tsx +++ b/site/src/components/StatusIndicator/StatusIndicator.stories.tsx @@ -1,10 +1,17 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { StatusIndicator } from "./StatusIndicator"; +import { StatusIndicator, StatusIndicatorDot } from "./StatusIndicator"; const meta: Meta = { title: "components/StatusIndicator", component: StatusIndicator, - args: {}, + args: { + children: ( + <> + + Status + + ), + }, }; export default meta; @@ -12,52 +19,37 @@ type Story = StoryObj; export const Success: Story = { args: { - color: "success", - }, -}; - -export const SuccessOutline: Story = { - args: { - color: "success", - variant: "outlined", - }, -}; - -export const Warning: Story = { - args: { - color: "warning", + variant: "success", }, }; -export const WarningOutline: Story = { +export const Failed: Story = { args: { - color: "warning", - variant: "outlined", + variant: "failed", }, }; -export const Danger: Story = { +export const Inactive: Story = { args: { - color: "danger", + variant: "inactive", }, }; -export const DangerOutline: Story = { +export const Warning: Story = { args: { - color: "danger", - variant: "outlined", + variant: "warning", }, }; -export const Inactive: Story = { +export const Pending: Story = { args: { - color: "inactive", + variant: "pending", }, }; -export const InactiveOutline: Story = { +export const Small: Story = { args: { - color: "inactive", - variant: "outlined", + variant: "success", + size: "sm", }, }; diff --git a/site/src/components/StatusIndicator/StatusIndicator.tsx b/site/src/components/StatusIndicator/StatusIndicator.tsx index f69efe0d2d719..3ea9ca0dba7ac 100644 --- a/site/src/components/StatusIndicator/StatusIndicator.tsx +++ b/site/src/components/StatusIndicator/StatusIndicator.tsx @@ -1,33 +1,97 @@ -import { useTheme } from "@emotion/react"; -import type { FC } from "react"; -import type { ThemeRole } from "theme/roles"; +import { type VariantProps, cva } from "class-variance-authority"; +import { type FC, createContext, useContext } from "react"; +import { cn } from "utils/cn"; -interface StatusIndicatorProps { - color: ThemeRole; - variant?: "solid" | "outlined"; -} +const statusIndicatorVariants = cva( + "font-medium inline-flex items-center gap-2", + { + variants: { + variant: { + success: "text-content-success", + failed: "text-content-destructive", + inactive: "text-highlight-grey", + warning: "text-content-warning", + pending: "text-highlight-sky", + }, + size: { + sm: "text-xs", + md: "text-sm", + }, + }, + defaultVariants: { + variant: "success", + size: "md", + }, + }, +); + +type StatusIndicatorContextValue = VariantProps; + +const StatusIndicatorContext = createContext({}); + +export interface StatusIndicatorProps + extends React.HTMLAttributes, + StatusIndicatorContextValue {} export const StatusIndicator: FC = ({ - color, - variant = "solid", + size, + variant, + className, + ...props }) => { - const theme = useTheme(); + return ( + +
+ + ); +}; + +const dotVariants = cva("rounded-full inline-block border-4 border-solid", { + variants: { + variant: { + success: "bg-content-success border-surface-green", + failed: "bg-content-destructive border-surface-destructive", + inactive: "bg-highlight-grey border-surface-grey", + warning: "bg-content-warning border-surface-orange", + pending: "bg-highlight-sky border-surface-sky", + }, + size: { + sm: "size-3 border-4", + md: "size-4 border-4", + }, + }, + defaultVariants: { + variant: "success", + size: "md", + }, +}); + +export interface StatusIndicatorDotProps + extends React.HTMLAttributes, + VariantProps {} + +export const StatusIndicatorDot: FC = ({ + className, + // We allow the size and variant to be overridden directly by the component. + // This allows StatusIndicatorDot to be used alone. + size, + variant, + ...props +}) => { + const { size: ctxSize, variant: ctxVariant } = useContext( + StatusIndicatorContext, + ); return (
); }; diff --git a/site/src/components/Table/Table.stories.tsx b/site/src/components/Table/Table.stories.tsx new file mode 100644 index 0000000000000..41361f3ab59fe --- /dev/null +++ b/site/src/components/Table/Table.stories.tsx @@ -0,0 +1,90 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "./Table"; + +const invoices = [ + { + invoice: "INV001", + paymentStatus: "Paid", + totalAmount: "$250.00", + paymentMethod: "Credit Card", + }, + { + invoice: "INV002", + paymentStatus: "Pending", + totalAmount: "$150.00", + paymentMethod: "PayPal", + }, + { + invoice: "INV003", + paymentStatus: "Unpaid", + totalAmount: "$350.00", + paymentMethod: "Bank Transfer", + }, + { + invoice: "INV004", + paymentStatus: "Paid", + totalAmount: "$450.00", + paymentMethod: "Credit Card", + }, + { + invoice: "INV005", + paymentStatus: "Paid", + totalAmount: "$550.00", + paymentMethod: "PayPal", + }, + { + invoice: "INV006", + paymentStatus: "Pending", + totalAmount: "$200.00", + paymentMethod: "Bank Transfer", + }, + { + invoice: "INV007", + paymentStatus: "Unpaid", + totalAmount: "$300.00", + paymentMethod: "Credit Card", + }, +]; + +const meta: Meta = { + title: "components/Table", + component: Table, + args: { + children: ( + <> + + + Invoice + Status + Method + Amount + + + + {invoices.map((invoice) => ( + + {invoice.invoice} + {invoice.paymentStatus} + {invoice.paymentMethod} + + {invoice.totalAmount} + + + ))} + + + ), + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/site/src/components/Table/Table.tsx b/site/src/components/Table/Table.tsx new file mode 100644 index 0000000000000..604fc3d4f4196 --- /dev/null +++ b/site/src/components/Table/Table.tsx @@ -0,0 +1,117 @@ +/** + * Copied from shadc/ui on 02/03/2025 + * @see {@link https://ui.shadcn.com/docs/components/table} + */ + +import * as React from "react"; +import { cn } from "utils/cn"; + +export const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)); + +export const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); + +export const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr:first-child>td]:border-t [&>tr>td:first-child]:border-l", + "[&>tr:last-child>td]:border-b [&>tr>td:last-child]:border-r", + "[&>tr:first-child>td:first-child]:rounded-tl-md [&>tr:first-child>td:last-child]:rounded-tr-md", + "[&>tr:last-child>td:first-child]:rounded-bl-md [&>tr:last-child>td:last-child]:rounded-br-md", + className, + )} + {...props} + /> +)); + +export const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className, + )} + {...props} + /> +)); + +export const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); + +export const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
[role=checkbox]]:translate-y-[2px]", + className, + )} + {...props} + /> +)); + +export const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + [role=checkbox]]:translate-y-[2px]", + className, + )} + {...props} + /> +)); + +export const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); diff --git a/site/src/components/Tooltip/Tooltip.stories.tsx b/site/src/components/Tooltip/Tooltip.stories.tsx new file mode 100644 index 0000000000000..9af79ca76c099 --- /dev/null +++ b/site/src/components/Tooltip/Tooltip.stories.tsx @@ -0,0 +1,28 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Button } from "components/Button/Button"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "./Tooltip"; + +const meta: Meta = { + title: "components/Tooltip", + component: TooltipProvider, + args: { + children: ( + + + + + Add to library + + ), + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/site/src/components/Tooltip/Tooltip.tsx b/site/src/components/Tooltip/Tooltip.tsx new file mode 100644 index 0000000000000..52f31299f1721 --- /dev/null +++ b/site/src/components/Tooltip/Tooltip.tsx @@ -0,0 +1,34 @@ +import * as TooltipPrimitive from "@radix-ui/react-tooltip"; +/** + * Copied from shadc/ui on 02/05/2025 + * @see {@link https://ui.shadcn.com/docs/components/tooltip} + */ +import * as React from "react"; +import { cn } from "utils/cn"; + +export const TooltipProvider = TooltipPrimitive.Provider; + +export const Tooltip = TooltipPrimitive.Root; + +export const TooltipTrigger = TooltipPrimitive.Trigger; + +export const TooltipContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)); diff --git a/site/src/contexts/auth/RequirePermission.tsx b/site/src/contexts/auth/RequirePermission.tsx index 50dbd0232ab88..6e4b0f3aac186 100644 --- a/site/src/contexts/auth/RequirePermission.tsx +++ b/site/src/contexts/auth/RequirePermission.tsx @@ -1,5 +1,13 @@ +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "components/Dialog/Dialog"; +import { Link } from "components/Link/Link"; import type { FC, ReactNode } from "react"; -import { Navigate } from "react-router-dom"; export interface RequirePermissionProps { children?: ReactNode; @@ -14,7 +22,24 @@ export const RequirePermission: FC = ({ isFeatureVisible, }) => { if (!isFeatureVisible) { - return ; + return ( + + + + + You don't have permission to view this page + + + + If you believe this is a mistake, please contact your administrator + or try signing in with different credentials. + + + Go to workspaces + + + + ); } return <>{children}; diff --git a/site/src/contexts/auth/permissions.tsx b/site/src/contexts/auth/permissions.tsx index b44d85e963fe4..1043862942edb 100644 --- a/site/src/contexts/auth/permissions.tsx +++ b/site/src/contexts/auth/permissions.tsx @@ -16,7 +16,6 @@ export const checks = { readWorkspaceProxies: "readWorkspaceProxies", editWorkspaceProxies: "editWorkspaceProxies", createOrganization: "createOrganization", - editAnyOrganization: "editAnyOrganization", viewAnyGroup: "viewAnyGroup", createGroup: "createGroup", viewAllLicenses: "viewAllLicenses", @@ -122,13 +121,6 @@ export const permissionsToCheck = { }, action: "create", }, - [checks.editAnyOrganization]: { - object: { - resource_type: "organization", - any_org: true, - }, - action: "update", - }, [checks.viewAnyGroup]: { object: { resource_type: "group", diff --git a/site/src/index.css b/site/src/index.css index 3dd12935c8c22..a5806bdc98a14 100644 --- a/site/src/index.css +++ b/site/src/index.css @@ -15,8 +15,8 @@ --content-invert: 0 0% 98%; --content-disabled: 240 5% 65%; --content-success: 142 72% 29%; - --content-danger: 0 84% 60%; --content-warning: 27 96% 61%; + --content-destructive: 0 84% 60%; --surface-primary: 0 0% 98%; --surface-secondary: 240 5% 96%; --surface-tertiary: 240 6% 90%; @@ -24,12 +24,19 @@ --surface-invert-primary: 240 4% 16%; --surface-invert-secondary: 240 5% 26%; --surface-destructive: 0 93% 94%; + --surface-green: 141 79% 85%; + --surface-grey: 240 5% 96%; + --surface-orange: 34 100% 92%; + --surface-sky: 201 94% 86%; --border-default: 240 6% 90%; --border-success: 142 76% 36%; --border-destructive: 0 84% 60%; + --overlay-default: 240 5% 84% / 80%; --radius: 0.5rem; --highlight-purple: 262 83% 58%; --highlight-green: 143 64% 24%; + --highlight-grey: 240 5% 65%; + --highlight-sky: 201 90% 27%; --border: 240 5.9% 90%; --input: 240 5.9% 90%; --ring: 240 10% 3.9%; @@ -44,8 +51,8 @@ --content-invert: 240 10% 4%; --content-disabled: 240 5% 26%; --content-success: 142 76% 36%; - --content-danger: 0 91% 71%; - --content-warning: 27 96% 61%; + --content-warning: 31 97% 72%; + --content-destructive: 0 91% 71%; --surface-primary: 240 10% 4%; --surface-secondary: 240 6% 10%; --surface-tertiary: 240 4% 16%; @@ -53,11 +60,18 @@ --surface-invert-primary: 240 6% 90%; --surface-invert-secondary: 240 5% 65%; --surface-destructive: 0 75% 15%; + --surface-green: 145 80% 10%; + --surface-grey: 240 6% 10%; + --surface-orange: 13 81% 15%; + --surface-sky: 204 80% 16%; --border-default: 240 4% 16%; --border-success: 142 76% 36%; --border-destructive: 0 91% 71%; + --overlay-default: 240 10% 4% / 80%; --highlight-purple: 252 95% 85%; --highlight-green: 141 79% 85%; + --highlight-grey: 240 4% 46%; + --highlight-sky: 198 93% 60%; --border: 240 3.7% 15.9%; --input: 240 3.7% 15.9%; --ring: 240 4.9% 83.9%; diff --git a/site/src/modules/dashboard/DashboardProvider.tsx b/site/src/modules/dashboard/DashboardProvider.tsx index d8fa339deccbb..bf8e307206aea 100644 --- a/site/src/modules/dashboard/DashboardProvider.tsx +++ b/site/src/modules/dashboard/DashboardProvider.tsx @@ -1,7 +1,10 @@ import { appearance } from "api/queries/appearance"; import { entitlements } from "api/queries/entitlements"; import { experiments } from "api/queries/experiments"; -import { organizations } from "api/queries/organizations"; +import { + anyOrganizationPermissions, + organizations, +} from "api/queries/organizations"; import type { AppearanceConfig, Entitlements, @@ -11,6 +14,7 @@ import type { import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Loader } from "components/Loader/Loader"; import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata"; +import { canViewAnyOrganization } from "modules/management/organizationPermissions"; import { type FC, type PropsWithChildren, createContext } from "react"; import { useQuery } from "react-query"; import { selectFeatureVisibility } from "./entitlements"; @@ -21,6 +25,7 @@ export interface DashboardValue { appearance: AppearanceConfig; organizations: readonly Organization[]; showOrganizations: boolean; + canViewOrganizationSettings: boolean; } export const DashboardContext = createContext( @@ -33,12 +38,16 @@ export const DashboardProvider: FC = ({ children }) => { const experimentsQuery = useQuery(experiments(metadata.experiments)); const appearanceQuery = useQuery(appearance(metadata.appearance)); const organizationsQuery = useQuery(organizations()); + const anyOrganizationPermissionsQuery = useQuery( + anyOrganizationPermissions(), + ); const error = entitlementsQuery.error || appearanceQuery.error || experimentsQuery.error || - organizationsQuery.error; + organizationsQuery.error || + anyOrganizationPermissionsQuery.error; if (error) { return ; @@ -48,7 +57,8 @@ export const DashboardProvider: FC = ({ children }) => { !entitlementsQuery.data || !appearanceQuery.data || !experimentsQuery.data || - !organizationsQuery.data; + !organizationsQuery.data || + !anyOrganizationPermissionsQuery.data; if (isLoading) { return ; @@ -58,6 +68,7 @@ export const DashboardProvider: FC = ({ children }) => { const organizationsEnabled = selectFeatureVisibility( entitlementsQuery.data, ).multiple_organizations; + const showOrganizations = hasMultipleOrganizations || organizationsEnabled; return ( = ({ children }) => { experiments: experimentsQuery.data, appearance: appearanceQuery.data, organizations: organizationsQuery.data, - showOrganizations: hasMultipleOrganizations || organizationsEnabled, + showOrganizations, + canViewOrganizationSettings: + showOrganizations && + canViewAnyOrganization(anyOrganizationPermissionsQuery.data), }} > {children} diff --git a/site/src/modules/dashboard/LicenseBanner/LicenseBannerView.tsx b/site/src/modules/dashboard/LicenseBanner/LicenseBannerView.tsx index 8ebcb216f51cd..7803f1dc828b1 100644 --- a/site/src/modules/dashboard/LicenseBanner/LicenseBannerView.tsx +++ b/site/src/modules/dashboard/LicenseBanner/LicenseBannerView.tsx @@ -6,6 +6,7 @@ import { useTheme, } from "@emotion/react"; import Link from "@mui/material/Link"; +import { LicenseTelemetryRequiredErrorText } from "api/typesGenerated"; import { Expander } from "components/Expander/Expander"; import { Pill } from "components/Pill/Pill"; import { type FC, useState } from "react"; @@ -14,6 +15,7 @@ export const Language = { licenseIssue: "License Issue", licenseIssues: (num: number): string => `${num} License Issues`, upgrade: "Contact sales@coder.com.", + exception: "Contact sales@coder.com if you need an exception.", exceeded: "It looks like you've exceeded some limits of your license.", lessDetails: "Less", moreDetails: "More", @@ -26,6 +28,14 @@ const styles = { }, } satisfies Record>; +const formatMessage = (message: string) => { + // If the message ends with an alphanumeric character, add a period. + if (/[a-z0-9]$/i.test(message)) { + return `${message}.`; + } + return message; +}; + export interface LicenseBannerViewProps { errors: readonly string[]; warnings: readonly string[]; @@ -57,14 +67,16 @@ export const LicenseBannerView: FC = ({
{Language.licenseIssue}
- {messages[0]} + {formatMessage(messages[0])}   - {Language.upgrade} + {messages[0] === LicenseTelemetryRequiredErrorText + ? Language.exception + : Language.upgrade}
@@ -90,7 +102,7 @@ export const LicenseBannerView: FC = ({
    {messages.map((message) => (
  • - {message} + {formatMessage(message)}
  • ))}
diff --git a/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx b/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx index d1a75c02cd315..746ddc8f89e78 100644 --- a/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx +++ b/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx @@ -97,7 +97,6 @@ const DeploymentDropdownContent: FC = ({ onClick={onPopoverClose} > Organizations - )} {canViewAuditLog && ( diff --git a/site/src/modules/dashboard/Navbar/MobileMenu.test.tsx b/site/src/modules/dashboard/Navbar/MobileMenu.test.tsx new file mode 100644 index 0000000000000..ce8a29df78fc4 --- /dev/null +++ b/site/src/modules/dashboard/Navbar/MobileMenu.test.tsx @@ -0,0 +1,22 @@ +import { includeOrigin } from "./MobileMenu"; + +const mockOrigin = "https://example.com"; + +describe("support link", () => { + it("should include origin if target starts with '/'", () => { + (window as unknown as { location: Partial }).location = { + origin: mockOrigin, + }; // Mock the location origin + + expect(includeOrigin("/test")).toBe(`${mockOrigin}/test`); + expect(includeOrigin("/path/to/resource")).toBe( + `${mockOrigin}/path/to/resource`, + ); + }); + + it("should return the target unchanged if it does not start with '/'", () => { + expect(includeOrigin(`${mockOrigin}/page`)).toBe(`${mockOrigin}/page`); + expect(includeOrigin("../relative/path")).toBe("../relative/path"); + expect(includeOrigin("relative/path")).toBe("relative/path"); + }); +}); diff --git a/site/src/modules/dashboard/Navbar/MobileMenu.tsx b/site/src/modules/dashboard/Navbar/MobileMenu.tsx index eb353d8d0b79e..20058335eb8e5 100644 --- a/site/src/modules/dashboard/Navbar/MobileMenu.tsx +++ b/site/src/modules/dashboard/Navbar/MobileMenu.tsx @@ -228,14 +228,7 @@ const AdminSettingsSub: FC = ({ asChild className={cn(itemStyles.default, itemStyles.sub)} > - - Organizations - - + Organizations )} {canViewAuditLog && ( @@ -314,7 +307,11 @@ const UserSettingsSub: FC = ({ asChild className={cn(itemStyles.default, itemStyles.sub)} > - + {l.name} @@ -325,3 +322,11 @@ const UserSettingsSub: FC = ({ ); }; + +export const includeOrigin = (target: string): string => { + if (target.startsWith("/")) { + const baseUrl = window.location.origin; + return `${baseUrl}${target}`; + } + return target; +}; diff --git a/site/src/modules/dashboard/Navbar/Navbar.tsx b/site/src/modules/dashboard/Navbar/Navbar.tsx index fa249f3a7f004..f80887e1f1aec 100644 --- a/site/src/modules/dashboard/Navbar/Navbar.tsx +++ b/site/src/modules/dashboard/Navbar/Navbar.tsx @@ -12,14 +12,13 @@ export const Navbar: FC = () => { const { metadata } = useEmbeddedMetadata(); const buildInfoQuery = useQuery(buildInfo(metadata["build-info"])); - const { appearance, showOrganizations } = useDashboard(); + const { appearance, canViewOrganizationSettings } = useDashboard(); const { user: me, permissions, signOut } = useAuthenticated(); const featureVisibility = useFeatureVisibility(); const canViewAuditLog = - featureVisibility.audit_log && Boolean(permissions.viewAnyAuditLog); - const canViewDeployment = Boolean(permissions.viewDeploymentValues); - const canViewOrganizations = - Boolean(permissions.editAnyOrganization) && showOrganizations; + featureVisibility.audit_log && permissions.viewAnyAuditLog; + const canViewDeployment = permissions.viewDeploymentValues; + const canViewOrganizations = canViewOrganizationSettings; const proxyContextValue = useProxy(); const canViewHealth = canViewDeployment; diff --git a/site/src/modules/dashboard/Navbar/ProxyMenu.tsx b/site/src/modules/dashboard/Navbar/ProxyMenu.tsx index 5345d3db9cdae..abbfbd5fd82f3 100644 --- a/site/src/modules/dashboard/Navbar/ProxyMenu.tsx +++ b/site/src/modules/dashboard/Navbar/ProxyMenu.tsx @@ -99,6 +99,7 @@ export const ProxyMenu: FC = ({ proxyContextValue }) => { ) : ( diff --git a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx index 90ea1dab74a67..9eb89407dea31 100644 --- a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx +++ b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx @@ -22,6 +22,7 @@ import { Stack } from "components/Stack/Stack"; import { usePopover } from "components/deprecated/Popover/Popover"; import type { FC } from "react"; import { Link } from "react-router-dom"; + export const Language = { accountLabel: "Account", signOutLabel: "Sign Out", @@ -129,7 +130,7 @@ export const UserDropdownContent: FC = ({ - {Boolean(buildInfo?.deployment_id) && ( + {buildInfo?.deployment_id && (
= ({ text-overflow: ellipsis; `} > - {buildInfo?.deployment_id} + {buildInfo.deployment_id}
{ const { permissions } = useAuthenticated(); + const { entitlements, showOrganizations } = useDashboard(); + const hasPremiumLicense = + entitlements.features.multiple_organizations.enabled; - return ; + return ( + + ); }; diff --git a/site/src/modules/management/DeploymentSidebarView.tsx b/site/src/modules/management/DeploymentSidebarView.tsx index 052dcf8329b11..21ff6f84b4a48 100644 --- a/site/src/modules/management/DeploymentSidebarView.tsx +++ b/site/src/modules/management/DeploymentSidebarView.tsx @@ -1,44 +1,18 @@ -import type { AuthorizationResponse, Organization } from "api/typesGenerated"; import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge"; import { Sidebar as BaseSidebar, SettingsSidebarNavItem as SidebarNavItem, } from "components/Sidebar/Sidebar"; +import { Stack } from "components/Stack/Stack"; import type { Permissions } from "contexts/auth/permissions"; -import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; +import { ArrowUpRight } from "lucide-react"; import type { FC } from "react"; -export interface OrganizationWithPermissions extends Organization { - permissions: AuthorizationResponse; -} - -interface DeploymentSidebarProps { +interface DeploymentSidebarViewProps { /** Site-wide permissions. */ permissions: Permissions; -} - -/** - * A combined deployment settings and organization menu. - */ -export const DeploymentSidebarView: FC = ({ - permissions, -}) => { - const { multiple_organizations: hasPremiumLicense } = useFeatureVisibility(); - - return ( - - - - ); -}; - -interface DeploymentSettingsNavigationProps { - /** Site-wide permissions. */ - permissions: Permissions; - isPremium: boolean; + showOrganizations: boolean; + hasPremiumLicense: boolean; } /** @@ -48,12 +22,13 @@ interface DeploymentSettingsNavigationProps { * Menu items are shown based on the permissions. If organizations can be * viewed, groups are skipped since they will show under each org instead. */ -const DeploymentSettingsNavigation: FC = ({ +export const DeploymentSidebarView: FC = ({ permissions, - isPremium, + showOrganizations, + hasPremiumLicense, }) => { return ( -
+
{permissions.viewDeploymentValues && ( General @@ -100,7 +75,11 @@ const DeploymentSettingsNavigation: FC = ({ Users )} {permissions.viewAnyGroup && ( - Groups + + + Groups {showOrganizations && } + + )} {permissions.viewNotificationTemplate && ( @@ -115,10 +94,15 @@ const DeploymentSettingsNavigation: FC = ({ IdP Organization Sync )} - {!isPremium && ( + {permissions.viewDeploymentValues && ( + + Provisioners + + )} + {!hasPremiumLicense && ( Premium )}
-
+ ); }; diff --git a/site/src/modules/management/OrganizationSettingsLayout.tsx b/site/src/modules/management/OrganizationSettingsLayout.tsx index d2d25cc4a41bd..ae1ce597641ae 100644 --- a/site/src/modules/management/OrganizationSettingsLayout.tsx +++ b/site/src/modules/management/OrganizationSettingsLayout.tsx @@ -1,20 +1,24 @@ -import type { AuthorizationResponse, Organization } from "api/typesGenerated"; +import { organizationsPermissions } from "api/queries/organizations"; +import type { Organization } from "api/typesGenerated"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Avatar } from "components/Avatar/Avatar"; import { Breadcrumb, BreadcrumbItem, - BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, } from "components/Breadcrumb/Breadcrumb"; -import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge"; import { Loader } from "components/Loader/Loader"; -import { useAuthenticated } from "contexts/auth/RequireAuth"; -import { RequirePermission } from "contexts/auth/RequirePermission"; import { useDashboard } from "modules/dashboard/useDashboard"; +import NotFoundPage from "pages/404Page/404Page"; import { type FC, Suspense, createContext, useContext } from "react"; +import { useQuery } from "react-query"; import { Outlet, useParams } from "react-router-dom"; +import { + type OrganizationPermissions, + canViewOrganization, +} from "./organizationPermissions"; export const OrganizationSettingsContext = createContext< OrganizationSettingsValue | undefined @@ -22,7 +26,12 @@ export const OrganizationSettingsContext = createContext< type OrganizationSettingsValue = Readonly<{ organizations: readonly Organization[]; + organizationPermissionsByOrganizationId: Record< + string, + OrganizationPermissions + >; organization?: Organization; + organizationPermissions?: OrganizationPermissions; }>; export const useOrganizationSettings = (): OrganizationSettingsValue => { @@ -36,83 +45,89 @@ export const useOrganizationSettings = (): OrganizationSettingsValue => { return context; }; -/** - * Return true if the user can edit the organization settings or its members. - */ -export const canEditOrganization = ( - permissions: AuthorizationResponse | undefined, -) => { - return ( - permissions !== undefined && - (permissions.editOrganization || - permissions.editMembers || - permissions.editGroups) - ); -}; - const OrganizationSettingsLayout: FC = () => { - const { permissions } = useAuthenticated(); - const { organizations } = useDashboard(); + const { organizations, showOrganizations } = useDashboard(); const { organization: orgName } = useParams() as { organization?: string; }; - const canViewOrganizationSettingsPage = - permissions.viewDeploymentValues || permissions.editAnyOrganization; + const organization = orgName + ? organizations.find((org) => org.name === orgName) + : undefined; + + const orgPermissionsQuery = useQuery( + organizationsPermissions(organizations?.map((o) => o.id)), + ); + + if (orgPermissionsQuery.isError) { + return ; + } + + if (!orgPermissionsQuery.data) { + return ; + } - const organization = - organizations && orgName - ? organizations.find((org) => org.name === orgName) - : undefined; + const viewableOrganizations = organizations.filter((org) => + canViewOrganization(orgPermissionsQuery.data?.[org.id]), + ); + + // It's currently up to each individual page to show an empty state if there + // is no matching organization. This is weird and we should probably fix it + // eventually, but if we handled it here it would break the /new route, and + // refactoring to fix _that_ is a non-trivial amount of work. + const organizationPermissions = + organization && orgPermissionsQuery.data?.[organization.id]; + if (organization && !canViewOrganization(organizationPermissions)) { + return ; + } return ( - - -
- - - - Admin Settings - - - - - Organizations - - - - {organization && ( - <> - - - - - {organization?.name} - - - - )} - - -
-
- }> - - -
+ +
+ + + + Admin Settings + + + + + Organizations + + + {organization && ( + <> + + + + + {organization.display_name} + + + + )} + + +
+
+ }> + +
- - +
+
); }; diff --git a/site/src/modules/management/OrganizationSidebar.tsx b/site/src/modules/management/OrganizationSidebar.tsx index 8ef14f9baf165..3b6451b0252bc 100644 --- a/site/src/modules/management/OrganizationSidebar.tsx +++ b/site/src/modules/management/OrganizationSidebar.tsx @@ -1,59 +1,25 @@ -import { organizationsPermissions } from "api/queries/organizations"; +import { Sidebar as BaseSidebar } from "components/Sidebar/Sidebar"; import { useAuthenticated } from "contexts/auth/RequireAuth"; -import { - canEditOrganization, - useOrganizationSettings, -} from "modules/management/OrganizationSettingsLayout"; +import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; import type { FC } from "react"; -import { useQuery } from "react-query"; -import { useParams } from "react-router-dom"; -import { - OrganizationSidebarView, - type OrganizationWithPermissions, -} from "./OrganizationSidebarView"; +import { OrganizationSidebarView } from "./OrganizationSidebarView"; /** - * A combined deployment settings and organization menu. - * - * This should only be used with multi-org support. If multi-org support is - * disabled or not licensed, this is the wrong sidebar to use. See - * DeploySettingsPage/Sidebar instead. + * Sidebar for the OrganizationSettingsLayout */ export const OrganizationSidebar: FC = () => { const { permissions } = useAuthenticated(); - const { organizations } = useOrganizationSettings(); - const { organization: organizationName } = useParams() as { - organization?: string; - }; - - const orgPermissionsQuery = useQuery( - organizationsPermissions(organizations?.map((o) => o.id)), - ); - - // Sometimes a user can read an organization but cannot actually do anything - // with it. For now, these are filtered out so you only see organizations you - // can manage in some way. - const editableOrgs = organizations - ?.map((org) => { - return { - ...org, - permissions: orgPermissionsQuery.data?.[org.id], - }; - }) - // TypeScript is not able to infer whether permissions are defined on the - // object even if we explicitly check org.permissions here, so add the `is` - // here to help out (canEditOrganization does the actual check). - .filter((org): org is OrganizationWithPermissions => { - return canEditOrganization(org.permissions); - }); - - const organization = editableOrgs?.find((o) => o.name === organizationName); + const { organizations, organization, organizationPermissions } = + useOrganizationSettings(); return ( - + + + ); }; diff --git a/site/src/modules/management/OrganizationSidebarView.stories.tsx b/site/src/modules/management/OrganizationSidebarView.stories.tsx index 4f1b17a27c181..0a3ebef493239 100644 --- a/site/src/modules/management/OrganizationSidebarView.stories.tsx +++ b/site/src/modules/management/OrganizationSidebarView.stories.tsx @@ -1,9 +1,12 @@ import type { Meta, StoryObj } from "@storybook/react"; import { expect, userEvent, waitFor, within } from "@storybook/test"; +import type { Organization } from "api/typesGenerated"; import { + MockNoOrganizationPermissions, MockNoPermissions, MockOrganization, MockOrganization2, + MockOrganizationPermissions, MockPermissions, } from "testHelpers/entities"; import { withDashboardProvider } from "testHelpers/storybook"; @@ -16,26 +19,7 @@ const meta: Meta = { parameters: { showOrganizations: true }, args: { activeOrganization: undefined, - organizations: [ - { - ...MockOrganization, - permissions: { - editOrganization: true, - editMembers: true, - editGroups: true, - auditOrganization: true, - }, - }, - { - ...MockOrganization2, - permissions: { - editOrganization: true, - editMembers: true, - editGroups: true, - auditOrganization: true, - }, - }, - ], + organizations: [MockOrganization, MockOrganization2], permissions: MockPermissions, }, }; @@ -43,18 +27,10 @@ const meta: Meta = { export default meta; type Story = StoryObj; -export const LoadingOrganizations: Story = { - args: { - organizations: undefined, - }, -}; - export const NoCreateOrg: Story = { args: { - activeOrganization: { - ...MockOrganization, - permissions: { createOrganization: false }, - }, + activeOrganization: MockOrganization, + orgPermissions: MockNoOrganizationPermissions, permissions: { ...MockPermissions, createOrganization: false, @@ -73,23 +49,15 @@ export const NoCreateOrg: Story = { export const OverflowDropdown: Story = { args: { - activeOrganization: { - ...MockOrganization, - permissions: { createOrganization: true }, - }, + activeOrganization: MockOrganization, + orgPermissions: MockOrganizationPermissions, permissions: { ...MockPermissions, createOrganization: true, }, organizations: [ - { - ...MockOrganization, - permissions: {}, - }, - { - ...MockOrganization2, - permissions: {}, - }, + MockOrganization, + MockOrganization2, { id: "my-organization-3-id", name: "my-organization-3", @@ -99,7 +67,6 @@ export const OverflowDropdown: Story = { created_at: "", updated_at: "", is_default: false, - permissions: {}, }, { id: "my-organization-4-id", @@ -110,7 +77,6 @@ export const OverflowDropdown: Story = { created_at: "", updated_at: "", is_default: false, - permissions: {}, }, { id: "my-organization-5-id", @@ -121,7 +87,6 @@ export const OverflowDropdown: Story = { created_at: "", updated_at: "", is_default: false, - permissions: {}, }, { id: "my-organization-6-id", @@ -132,7 +97,6 @@ export const OverflowDropdown: Story = { created_at: "", updated_at: "", is_default: false, - permissions: {}, }, { id: "my-organization-7-id", @@ -143,7 +107,6 @@ export const OverflowDropdown: Story = { created_at: "", updated_at: "", is_default: false, - permissions: {}, }, ], }, @@ -155,134 +118,191 @@ export const OverflowDropdown: Story = { }, }; +export const NoOrganizations: Story = { + args: { + organizations: [], + activeOrganization: undefined, + orgPermissions: MockNoOrganizationPermissions, + permissions: MockNoPermissions, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click( + canvas.getByRole("button", { name: /No organization selected/i }), + ); + }, +}; + +export const NoOtherOrganizations: Story = { + args: { + organizations: [MockOrganization], + activeOrganization: MockOrganization, + orgPermissions: MockNoOrganizationPermissions, + permissions: MockNoPermissions, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click( + canvas.getByRole("button", { name: /My Organization/i }), + ); + }, +}; + export const NoPermissions: Story = { args: { - activeOrganization: { - ...MockOrganization, - permissions: MockNoPermissions, - }, + activeOrganization: MockOrganization, + orgPermissions: MockNoOrganizationPermissions, permissions: MockNoPermissions, }, }; export const AllPermissions: Story = { args: { - activeOrganization: { - ...MockOrganization, - permissions: { - editOrganization: true, - editMembers: true, - editGroups: true, - auditOrganization: true, - assignOrgRole: true, - viewProvisioners: true, - viewIdpSyncSettings: true, - }, - }, - organizations: [ - { - ...MockOrganization, - permissions: { - editOrganization: true, - editMembers: true, - editGroups: true, - auditOrganization: true, - assignOrgRole: true, - viewProvisioners: true, - viewIdpSyncSettings: true, - }, - }, - ], + activeOrganization: MockOrganization, + orgPermissions: MockOrganizationPermissions, + organizations: [MockOrganization], }, }; export const SelectedOrgAdmin: Story = { args: { - activeOrganization: { - ...MockOrganization, - permissions: { - editOrganization: true, - editMembers: true, - editGroups: true, - auditOrganization: true, - assignOrgRole: true, - }, - }, - organizations: [ - { - ...MockOrganization, - permissions: { - editOrganization: true, - editMembers: true, - editGroups: true, - auditOrganization: true, - assignOrgRole: true, - }, - }, - ], + activeOrganization: MockOrganization, + orgPermissions: MockOrganizationPermissions, + organizations: [MockOrganization], }, }; export const SelectedOrgAuditor: Story = { args: { - activeOrganization: { - ...MockOrganization, - permissions: { - editOrganization: false, - editMembers: false, - editGroups: false, - auditOrganization: true, - }, - }, + activeOrganization: MockOrganization, + orgPermissions: MockNoOrganizationPermissions, permissions: { ...MockPermissions, createOrganization: false, }, - organizations: [ - { - ...MockOrganization, - permissions: { - editOrganization: false, - editMembers: false, - editGroups: false, - auditOrganization: true, - }, - }, - ], + organizations: [MockOrganization], }, }; export const SelectedOrgUserAdmin: Story = { args: { - activeOrganization: { - ...MockOrganization, - permissions: { - editOrganization: false, - editMembers: true, - editGroups: true, - auditOrganization: false, - }, + activeOrganization: MockOrganization, + orgPermissions: { + ...MockNoOrganizationPermissions, + viewMembers: true, + viewGroups: true, + viewOrgRoles: true, + viewProvisioners: true, + viewIdpSyncSettings: true, }, permissions: { ...MockPermissions, createOrganization: false, }, + organizations: [MockOrganization], + }, +}; + +export const OrgsDisabled: Story = { + parameters: { + showOrganizations: false, + }, +}; + +const activeOrganization: Organization = { + ...MockOrganization, + display_name: "Omega org", + name: "omega", + id: "1", +}; + +export const OrgsSortedAlphabetically: Story = { + args: { + activeOrganization, + orgPermissions: MockOrganizationPermissions, + permissions: { + ...MockPermissions, + createOrganization: true, + }, organizations: [ { ...MockOrganization, - permissions: { - editOrganization: false, - editMembers: true, - editGroups: true, - auditOrganization: false, - }, + display_name: "Zeta Org", + id: "2", + name: "zeta", }, + { + ...MockOrganization, + display_name: "alpha Org", + id: "3", + name: "alpha", + }, + activeOrganization, ], }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click(canvas.getByRole("button", { name: /Omega org/i })); + + // dropdown is not in #storybook-root so must query full document + const globalScreen = within(document.body); + + await waitFor(() => { + expect(globalScreen.queryByText("alpha Org")).toBeInTheDocument(); + expect(globalScreen.queryByText("Zeta Org")).toBeInTheDocument(); + }); + + const orgElements = globalScreen.getAllByRole("option"); + // filter out Create btn + const filteredElems = orgElements.slice(0, 3); + + const orgNames = filteredElems.map( + // handling fuzzy matching + (el) => el.textContent?.replace(/^[A-Z]/, "").trim() || "", + ); + + // active name first + expect(orgNames).toEqual(["Omega org", "alpha Org", "Zeta Org"]); + }, }; -export const OrgsDisabled: Story = { - parameters: { - showOrganizations: false, +export const SearchForOrg: Story = { + args: { + activeOrganization, + permissions: MockPermissions, + organizations: [ + { + ...MockOrganization, + display_name: "Zeta Org", + id: "2", + name: "zeta", + }, + { + ...MockOrganization, + display_name: "alpha Org", + id: "3", + name: "fish", + }, + activeOrganization, + ], + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click(canvas.getByRole("button", { name: /Omega org/i })); + + // dropdown is not in #storybook-root so must query full document + const globalScreen = within(document.body); + const searchInput = + await globalScreen.findByPlaceholderText("Find organization"); + + await userEvent.type(searchInput, "ALPHA"); + + const filteredResult = await globalScreen.findByText("alpha Org"); + expect(filteredResult).toBeInTheDocument(); + + // Omega org remains visible as the default org + await waitFor(() => { + expect(globalScreen.queryByText("Zeta Org")).not.toBeInTheDocument(); + }); }, }; diff --git a/site/src/modules/management/OrganizationSidebarView.tsx b/site/src/modules/management/OrganizationSidebarView.tsx index 17a9c097b9c62..71a37659ab14d 100644 --- a/site/src/modules/management/OrganizationSidebarView.tsx +++ b/site/src/modules/management/OrganizationSidebarView.tsx @@ -1,73 +1,34 @@ -import type { AuthorizationResponse, Organization } from "api/typesGenerated"; +import type { Organization } from "api/typesGenerated"; import { Avatar } from "components/Avatar/Avatar"; import { Button } from "components/Button/Button"; import { Command, + CommandEmpty, CommandGroup, + CommandInput, CommandItem, CommandList, + CommandSeparator, } from "components/Command/Command"; -import { Loader } from "components/Loader/Loader"; import { Popover, PopoverContent, PopoverTrigger, } from "components/Popover/Popover"; -import { - Sidebar as BaseSidebar, - SettingsSidebarNavItem, -} from "components/Sidebar/Sidebar"; +import { SettingsSidebarNavItem } from "components/Sidebar/Sidebar"; import type { Permissions } from "contexts/auth/permissions"; -import { ChevronDown, Plus } from "lucide-react"; -import { useDashboard } from "modules/dashboard/useDashboard"; +import { Check, ChevronDown, Plus } from "lucide-react"; import { type FC, useState } from "react"; import { useNavigate } from "react-router-dom"; - -export interface OrganizationWithPermissions extends Organization { - permissions: AuthorizationResponse; -} - -interface SidebarProps { - /** The active org name, if any. Overrides activeSettings. */ - activeOrganization: OrganizationWithPermissions | undefined; - /** Organizations and their permissions or undefined if still fetching. */ - organizations: OrganizationWithPermissions[] | undefined; - /** Site-wide permissions. */ - permissions: Permissions; -} - -/** - * Organization settings left sidebar menu. - */ -export const OrganizationSidebarView: FC = ({ - activeOrganization, - organizations, - permissions, -}) => { - const { showOrganizations } = useDashboard(); - - return ( - - {showOrganizations && ( - - )} - - ); -}; - -function urlForSubpage(organizationName: string, subpage = ""): string { - return `/organizations/${organizationName}/${subpage}`; -} +import type { OrganizationPermissions } from "./organizationPermissions"; interface OrganizationsSettingsNavigationProps { - /** The active org name if an org is being viewed. */ - activeOrganization: OrganizationWithPermissions | undefined; + /** The organization selected from the dropdown */ + activeOrganization: Organization | undefined; + /** Permissions for the active organization */ + orgPermissions: OrganizationPermissions | undefined; /** Organizations and their permissions or undefined if still fetching. */ - organizations: OrganizationWithPermissions[] | undefined; + organizations: readonly Organization[]; /** Site-wide permissions. */ permissions: Permissions; } @@ -78,19 +39,18 @@ interface OrganizationsSettingsNavigationProps { * * If organizations or their permissions are still loading, show a loader. */ -const OrganizationsSettingsNavigation: FC< +export const OrganizationSidebarView: FC< OrganizationsSettingsNavigationProps -> = ({ activeOrganization, organizations, permissions }) => { - // Wait for organizations and their permissions to load - if (!organizations || !activeOrganization) { - return ; - } +> = ({ activeOrganization, orgPermissions, organizations, permissions }) => { + const sortedOrganizations = [...organizations].sort((a, b) => { + // active org first + if (a.id === activeOrganization?.id) return -1; + if (b.id === activeOrganization?.id) return 1; - // Sort organizations to put active organization first - const sortedOrganizations = [ - activeOrganization, - ...organizations.filter((org) => org.id !== activeOrganization.id), - ]; + return a.display_name + .toLowerCase() + .localeCompare(b.display_name.toLowerCase()); + }); const [isPopoverOpen, setIsPopoverOpen] = useState(false); const navigate = useNavigate(); @@ -105,55 +65,62 @@ const OrganizationsSettingsNavigation: FC< className="w-60 justify-between p-2 h-11" >
- {activeOrganization && ( - + {activeOrganization ? ( + <> + + + {activeOrganization.display_name || activeOrganization.name} + + + ) : ( + No organization selected )} - - {activeOrganization?.display_name || activeOrganization?.name} -
+ + No organization found. - {sortedOrganizations.length > 1 && ( -
- {sortedOrganizations.map((organization) => ( - { - setIsPopoverOpen(false); - navigate(urlForSubpage(organization.name)); - }} - // There is currently an issue with the cmdk component for keyboard navigation - // https://github.com/pacocoursey/cmdk/issues/322 - tabIndex={0} - > - - - {organization?.display_name || organization?.name} - - - ))} -
- )} - {permissions.createOrganization && ( - <> - {organizations.length > 1 && ( -
- )} +
+ {sortedOrganizations.map((organization) => ( + { + setIsPopoverOpen(false); + navigate(urlForSubpage(organization.name)); + }} + // There is currently an issue with the cmdk component for keyboard navigation + // https://github.com/pacocoursey/cmdk/issues/322 + tabIndex={0} + > + + + {organization?.display_name || organization?.name} + + {activeOrganization?.name === organization.name && ( + + )} + + ))} +
+
+ {permissions.createOrganization && ( + <> + {organizations.length > 1 && } + { @@ -165,65 +132,74 @@ const OrganizationsSettingsNavigation: FC< > Create Organization - - )} - + + + )}
- + {activeOrganization && orgPermissions && ( + + )} ); }; +function urlForSubpage(organizationName: string, subpage = ""): string { + return [`/organizations/${organizationName}`, subpage] + .filter(Boolean) + .join("/"); +} + interface OrganizationSettingsNavigationProps { - organization: OrganizationWithPermissions; + organization: Organization; + orgPermissions: OrganizationPermissions; } const OrganizationSettingsNavigation: FC< OrganizationSettingsNavigationProps -> = ({ organization }) => { +> = ({ organization, orgPermissions }) => { return ( <>
- {organization.permissions.editMembers && ( - - Members - - )} - {organization.permissions.editGroups && ( + + Members + + {orgPermissions.viewGroups && ( Groups )} - {organization.permissions.assignOrgRole && ( + {orgPermissions.viewOrgRoles && ( Roles )} - {organization.permissions.viewProvisioners && ( - - Provisioners - - )} - {organization.permissions.viewIdpSyncSettings && ( + {orgPermissions.viewProvisioners && + orgPermissions.viewProvisionerJobs && ( + + Provisioners + + )} + {orgPermissions.viewIdpSyncSettings && ( IdP Sync )} - {organization.permissions.editOrganization && ( + {orgPermissions.editSettings && ( diff --git a/site/src/modules/management/organizationPermissions.tsx b/site/src/modules/management/organizationPermissions.tsx new file mode 100644 index 0000000000000..2059d8fd6f76f --- /dev/null +++ b/site/src/modules/management/organizationPermissions.tsx @@ -0,0 +1,199 @@ +import type { AuthorizationCheck } from "api/typesGenerated"; + +export type OrganizationPermissions = { + [k in OrganizationPermissionName]: boolean; +}; + +export type OrganizationPermissionName = keyof ReturnType< + typeof organizationPermissionChecks +>; + +export const organizationPermissionChecks = (organizationId: string) => + ({ + viewMembers: { + object: { + resource_type: "organization_member", + organization_id: organizationId, + }, + action: "read", + }, + editMembers: { + object: { + resource_type: "organization_member", + organization_id: organizationId, + }, + action: "update", + }, + createGroup: { + object: { + resource_type: "group", + organization_id: organizationId, + }, + action: "create", + }, + viewGroups: { + object: { + resource_type: "group", + organization_id: organizationId, + }, + action: "read", + }, + editGroups: { + object: { + resource_type: "group", + organization_id: organizationId, + }, + action: "update", + }, + editSettings: { + object: { + resource_type: "organization", + organization_id: organizationId, + }, + action: "update", + }, + assignOrgRoles: { + object: { + resource_type: "assign_org_role", + organization_id: organizationId, + }, + action: "assign", + }, + viewOrgRoles: { + object: { + resource_type: "assign_org_role", + organization_id: organizationId, + }, + action: "read", + }, + createOrgRoles: { + object: { + resource_type: "assign_org_role", + organization_id: organizationId, + }, + action: "create", + }, + viewProvisioners: { + object: { + resource_type: "provisioner_daemon", + organization_id: organizationId, + }, + action: "read", + }, + viewProvisionerJobs: { + object: { + resource_type: "provisioner_jobs", + organization_id: organizationId, + }, + action: "read", + }, + viewIdpSyncSettings: { + object: { + resource_type: "idpsync_settings", + organization_id: organizationId, + }, + action: "read", + }, + editIdpSyncSettings: { + object: { + resource_type: "idpsync_settings", + organization_id: organizationId, + }, + action: "update", + }, + }) as const satisfies Record; + +/** + * Checks if the user can view or edit members or groups for the organization + * that produced the given OrganizationPermissions. + */ +export const canViewOrganization = ( + permissions: OrganizationPermissions | undefined, +): permissions is OrganizationPermissions => { + return ( + permissions !== undefined && + (permissions.viewMembers || + permissions.viewGroups || + permissions.viewProvisioners || + permissions.viewIdpSyncSettings) + ); +}; + +/** + * Return true if the user can edit the organization settings or its members. + */ +export const canEditOrganization = ( + permissions: OrganizationPermissions | undefined, +): permissions is OrganizationPermissions => { + return ( + permissions !== undefined && + (permissions.editMembers || + permissions.editGroups || + permissions.editSettings || + permissions.assignOrgRoles || + permissions.editIdpSyncSettings || + permissions.createOrgRoles) + ); +}; + +export type AnyOrganizationPermissions = { + [k in AnyOrganizationPermissionName]: boolean; +}; + +export type AnyOrganizationPermissionName = + keyof typeof anyOrganizationPermissionChecks; + +export const anyOrganizationPermissionChecks = { + viewAnyMembers: { + object: { + resource_type: "organization_member", + any_org: true, + }, + action: "read", + }, + editAnyGroups: { + object: { + resource_type: "group", + any_org: true, + }, + action: "update", + }, + assignAnyRoles: { + object: { + resource_type: "assign_org_role", + any_org: true, + }, + action: "assign", + }, + viewAnyIdpSyncSettings: { + object: { + resource_type: "idpsync_settings", + any_org: true, + }, + action: "read", + }, + editAnySettings: { + object: { + resource_type: "organization", + any_org: true, + }, + action: "update", + }, +} as const satisfies Record; + +/** + * Checks if the user can view or edit members or groups for the organization + * that produced the given OrganizationPermissions. + */ +export const canViewAnyOrganization = ( + permissions: AnyOrganizationPermissions | undefined, +): permissions is AnyOrganizationPermissions => { + return ( + permissions !== undefined && + (permissions.viewAnyMembers || + permissions.editAnyGroups || + permissions.assignAnyRoles || + permissions.viewAnyIdpSyncSettings || + permissions.editAnySettings) + ); +}; diff --git a/site/src/modules/provisioners/ProvisionerAlert.tsx b/site/src/modules/provisioners/ProvisionerAlert.tsx index 86d69796cd4b9..95c4417ba68ce 100644 --- a/site/src/modules/provisioners/ProvisionerAlert.tsx +++ b/site/src/modules/provisioners/ProvisionerAlert.tsx @@ -52,13 +52,13 @@ export const ProvisionerAlert: FC = ({ {title}
{detail}
- +
{Object.entries(tags ?? {}) .filter(([key]) => key !== "owner") .map(([key, value]) => ( ))} - +
); diff --git a/site/src/modules/provisioners/ProvisionerGroup.tsx b/site/src/modules/provisioners/ProvisionerGroup.tsx index 7bc652a5316f7..017c8f9a2b22c 100644 --- a/site/src/modules/provisioners/ProvisionerGroup.tsx +++ b/site/src/modules/provisioners/ProvisionerGroup.tsx @@ -16,7 +16,7 @@ import { } from "components/HelpTooltip/HelpTooltip"; import { Pill } from "components/Pill/Pill"; import { Stack } from "components/Stack/Stack"; -import { StatusIndicator } from "components/StatusIndicator/StatusIndicator"; +import { StatusIndicatorDot } from "components/StatusIndicator/StatusIndicator"; import { Popover, PopoverContent, @@ -127,7 +127,7 @@ export const ProvisionerGroup: FC = ({ }} >
- +
= ({ <> {kv} { @@ -53,6 +52,7 @@ export const ProvisionerTag: FC = ({ }} > + Delete {tagName} ) : ( @@ -62,7 +62,7 @@ export const ProvisionerTag: FC = ({ return {content}; } return ( - }> + } data-testid={`tag-${tagName}`}> {content} ); diff --git a/site/src/modules/provisioners/ProvisionerTagsField.stories.tsx b/site/src/modules/provisioners/ProvisionerTagsField.stories.tsx new file mode 100644 index 0000000000000..168fb72c2140e --- /dev/null +++ b/site/src/modules/provisioners/ProvisionerTagsField.stories.tsx @@ -0,0 +1,108 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { expect, userEvent, within } from "@storybook/test"; +import type { ProvisionerDaemon } from "api/typesGenerated"; +import { type FC, useState } from "react"; +import { ProvisionerTagsField } from "./ProvisionerTagsField"; + +const meta: Meta = { + title: "modules/provisioners/ProvisionerTagsField", + component: ProvisionerTagsField, + args: { + value: {}, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Empty: Story = { + args: { + value: {}, + }, +}; + +export const WithInitialValue: Story = { + args: { + value: { + cluster: "dogfood-2", + env: "gke", + scope: "organization", + }, + }, +}; + +type StatefulProvisionerTagsFieldProps = { + initialValue?: ProvisionerDaemon["tags"]; +}; + +const StatefulProvisionerTagsField: FC = ({ + initialValue = {}, +}) => { + const [value, setValue] = useState(initialValue); + return ; +}; + +export const OnOverwriteOwner: Story = { + play: async ({ canvasElement }) => { + const user = userEvent.setup(); + const canvas = within(canvasElement); + const keyInput = canvas.getByLabelText("Tag key"); + const valueInput = canvas.getByLabelText("Tag value"); + const addButton = canvas.getByRole("button", { name: "Add tag" }); + + await user.type(keyInput, "owner"); + await user.type(valueInput, "dogfood-2"); + await user.click(addButton); + + await canvas.findByText("Cannot override owner tag"); + }, +}; + +export const OnInvalidScope: Story = { + play: async ({ canvasElement }) => { + const user = userEvent.setup(); + const canvas = within(canvasElement); + const keyInput = canvas.getByLabelText("Tag key"); + const valueInput = canvas.getByLabelText("Tag value"); + const addButton = canvas.getByRole("button", { name: "Add tag" }); + + await user.type(keyInput, "scope"); + await user.type(valueInput, "invalid"); + await user.click(addButton); + + await canvas.findByText("Scope value must be 'organization' or 'user'"); + }, +}; + +export const OnAddTag: Story = { + render: () => , + play: async ({ canvasElement }) => { + const user = userEvent.setup(); + const canvas = within(canvasElement); + const keyInput = canvas.getByLabelText("Tag key"); + const valueInput = canvas.getByLabelText("Tag value"); + const addButton = canvas.getByRole("button", { name: "Add tag" }); + + await user.type(keyInput, "cluster"); + await user.type(valueInput, "dogfood-2"); + await user.click(addButton); + + const addedTag = await canvas.findByTestId("tag-cluster"); + await expect(addedTag).toHaveTextContent("cluster dogfood-2"); + }, +}; + +export const OnRemoveTag: Story = { + render: () => ( + + ), + play: async ({ canvasElement }) => { + const user = userEvent.setup(); + const canvas = within(canvasElement); + const removeButton = canvas.getByRole("button", { name: "Delete cluster" }); + + await user.click(removeButton); + + await expect(canvas.queryByTestId("tag-cluster")).toBeNull(); + }, +}; diff --git a/site/src/modules/provisioners/ProvisionerTagsField.tsx b/site/src/modules/provisioners/ProvisionerTagsField.tsx new file mode 100644 index 0000000000000..26ef7f2ebefe9 --- /dev/null +++ b/site/src/modules/provisioners/ProvisionerTagsField.tsx @@ -0,0 +1,164 @@ +import TextField from "@mui/material/TextField"; +import type { ProvisionerDaemon } from "api/typesGenerated"; +import { Button } from "components/Button/Button"; +import { Input } from "components/Input/Input"; +import { PlusIcon } from "lucide-react"; +import { ProvisionerTag } from "modules/provisioners/ProvisionerTag"; +import { type FC, useRef, useState } from "react"; +import * as Yup from "yup"; + +// Users can't delete these tags +const REQUIRED_TAGS = ["scope", "organization", "user"]; + +// Users can't override these tags +const IMMUTABLE_TAGS = ["owner"]; + +type ProvisionerTagsFieldProps = { + value: ProvisionerDaemon["tags"]; + onChange: (value: ProvisionerDaemon["tags"]) => void; +}; + +export const ProvisionerTagsField: FC = ({ + value: fieldValue, + onChange, +}) => { + return ( +
+
+ {Object.entries(fieldValue) + // Filter out since users cannot override it + .filter(([key]) => !IMMUTABLE_TAGS.includes(key)) + .map(([key, value]) => { + const onDelete = (key: string) => { + const { [key]: _, ...newFieldValue } = fieldValue; + onChange(newFieldValue); + }; + + return ( + + ); + })} +
+ + { + onChange({ ...fieldValue, [tag.key]: tag.value }); + }} + /> +
+ ); +}; + +const newTagSchema = Yup.object({ + key: Yup.string() + .required("Key is required") + .notOneOf(["owner"], "Cannot override owner tag"), + value: Yup.string() + .required("Value is required") + .when("key", ([key], schema) => { + if (key === "scope") { + return schema.oneOf( + ["organization", "scope"], + "Scope value must be 'organization' or 'user'", + ); + } + + return schema; + }), +}); + +type Tag = { key: string; value: string }; + +type NewTagControlProps = { + onAdd: (tag: Tag) => void; +}; + +const NewTagControl: FC = ({ onAdd }) => { + const keyInputRef = useRef(null); + const [error, setError] = useState(); + const [newTag, setNewTag] = useState({ + key: "", + value: "", + }); + + const addNewTag = async () => { + try { + await newTagSchema.validate(newTag); + onAdd(newTag); + setNewTag({ key: "", value: "" }); + keyInputRef.current?.focus(); + } catch (e) { + const isValidationError = e instanceof Yup.ValidationError; + + if (!isValidationError) { + throw e; + } + + if (e instanceof Yup.ValidationError) { + setError(e.errors[0]); + } + } + }; + + const addNewTagOnEnter = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + e.stopPropagation(); + addNewTag(); + } + }; + + return ( +
+
+ + setNewTag({ ...newTag, key: e.target.value.trim() })} + onKeyDown={addNewTagOnEnter} + /> + + + + setNewTag({ ...newTag, value: e.target.value.trim() }) + } + onKeyDown={addNewTagOnEnter} + /> + + +
+ {error && ( + {error} + )} +
+ ); +}; diff --git a/site/src/modules/resources/AppLink/AppLink.tsx b/site/src/modules/resources/AppLink/AppLink.tsx index 15ccfb3d0ed71..e9d5f7d59561b 100644 --- a/site/src/modules/resources/AppLink/AppLink.tsx +++ b/site/src/modules/resources/AppLink/AppLink.tsx @@ -37,6 +37,7 @@ export const AppLink: FC = ({ app, workspace, agent }) => { const preferredPathBase = proxy.preferredPathAppURL; const appsHost = proxy.preferredWildcardHostname; const [fetchingSessionToken, setFetchingSessionToken] = useState(false); + const [iconError, setIconError] = useState(false); const theme = useTheme(); const username = workspace.owner_name; @@ -67,7 +68,9 @@ export const AppLink: FC = ({ app, workspace, agent }) => { // To avoid bugs in the healthcheck code locking users out of apps, we no // longer block access to apps if they are unhealthy/initializing. let canClick = true; - let icon = ; + let icon = !iconError && ( + setIconError(true)} /> + ); let primaryTooltip = ""; if (app.health === "initializing") { diff --git a/site/src/modules/resources/AppLink/BaseIcon.tsx b/site/src/modules/resources/AppLink/BaseIcon.tsx index d6cbf145d4071..1f2885a49a02f 100644 --- a/site/src/modules/resources/AppLink/BaseIcon.tsx +++ b/site/src/modules/resources/AppLink/BaseIcon.tsx @@ -4,14 +4,21 @@ import type { FC } from "react"; interface BaseIconProps { app: WorkspaceApp; + onIconPathError?: () => void; } -export const BaseIcon: FC = ({ app }) => { +export const BaseIcon: FC = ({ app, onIconPathError }) => { return app.icon ? ( {`${app.display_name} { + console.warn( + `Application icon for "${app.id}" has invalid source "${app.icon}".`, + ); + onIconPathError?.(); + }} /> ) : ( diff --git a/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx b/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx index 909fb7cf5646e..1967435425f8f 100644 --- a/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx +++ b/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx @@ -10,6 +10,7 @@ import { DropdownArrow } from "components/DropdownArrow/DropdownArrow"; import { Pill } from "components/Pill/Pill"; import { Stack } from "components/Stack/Stack"; import { TimelineEntry } from "components/Timeline/TimelineEntry"; +import { NetworkIcon } from "lucide-react"; import { type FC, useState } from "react"; import { Link as RouterLink } from "react-router-dom"; import type { ThemeRole } from "theme/roles"; @@ -101,10 +102,20 @@ export const AuditLogRow: FC = ({ css={styles.auditLogHeaderInfo} > - + {/* + * Session logs don't have an associated user to the log, + * so when it happens we display a default icon to represent non user actions + */} + {auditLog.user ? ( + + ) : ( + + + + )} { }), }); + if (auditsQuery.error) { + return ( +
+ +
+ ); + } + return ( <> diff --git a/site/src/pages/CreateTemplateGalleryPage/CreateTemplateGalleryPage.test.tsx b/site/src/pages/CreateTemplateGalleryPage/CreateTemplateGalleryPage.test.tsx index 49c007724aecf..61cf4d353e053 100644 --- a/site/src/pages/CreateTemplateGalleryPage/CreateTemplateGalleryPage.test.tsx +++ b/site/src/pages/CreateTemplateGalleryPage/CreateTemplateGalleryPage.test.tsx @@ -10,7 +10,7 @@ import { import { server } from "testHelpers/server"; import CreateTemplateGalleryPage from "./CreateTemplateGalleryPage"; -test("does not display the scratch template", async () => { +test("displays the scratch template", async () => { server.use( http.get("api/v2/templates/examples", () => { return HttpResponse.json([ @@ -49,5 +49,5 @@ test("does not display the scratch template", async () => { await screen.findByText(MockTemplateExample.name); screen.getByText(MockTemplateExample2.name); - expect(screen.queryByText("Scratch")).not.toBeInTheDocument(); + expect(screen.queryByText("Scratch")).toBeInTheDocument(); }); diff --git a/site/src/pages/CreateTemplateGalleryPage/CreateTemplateGalleryPage.tsx b/site/src/pages/CreateTemplateGalleryPage/CreateTemplateGalleryPage.tsx index 695dd3bfdfc75..e3f1de37a3a3e 100644 --- a/site/src/pages/CreateTemplateGalleryPage/CreateTemplateGalleryPage.tsx +++ b/site/src/pages/CreateTemplateGalleryPage/CreateTemplateGalleryPage.tsx @@ -1,5 +1,4 @@ import { templateExamples } from "api/queries/templates"; -import type { TemplateExample } from "api/typesGenerated"; import type { FC } from "react"; import { Helmet } from "react-helmet-async"; import { useQuery } from "react-query"; @@ -10,8 +9,7 @@ import { CreateTemplateGalleryPageView } from "./CreateTemplateGalleryPageView"; const CreateTemplatesGalleryPage: FC = () => { const templateExamplesQuery = useQuery(templateExamples()); const starterTemplatesByTag = templateExamplesQuery.data - ? // Currently, the scratch template should not be displayed on the starter templates page. - getTemplatesByTag(removeScratchExample(templateExamplesQuery.data)) + ? getTemplatesByTag(templateExamplesQuery.data) : undefined; return ( @@ -27,8 +25,4 @@ const CreateTemplatesGalleryPage: FC = () => { ); }; -const removeScratchExample = (data: TemplateExample[]) => { - return data.filter((example) => example.id !== "scratch"); -}; - export default CreateTemplatesGalleryPage; diff --git a/site/src/pages/CreateTemplateGalleryPage/CreateTemplateGalleryPageView.tsx b/site/src/pages/CreateTemplateGalleryPage/CreateTemplateGalleryPageView.tsx index d34054e9be764..bfa482ac55b94 100644 --- a/site/src/pages/CreateTemplateGalleryPage/CreateTemplateGalleryPageView.tsx +++ b/site/src/pages/CreateTemplateGalleryPage/CreateTemplateGalleryPageView.tsx @@ -41,34 +41,6 @@ export const CreateTemplateGalleryPageView: FC< height: "max-content", }} > - - - - -
- -
-
-

Scratch Template

- - Create a minimal starter template that you can customize - -
-
-
-
-
{ : undefined; }; +const sortVisibleTemplates = (templates: TemplateExample[]) => { + // The docker template should be the first template in the list, + // as it's the easiest way to get started with Coder. + const dockerTemplateId = "docker"; + return templates.sort((a, b) => { + if (a.id === dockerTemplateId) { + return -1; + } + if (b.id === dockerTemplateId) { + return 1; + } + return a.name.localeCompare(b.name); + }); +}; + export interface StarterTemplatesProps { starterTemplatesByTag?: StarterTemplatesByTag; } @@ -34,7 +50,7 @@ export const StarterTemplates: FC = ({ : undefined; const activeTag = urlParams.get("tag") ?? "all"; const visibleTemplates = starterTemplatesByTag - ? starterTemplatesByTag[activeTag] + ? sortVisibleTemplates(starterTemplatesByTag[activeTag]) : undefined; return ( diff --git a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx index 617b7052a2b73..f5417872b27cd 100644 --- a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx +++ b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx @@ -2,6 +2,7 @@ import Link from "@mui/material/Link"; import TextField from "@mui/material/TextField"; import { provisionerDaemons } from "api/queries/organizations"; import type { + CreateTemplateVersionRequest, Organization, ProvisionerJobLog, ProvisionerType, @@ -24,6 +25,7 @@ import { Spinner } from "components/Spinner/Spinner"; import { useFormik } from "formik"; import camelCase from "lodash/camelCase"; import capitalize from "lodash/capitalize"; +import { ProvisionerTagsField } from "modules/provisioners/ProvisionerTagsField"; import { SelectedTemplate } from "pages/CreateWorkspacePage/SelectedTemplate"; import { type FC, useState } from "react"; import { useQuery } from "react-query"; @@ -63,6 +65,7 @@ export interface CreateTemplateFormData { allow_everyone_group_access: boolean; provisioner_type: ProvisionerType; organization: string; + tags: CreateTemplateVersionRequest["tags"]; } const validationSchema = Yup.object({ @@ -96,6 +99,7 @@ const defaultInitialValues: CreateTemplateFormData = { allow_everyone_group_access: true, provisioner_type: "terraform", organization: "default", + tags: {}, }; type GetInitialValuesParams = { @@ -217,12 +221,11 @@ export const CreateTemplateForm: FC = (props) => { }); const getFieldHelpers = getFormHelpers(form, error); - const provisionerDaemonsQuery = useQuery( + const { data: provisioners } = useQuery( selectedOrg ? { ...provisionerDaemons(selectedOrg.id), enabled: showOrganizationPicker, - select: (provisioners) => provisioners.length < 1, } : { enabled: false }, ); @@ -233,7 +236,7 @@ export const CreateTemplateForm: FC = (props) => { // form submission**!! A user could easily see this warning, connect a // provisioner, and then not refresh the page. Even if they submit without // a provisioner, it'll just sit in the job queue until they connect one. - const showProvisionerWarning = provisionerDaemonsQuery.data; + const showProvisionerWarning = provisioners ? provisioners.length < 1 : false; return ( @@ -326,6 +329,32 @@ export const CreateTemplateForm: FC = (props) => { + {provisioners && provisioners.length > 0 && ( + + Tags are a way to control which provisioner daemons complete which + build jobs.  + + Learn more... + + + } + > + + form.setFieldValue("tags", tags)} + /> + + + )} + {/* Variables */} {variables && variables.length > 0 && ( = ({ templateVersionQuery.data!.job.file_id, formData.user_variable_values, formData.provisioner_type, + formData.tags, ), template: newTemplate(formData), }); diff --git a/site/src/pages/CreateTemplatePage/ImportStarterTemplateView.tsx b/site/src/pages/CreateTemplatePage/ImportStarterTemplateView.tsx index e1dcdbcf98cbe..dc611076e4d1b 100644 --- a/site/src/pages/CreateTemplatePage/ImportStarterTemplateView.tsx +++ b/site/src/pages/CreateTemplatePage/ImportStarterTemplateView.tsx @@ -7,7 +7,6 @@ import { import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Loader } from "components/Loader/Loader"; import { useDashboard } from "modules/dashboard/useDashboard"; -import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; import type { FC } from "react"; import { useQuery } from "react-query"; import { useNavigate, useSearchParams } from "react-router-dom"; @@ -79,6 +78,7 @@ export const ImportStarterTemplateView: FC = ({ version: firstVersionFromExample( templateExample!, formData.user_variable_values, + formData.tags, ), template: newTemplate(formData), }); diff --git a/site/src/pages/CreateTemplatePage/UploadTemplateView.tsx b/site/src/pages/CreateTemplatePage/UploadTemplateView.tsx index 8294bfc44ed16..fea9c0d934249 100644 --- a/site/src/pages/CreateTemplatePage/UploadTemplateView.tsx +++ b/site/src/pages/CreateTemplatePage/UploadTemplateView.tsx @@ -7,7 +7,6 @@ import { } from "api/queries/templates"; import { displayError } from "components/GlobalSnackbar/utils"; import { useDashboard } from "modules/dashboard/useDashboard"; -import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; import type { FC } from "react"; import { useMutation, useQuery } from "react-query"; import { useNavigate } from "react-router-dom"; @@ -73,6 +72,7 @@ export const UploadTemplateView: FC = ({ uploadedFile!.hash, formData.user_variable_values, formData.provisioner_type, + formData.tags, ), template: newTemplate(formData), }); diff --git a/site/src/pages/CreateTemplatePage/utils.ts b/site/src/pages/CreateTemplatePage/utils.ts index 48e45fbdaaf52..a10c52a70c16a 100644 --- a/site/src/pages/CreateTemplatePage/utils.ts +++ b/site/src/pages/CreateTemplatePage/utils.ts @@ -58,19 +58,21 @@ export const firstVersionFromFile = ( fileId: string, variables: VariableValue[] | undefined, provisionerType: ProvisionerType, + tags: CreateTemplateVersionRequest["tags"], ): CreateTemplateVersionRequest => { return { storage_method: "file" as const, provisioner: provisionerType, user_variable_values: variables, file_id: fileId, - tags: {}, + tags, }; }; export const firstVersionFromExample = ( example: TemplateExample, variables: VariableValue[] | undefined, + tags: CreateTemplateVersionRequest["tags"], ): CreateTemplateVersionRequest => { return { storage_method: "file" as const, @@ -78,6 +80,6 @@ export const firstVersionFromExample = ( provisioner: "terraform", user_variable_values: variables, example_id: example.id, - tags: {}, + tags, }; }; diff --git a/site/src/pages/CreateTokenPage/CreateTokenForm.tsx b/site/src/pages/CreateTokenPage/CreateTokenForm.tsx index de7c17232a3b7..ee5c3bf8f3a6e 100644 --- a/site/src/pages/CreateTokenPage/CreateTokenForm.tsx +++ b/site/src/pages/CreateTokenPage/CreateTokenForm.tsx @@ -80,6 +80,7 @@ export const CreateTokenForm: FC = ({ = { }, ], }, - decorators: [ - (Story) => { - Date.now = () => new Date("01/01/2014").getTime(); - return ; - }, - ], }; export default meta; diff --git a/site/src/pages/CreateUserPage/CreateUserForm.tsx b/site/src/pages/CreateUserPage/CreateUserForm.tsx index aebdd36e45adc..be8b4a15797b5 100644 --- a/site/src/pages/CreateUserPage/CreateUserForm.tsx +++ b/site/src/pages/CreateUserPage/CreateUserForm.tsx @@ -19,18 +19,7 @@ import { onChangeTrimmed, } from "utils/formUtils"; import * as Yup from "yup"; - -export const Language = { - emailLabel: "Email", - passwordLabel: "Password", - usernameLabel: "Username", - nameLabel: "Full name", - emailInvalid: "Please enter a valid email address.", - emailRequired: "Please enter an email address.", - passwordRequired: "Please enter a password.", - createUser: "Create", - cancel: "Cancel", -}; +import { Language } from "./Language"; export const authMethodLanguage = { password: { diff --git a/site/src/pages/CreateUserPage/CreateUserPage.test.tsx b/site/src/pages/CreateUserPage/CreateUserPage.test.tsx index f8b256e2d0cbb..ec75fc9a8e244 100644 --- a/site/src/pages/CreateUserPage/CreateUserPage.test.tsx +++ b/site/src/pages/CreateUserPage/CreateUserPage.test.tsx @@ -4,8 +4,8 @@ import { renderWithAuth, waitForLoaderToBeRemoved, } from "testHelpers/renderHelpers"; -import { Language as FormLanguage } from "./CreateUserForm"; import { CreateUserPage } from "./CreateUserPage"; +import { Language as FormLanguage } from "./Language"; const renderCreateUserPage = async () => { renderWithAuth(, { diff --git a/site/src/pages/CreateUserPage/Language.ts b/site/src/pages/CreateUserPage/Language.ts new file mode 100644 index 0000000000000..d449829aea89d --- /dev/null +++ b/site/src/pages/CreateUserPage/Language.ts @@ -0,0 +1,11 @@ +export const Language = { + emailLabel: "Email", + passwordLabel: "Password", + usernameLabel: "Username", + nameLabel: "Full name", + emailInvalid: "Please enter a valid email address.", + emailRequired: "Please enter an email address.", + passwordRequired: "Please enter a password.", + createUser: "Create", + cancel: "Cancel", +}; diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx index 56bd0da8a0516..b2481b4729915 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx @@ -5,6 +5,7 @@ import { richParameters, templateByName, templateVersionExternalAuth, + templateVersionPresets, } from "api/queries/templates"; import { autoCreateWorkspace, createWorkspace } from "api/queries/workspaces"; import type { @@ -56,6 +57,10 @@ const CreateWorkspacePage: FC = () => { const templateQuery = useQuery( templateByName(organizationName, templateName), ); + const templateVersionPresetsQuery = useQuery({ + ...templateVersionPresets(templateQuery.data?.active_version_id ?? ""), + enabled: templateQuery.data !== undefined, + }); const permissionsQuery = useQuery( templateQuery.data ? checkAuthorization({ @@ -203,6 +208,7 @@ const CreateWorkspacePage: FC = () => { hasAllRequiredExternalAuth={hasAllRequiredExternalAuth} permissions={permissionsQuery.data as CreateWSPermissions} parameters={realizedParameters as TemplateVersionParameter[]} + presets={templateVersionPresetsQuery.data ?? []} creatingWorkspace={createWorkspaceMutation.isLoading} onCancel={() => { navigate(-1); diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx index 46f1f87e8a50f..6f0647c9f28e8 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx @@ -1,5 +1,7 @@ import { action } from "@storybook/addon-actions"; import type { Meta, StoryObj } from "@storybook/react"; +import { within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { chromatic } from "testHelpers/chromatic"; import { MockTemplate, @@ -116,6 +118,47 @@ export const Parameters: Story = { }, }; +export const PresetsButNoneSelected: Story = { + args: { + presets: [ + { + ID: "preset-1", + Name: "Preset 1", + Parameters: [ + { + Name: MockTemplateVersionParameter1.name, + Value: "preset 1 override", + }, + ], + }, + { + ID: "preset-2", + Name: "Preset 2", + Parameters: [ + { + Name: MockTemplateVersionParameter2.name, + Value: "42", + }, + ], + }, + ], + parameters: [ + MockTemplateVersionParameter1, + MockTemplateVersionParameter2, + MockTemplateVersionParameter3, + ], + }, +}; + +export const PresetSelected: Story = { + args: PresetsButNoneSelected.args, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click(canvas.getByLabelText("Preset")); + await userEvent.click(canvas.getByText("Preset 1")); + }, +}; + export const ExternalAuth: Story = { args: { externalAuth: [ diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index cc912e1f6facf..de72a79e456ef 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -6,6 +6,7 @@ import { Alert } from "components/Alert/Alert"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Avatar } from "components/Avatar/Avatar"; import { Button } from "components/Button/Button"; +import { SelectFilter } from "components/Filter/SelectFilter"; import { FormFields, FormFooter, @@ -64,6 +65,7 @@ export interface CreateWorkspacePageViewProps { hasAllRequiredExternalAuth: boolean; parameters: TypesGen.TemplateVersionParameter[]; autofillParameters: AutofillBuildParameter[]; + presets: TypesGen.Preset[]; permissions: CreateWSPermissions; creatingWorkspace: boolean; onCancel: () => void; @@ -88,6 +90,7 @@ export const CreateWorkspacePageView: FC = ({ hasAllRequiredExternalAuth, parameters, autofillParameters, + presets = [], permissions, creatingWorkspace, onSubmit, @@ -145,6 +148,62 @@ export const CreateWorkspacePageView: FC = ({ [autofillParameters], ); + const [presetOptions, setPresetOptions] = useState([ + { label: "None", value: "" }, + ]); + useEffect(() => { + setPresetOptions([ + { label: "None", value: "" }, + ...presets.map((preset) => ({ + label: preset.Name, + value: preset.ID, + })), + ]); + }, [presets]); + + const [selectedPresetIndex, setSelectedPresetIndex] = useState(0); + const [presetParameterNames, setPresetParameterNames] = useState( + [], + ); + + useEffect(() => { + const selectedPresetOption = presetOptions[selectedPresetIndex]; + let selectedPreset: TypesGen.Preset | undefined; + for (const preset of presets) { + if (preset.ID === selectedPresetOption.value) { + selectedPreset = preset; + break; + } + } + + if (!selectedPreset || !selectedPreset.Parameters) { + setPresetParameterNames([]); + return; + } + + setPresetParameterNames(selectedPreset.Parameters.map((p) => p.Name)); + + for (const presetParameter of selectedPreset.Parameters) { + const parameterIndex = parameters.findIndex( + (p) => p.name === presetParameter.Name, + ); + if (parameterIndex === -1) continue; + + const parameterField = `rich_parameter_values.${parameterIndex}`; + + form.setFieldValue(parameterField, { + name: presetParameter.Name, + value: presetParameter.Value, + }); + } + }, [ + presetOptions, + selectedPresetIndex, + presets, + parameters, + form.setFieldValue, + ]); + return ( = ({
)} + {presets.length > 0 && ( + + + Select a preset to get started + + + { + setSelectedPresetIndex( + presetOptions.findIndex( + (preset) => preset.value === option?.value, + ), + ); + }} + placeholder="Select a preset" + selectedOption={presetOptions[selectedPresetIndex]} + /> + + + )}
= ({ const isDisabled = disabledParams?.includes( parameter.name.toLowerCase().replace(/ /g, "_"), - ) || creatingWorkspace; + ) || + creatingWorkspace || + presetParameterNames.includes(parameter.name); return ( = { component: UserEngagementChart, args: { data: [ - { date: "1/1/2024", users: 150 }, - { date: "1/2/2024", users: 165 }, - { date: "1/3/2024", users: 180 }, - { date: "1/4/2024", users: 155 }, - { date: "1/5/2024", users: 190 }, - { date: "1/6/2024", users: 200 }, + { date: "1/1/2024", users: 140 }, + { date: "1/2/2024", users: 175 }, + { date: "1/3/2024", users: 120 }, + { date: "1/4/2024", users: 195 }, + { date: "1/5/2024", users: 230 }, + { date: "1/6/2024", users: 130 }, { date: "1/7/2024", users: 210 }, ], }, diff --git a/site/src/pages/DeploymentSettingsPage/GeneralSettingsPage/UserEngagementChart.tsx b/site/src/pages/DeploymentSettingsPage/GeneralSettingsPage/UserEngagementChart.tsx index 431141a148eb0..585088f02db1d 100644 --- a/site/src/pages/DeploymentSettingsPage/GeneralSettingsPage/UserEngagementChart.tsx +++ b/site/src/pages/DeploymentSettingsPage/GeneralSettingsPage/UserEngagementChart.tsx @@ -157,7 +157,7 @@ export const UserEngagementChart: FC = ({ data }) => { { // IdP sync does not have its own entitlement and is based on templace_rbac const { template_rbac: isIdpSyncEnabled } = useFeatureVisibility(); const { organizations } = useDashboard(); - const { - data: orgSyncSettingsData, - isLoading, - error, - } = useQuery(organizationIdpSyncSettings(isIdpSyncEnabled)); + const settingsQuery = useQuery(organizationIdpSyncSettings(isIdpSyncEnabled)); + + const [field, setField] = useState(""); + useEffect(() => { + if (!settingsQuery.data) { + return; + } + + setField(settingsQuery.data.field); + }, [settingsQuery.data]); + + const fieldValuesQuery = useQuery( + field ? deploymentIdpSyncFieldValues(field) : { enabled: false }, + ); const patchOrganizationSyncSettingsMutation = useMutation( patchOrganizationSyncSettings(queryClient), @@ -45,7 +55,7 @@ export const IdpOrgSyncPage: FC = () => { } }, [patchOrganizationSyncSettingsMutation.error]); - if (isLoading) { + if (settingsQuery.isLoading) { return ; } @@ -67,7 +77,7 @@ export const IdpOrgSyncPage: FC = () => {

- + @@ -79,8 +89,10 @@ export const IdpOrgSyncPage: FC = () => { { try { await patchOrganizationSyncSettingsMutation.mutateAsync(data); @@ -94,7 +106,7 @@ export const IdpOrgSyncPage: FC = () => { ); } }} - error={error || patchOrganizationSyncSettingsMutation.error} + error={settingsQuery.error || fieldValuesQuery.error} /> diff --git a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.stories.tsx b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.stories.tsx index 8d02e1f248833..430fce3a2ee05 100644 --- a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.stories.tsx +++ b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.stories.tsx @@ -1,16 +1,23 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { userEvent, within } from "@storybook/test"; +import { expect, userEvent, within } from "@storybook/test"; import { MockOrganization, MockOrganization2, MockOrganizationSyncSettings, MockOrganizationSyncSettings2, + MockOrganizationSyncSettingsEmpty, } from "testHelpers/entities"; import { IdpOrgSyncPageView } from "./IdpOrgSyncPageView"; const meta: Meta = { title: "pages/IdpOrgSyncPageView", component: IdpOrgSyncPageView, + args: { + organizationSyncSettings: MockOrganizationSyncSettings2, + claimFieldValues: Object.keys(MockOrganizationSyncSettings2.mapping), + organizations: [MockOrganization, MockOrganization2], + error: undefined, + }, }; export default meta; @@ -18,35 +25,35 @@ type Story = StoryObj; export const Empty: Story = { args: { - organizationSyncSettings: { - field: "", - mapping: {}, - organization_assign_default: true, - }, - organizations: [MockOrganization, MockOrganization2], - error: undefined, + organizationSyncSettings: MockOrganizationSyncSettingsEmpty, }, }; -export const Default: Story = { - args: { - organizationSyncSettings: MockOrganizationSyncSettings2, - organizations: [MockOrganization, MockOrganization2], - error: undefined, - }, -}; +export const Default: Story = {}; export const HasError: Story = { args: { - ...Default.args, error: "This is a test error", }, }; export const MissingGroups: Story = { args: { - ...Default.args, organizationSyncSettings: MockOrganizationSyncSettings, + claimFieldValues: Object.keys(MockOrganizationSyncSettings.mapping), + organizations: [], + }, +}; + +export const MissingClaims: Story = { + args: { + claimFieldValues: [], + }, + play: async ({ canvasElement }) => { + const user = userEvent.setup(); + const warning = canvasElement.querySelector(".lucide-triangle-alert")!; + expect(warning).not.toBe(null); + await user.hover(warning); }, }; diff --git a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx index 7ed1b85e8c9dd..5871cf98f21a5 100644 --- a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx +++ b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx @@ -1,15 +1,10 @@ -import Table from "@mui/material/Table"; -import TableBody from "@mui/material/TableBody"; -import TableCell from "@mui/material/TableCell"; -import TableContainer from "@mui/material/TableContainer"; -import TableHead from "@mui/material/TableHead"; -import TableRow from "@mui/material/TableRow"; import type { Organization, OrganizationSyncSettings, } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Button } from "components/Button/Button"; +import { Combobox } from "components/Combobox/Combobox"; import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; import { Dialog, @@ -35,9 +30,22 @@ import { } from "components/MultiSelectCombobox/MultiSelectCombobox"; import { Spinner } from "components/Spinner/Spinner"; import { Switch } from "components/Switch/Switch"; +import { + Table, + TableBody, + TableCell, + TableHeader, + TableRow, +} from "components/Table/Table"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "components/Tooltip/Tooltip"; import { useFormik } from "formik"; -import { Plus, Trash } from "lucide-react"; -import { type FC, useId, useState } from "react"; +import { Plus, Trash, TriangleAlert } from "lucide-react"; +import { type FC, type KeyboardEventHandler, useId, useState } from "react"; import { docs } from "utils/docs"; import { isUUID } from "utils/uuid"; import * as Yup from "yup"; @@ -45,8 +53,10 @@ import { OrganizationPills } from "./OrganizationPills"; interface IdpSyncPageViewProps { organizationSyncSettings: OrganizationSyncSettings | undefined; + claimFieldValues: readonly string[] | undefined; organizations: readonly Organization[]; onSubmit: (data: OrganizationSyncSettings) => void; + onSyncFieldChange: (value: string) => void; error?: unknown; } @@ -74,8 +84,10 @@ const validationSchema = Yup.object({ export const IdpOrgSyncPageView: FC = ({ organizationSyncSettings, + claimFieldValues, organizations, onSubmit, + onSyncFieldChange, error, }) => { const form = useFormik({ @@ -91,11 +103,13 @@ export const IdpOrgSyncPageView: FC = ({ }); const [coderOrgs, setCoderOrgs] = useState([]); const [idpOrgName, setIdpOrgName] = useState(""); + const [inputValue, setInputValue] = useState(""); const organizationMappingCount = form.values.mapping ? Object.entries(form.values.mapping).length : 0; const [isDialogOpen, setIsDialogOpen] = useState(false); const id = useId(); + const [open, setOpen] = useState(false); const getOrgNames = (orgIds: readonly string[]) => { return orgIds.map( @@ -118,6 +132,19 @@ export const IdpOrgSyncPageView: FC = ({ form.handleSubmit(); }; + const handleKeyDown: KeyboardEventHandler = (event) => { + if ( + event.key === "Enter" && + inputValue && + !claimFieldValues?.some((value) => value === inputValue.toLowerCase()) + ) { + event.preventDefault(); + setIdpOrgName(inputValue); + setInputValue(""); + setOpen(false); + } + }; + return (
{Boolean(error) && } @@ -135,6 +162,7 @@ export const IdpOrgSyncPageView: FC = ({ value={form.values.field} onChange={(event) => { void form.setFieldValue("field", event.target.value); + onSyncFieldChange(event.target.value); }} />
); }; interface OrganizationRowProps { idpOrg: string; + exists: boolean | undefined; coderOrgs: readonly string[]; onDelete: (idpOrg: string) => void; } const OrganizationRow: FC = ({ idpOrg, + exists = true, coderOrgs, onDelete, }) => { return ( - {idpOrg} + +
+ {idpOrg} + {!exists && ( + + + + + + + This value has not be seen in the specified claim field + before. You might want to check your IdP configuration and + ensure that this value is not misspelled. + + + + )} +
+
diff --git a/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationsPage.tsx b/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationsPage.tsx index 23f8e6b42651e..a68013b0bfef3 100644 --- a/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationsPage.tsx +++ b/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationsPage.tsx @@ -4,7 +4,9 @@ import { selectTemplatesByGroup, systemNotificationTemplates, } from "api/queries/notifications"; +import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge"; import { Loader } from "components/Loader/Loader"; +import { SettingsHeader } from "components/SettingsHeader/SettingsHeader"; import { TabLink, Tabs, TabsList } from "components/Tabs/Tabs"; import { useSearchParamsKey } from "hooks/useSearchParamsKey"; import { useDeploymentSettings } from "modules/management/DeploymentSettingsProvider"; @@ -14,9 +16,11 @@ import type { FC } from "react"; import { Helmet } from "react-helmet-async"; import { useQueries } from "react-query"; import { deploymentGroupHasParent } from "utils/deployOptions"; +import { docs } from "utils/docs"; import { pageTitle } from "utils/page"; import OptionsTable from "../OptionsTable"; import { NotificationEvents } from "./NotificationEvents"; +import { Troubleshooting } from "./Troubleshooting"; export const NotificationsPage: FC = () => { const { deploymentConfig } = useDeploymentSettings(); @@ -40,48 +44,62 @@ export const NotificationsPage: FC = () => { {pageTitle("Notifications Settings")} -
+ Notifications + + + + + } description="Control delivery methods for notifications on this deployment." - layout="fluid" - featureStage={"beta"} - > - - - - Events - - - Settings - - - + docsHref={docs("/admin/monitoring/notifications")} + /> + + + + Events + + + Settings + + + Troubleshooting + + + -
- {ready ? ( - tabState.value === "events" ? ( - - ) : ( - - deploymentGroupHasParent(o.group, "Notifications"), - )} - /> - ) +
+ {ready ? ( + tabState.value === "events" ? ( + + ) : tabState.value === "troubleshooting" ? ( + ) : ( - - )} -
-
+ + deploymentGroupHasParent(o.group, "Notifications"), + )} + /> + ) + ) : ( + + )} +
); }; diff --git a/site/src/pages/DeploymentSettingsPage/NotificationsPage/Troubleshooting.stories.tsx b/site/src/pages/DeploymentSettingsPage/NotificationsPage/Troubleshooting.stories.tsx new file mode 100644 index 0000000000000..052e855b284a9 --- /dev/null +++ b/site/src/pages/DeploymentSettingsPage/NotificationsPage/Troubleshooting.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { spyOn, userEvent, within } from "@storybook/test"; +import { API } from "api/api"; +import { Troubleshooting } from "./Troubleshooting"; +import { baseMeta } from "./storybookUtils"; + +const meta: Meta = { + title: "pages/DeploymentSettingsPage/NotificationsPage/Troubleshooting", + component: Troubleshooting, + ...baseMeta, +}; + +export default meta; + +type Story = StoryObj; + +export const TestNotification: Story = { + play: async ({ canvasElement }) => { + spyOn(API, "postTestNotification").mockResolvedValue(); + const user = userEvent.setup(); + const canvas = within(canvasElement); + + const sendButton = canvas.getByRole("button", { + name: "Send notification", + }); + await user.click(sendButton); + await within(document.body).findByText("Test notification sent"); + }, +}; diff --git a/site/src/pages/DeploymentSettingsPage/NotificationsPage/Troubleshooting.tsx b/site/src/pages/DeploymentSettingsPage/NotificationsPage/Troubleshooting.tsx new file mode 100644 index 0000000000000..c9a4362427cf7 --- /dev/null +++ b/site/src/pages/DeploymentSettingsPage/NotificationsPage/Troubleshooting.tsx @@ -0,0 +1,47 @@ +import { useTheme } from "@emotion/react"; +import LoadingButton from "@mui/lab/LoadingButton"; +import { API } from "api/api"; +import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"; +import type { FC } from "react"; +import { useMutation } from "react-query"; + +export const Troubleshooting: FC = () => { + const { mutate: sendTestNotificationApi, isLoading } = useMutation( + API.postTestNotification, + { + onSuccess: () => displaySuccess("Test notification sent"), + onError: () => displayError("Failed to send test notification"), + }, + ); + + const theme = useTheme(); + return ( + <> +
+ Send a test notification to troubleshoot your notification settings. +
+
+ + { + sendTestNotificationApi(); + }} + > + Send notification + + +
+ + ); +}; diff --git a/site/src/pages/DeploymentSettingsPage/NotificationsPage/storybookUtils.ts b/site/src/pages/DeploymentSettingsPage/NotificationsPage/storybookUtils.ts index 4906a5ab54496..fc500efd847d6 100644 --- a/site/src/pages/DeploymentSettingsPage/NotificationsPage/storybookUtils.ts +++ b/site/src/pages/DeploymentSettingsPage/NotificationsPage/storybookUtils.ts @@ -13,7 +13,7 @@ import { withAuthProvider, withDashboardProvider, withGlobalSnackbar, - withManagementSettingsProvider, + withOrganizationSettingsProvider, } from "testHelpers/storybook"; import type { NotificationsPage } from "./NotificationsPage"; @@ -213,6 +213,6 @@ export const baseMeta = { withGlobalSnackbar, withAuthProvider, withDashboardProvider, - withManagementSettingsProvider, + withOrganizationSettingsProvider, ], } satisfies Meta; diff --git a/site/src/pages/ExternalAuthPage/ExternalAuthPageView.tsx b/site/src/pages/ExternalAuthPage/ExternalAuthPageView.tsx index 5ff3b5a626b93..fd379bf0121fa 100644 --- a/site/src/pages/ExternalAuthPage/ExternalAuthPageView.tsx +++ b/site/src/pages/ExternalAuthPage/ExternalAuthPageView.tsx @@ -1,15 +1,13 @@ import type { Interpolation, Theme } from "@emotion/react"; import OpenInNewIcon from "@mui/icons-material/OpenInNew"; import RefreshIcon from "@mui/icons-material/Refresh"; -import AlertTitle from "@mui/material/AlertTitle"; -import CircularProgress from "@mui/material/CircularProgress"; import Link from "@mui/material/Link"; import Tooltip from "@mui/material/Tooltip"; import type { ApiErrorResponse } from "api/errors"; import type { ExternalAuth, ExternalAuthDevice } from "api/typesGenerated"; -import { Alert, AlertDetail } from "components/Alert/Alert"; +import { Alert } from "components/Alert/Alert"; import { Avatar } from "components/Avatar/Avatar"; -import { CopyButton } from "components/CopyButton/CopyButton"; +import { GitDeviceAuth } from "components/GitDeviceAuth/GitDeviceAuth"; import { SignInLayout } from "components/SignInLayout/SignInLayout"; import { Welcome } from "components/Welcome/Welcome"; import type { FC, ReactNode } from "react"; @@ -141,89 +139,6 @@ const ExternalAuthPageView: FC = ({ ); }; -interface GitDeviceAuthProps { - externalAuthDevice?: ExternalAuthDevice; - deviceExchangeError?: ApiErrorResponse; -} - -const GitDeviceAuth: FC = ({ - externalAuthDevice, - deviceExchangeError, -}) => { - let status = ( -

- - Checking for authentication... -

- ); - if (deviceExchangeError) { - // See https://datatracker.ietf.org/doc/html/rfc8628#section-3.5 - switch (deviceExchangeError.detail) { - case "authorization_pending": - break; - case "expired_token": - status = ( - - The one-time code has expired. Refresh to get a new one! - - ); - break; - case "access_denied": - status = ( - Access to the Git provider was denied. - ); - break; - default: - status = ( - - {deviceExchangeError.message} - {deviceExchangeError.detail && ( - {deviceExchangeError.detail} - )} - - ); - break; - } - } - - // If the error comes from the `externalAuthDevice` query, - // we cannot even display the user_code. - if (deviceExchangeError && !externalAuthDevice) { - return
{status}
; - } - - if (!externalAuthDevice) { - return ; - } - - return ( -
-

- Copy your one-time code:  -

- {externalAuthDevice.user_code} -   -
-
- Then open the link below and paste it: -

-
- - - Open and Paste - -
- - {status} -
- ); -}; - export default ExternalAuthPageView; const styles = { @@ -235,16 +150,6 @@ const styles = { margin: 0, }), - copyCode: { - display: "inline-flex", - alignItems: "center", - }, - - code: (theme) => ({ - fontWeight: "bold", - color: theme.palette.text.primary, - }), - installAlert: { margin: 16, }, @@ -264,14 +169,6 @@ const styles = { gap: 8, }, - status: (theme) => ({ - display: "flex", - alignItems: "center", - justifyContent: "center", - gap: 8, - color: theme.palette.text.disabled, - }), - authorizedInstalls: (theme) => ({ display: "flex", gap: 4, diff --git a/site/src/pages/GroupsPage/CreateGroupPage.tsx b/site/src/pages/GroupsPage/CreateGroupPage.tsx index 92f480d8ab959..257a404a3b7ea 100644 --- a/site/src/pages/GroupsPage/CreateGroupPage.tsx +++ b/site/src/pages/GroupsPage/CreateGroupPage.tsx @@ -2,14 +2,17 @@ import { createGroup } from "api/queries/groups"; import type { FC } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQueryClient } from "react-query"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; import { pageTitle } from "utils/page"; import CreateGroupPageView from "./CreateGroupPageView"; export const CreateGroupPage: FC = () => { const queryClient = useQueryClient(); const navigate = useNavigate(); - const createGroupMutation = useMutation(createGroup(queryClient, "default")); + const { organization } = useParams() as { organization: string }; + const createGroupMutation = useMutation( + createGroup(queryClient, organization ?? "default"), + ); return ( <> @@ -19,7 +22,11 @@ export const CreateGroupPage: FC = () => { { const newGroup = await createGroupMutation.mutateAsync(data); - navigate(`/deployment/groups/${newGroup.name}`); + navigate( + organization + ? `/organizations/${organization}/groups/${newGroup.name}` + : `/deployment/groups/${newGroup.name}`, + ); }} error={createGroupMutation.error} isLoading={createGroupMutation.isLoading} diff --git a/site/src/pages/GroupsPage/CreateGroupPageView.stories.tsx b/site/src/pages/GroupsPage/CreateGroupPageView.stories.tsx index 735c4160c9f67..ea8dfcc3f3e02 100644 --- a/site/src/pages/GroupsPage/CreateGroupPageView.stories.tsx +++ b/site/src/pages/GroupsPage/CreateGroupPageView.stories.tsx @@ -4,7 +4,7 @@ import { mockApiError } from "testHelpers/entities"; import { CreateGroupPageView } from "./CreateGroupPageView"; const meta: Meta = { - title: "pages/GroupsPage/CreateGroupPageView", + title: "pages/OrganizationGroupsPage/CreateGroupPageView", component: CreateGroupPageView, }; @@ -19,7 +19,15 @@ export const WithError: Story = { message: "A group named new-group already exists.", validations: [{ field: "name", detail: "Group names must be unique" }], }), - initialTouched: { name: true }, + }, + play: async ({ canvasElement, step }) => { + const canvas = within(canvasElement); + + await step("Enter name", async () => { + const input = canvas.getByLabelText("Name"); + await userEvent.type(input, "new-group"); + input.blur(); + }); }, }; diff --git a/site/src/pages/GroupsPage/CreateGroupPageView.tsx b/site/src/pages/GroupsPage/CreateGroupPageView.tsx index dd400459d0c2c..5557abd39dc1f 100644 --- a/site/src/pages/GroupsPage/CreateGroupPageView.tsx +++ b/site/src/pages/GroupsPage/CreateGroupPageView.tsx @@ -3,13 +3,16 @@ import { isApiValidationError } from "api/errors"; import type { CreateGroupRequest } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Button } from "components/Button/Button"; -import { FormFooter } from "components/Form/Form"; -import { FullPageForm } from "components/FullPageForm/FullPageForm"; +import { + FormFields, + FormFooter, + FormSection, + HorizontalForm, +} from "components/Form/Form"; import { IconField } from "components/IconField/IconField"; -import { Margins } from "components/Margins/Margins"; +import { SettingsHeader } from "components/SettingsHeader/SettingsHeader"; import { Spinner } from "components/Spinner/Spinner"; -import { Stack } from "components/Stack/Stack"; -import { type FormikTouched, useFormik } from "formik"; +import { useFormik } from "formik"; import type { FC } from "react"; import { useNavigate } from "react-router-dom"; import { @@ -27,15 +30,12 @@ export type CreateGroupPageViewProps = { onSubmit: (data: CreateGroupRequest) => void; error?: unknown; isLoading: boolean; - // Helpful to show field errors on Storybook - initialTouched?: FormikTouched; }; export const CreateGroupPageView: FC = ({ onSubmit, error, isLoading, - initialTouched, }) => { const navigate = useNavigate(); const form = useFormik({ @@ -47,16 +47,23 @@ export const CreateGroupPageView: FC = ({ }, validationSchema, onSubmit, - initialTouched, }); const getFieldHelpers = getFormHelpers(form, error); - const onCancel = () => navigate("/deployment/groups"); + const onCancel = () => navigate(-1); return ( - - -
- + <> + + + + + {Boolean(error) && !isApiValidationError(error) && ( )} @@ -84,21 +91,21 @@ export const CreateGroupPageView: FC = ({ label="Avatar URL" onPickEmoji={(value) => form.setFieldValue("avatar_url", value)} /> - + + - - + + - - - -
-
+ + + + ); }; export default CreateGroupPageView; diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.stories.tsx b/site/src/pages/GroupsPage/GroupPage.stories.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.stories.tsx rename to site/src/pages/GroupsPage/GroupPage.stories.tsx diff --git a/site/src/pages/GroupsPage/GroupPage.tsx b/site/src/pages/GroupsPage/GroupPage.tsx index 913101518c61e..6c226a1dba9ff 100644 --- a/site/src/pages/GroupsPage/GroupPage.tsx +++ b/site/src/pages/GroupsPage/GroupPage.tsx @@ -18,7 +18,11 @@ import { groupPermissions, removeMember, } from "api/queries/groups"; -import type { Group, ReducedUser, User } from "api/typesGenerated"; +import type { + Group, + OrganizationMemberWithUserData, + ReducedUser, +} from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Avatar } from "components/Avatar/Avatar"; import { AvatarData } from "components/Avatar/AvatarData"; @@ -27,7 +31,6 @@ import { EmptyState } from "components/EmptyState/EmptyState"; import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"; import { LastSeen } from "components/LastSeen/LastSeen"; import { Loader } from "components/Loader/Loader"; -import { Margins } from "components/Margins/Margins"; import { MoreMenu, MoreMenuContent, @@ -35,17 +38,13 @@ import { MoreMenuTrigger, ThreeDotsButton, } from "components/MoreMenu/MoreMenu"; -import { - PageHeader, - PageHeaderSubtitle, - PageHeaderTitle, -} from "components/PageHeader/PageHeader"; +import { SettingsHeader } from "components/SettingsHeader/SettingsHeader"; import { Stack } from "components/Stack/Stack"; import { PaginationStatus, TableToolbar, } from "components/TableToolbar/TableToolbar"; -import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete"; +import { MemberAutocomplete } from "components/UserAutocomplete/UserAutocomplete"; import { type FC, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQuery, useQueryClient } from "react-query"; @@ -54,19 +53,19 @@ import { isEveryoneGroup } from "utils/groups"; import { pageTitle } from "utils/page"; export const GroupPage: FC = () => { - const { groupName } = useParams() as { + const { organization = "default", groupName } = useParams() as { + organization?: string; groupName: string; }; const queryClient = useQueryClient(); const navigate = useNavigate(); - const groupQuery = useQuery(group("default", groupName)); + const groupQuery = useQuery(group(organization, groupName)); const groupData = groupQuery.data; const { data: permissions } = useQuery( - groupData !== undefined - ? groupPermissions(groupData.id) - : { enabled: false }, + groupData ? groupPermissions(groupData.id) : { enabled: false }, ); const addMemberMutation = useMutation(addMember(queryClient)); + const removeMemberMutation = useMutation(removeMember(queryClient)); const deleteGroupMutation = useMutation(deleteGroup(queryClient)); const [isDeletingGroup, setIsDeletingGroup] = useState(false); const isLoading = groupQuery.isLoading || !groupData || !permissions; @@ -100,106 +99,115 @@ export const GroupPage: FC = () => { <> {helmet} - - - - - - ) - } - > - - {groupData?.display_name || groupData?.name} - - - {/* Show the name if it differs from the display name. */} - {groupData?.display_name && - groupData?.display_name !== groupData?.name - ? groupData?.name - : ""}{" "} - - - - - {canUpdateGroup && groupData && !isEveryoneGroup(groupData) && ( - { - try { - await addMemberMutation.mutateAsync({ - groupId, - userId: user.id, - }); - reset(); - await groupQuery.refetch(); - } catch (error) { - displayError(getErrorMessage(error, "Failed to add member.")); - } + + + {canUpdateGroup && ( + + + + + )} + - - - - - User - Status - - - + + {canUpdateGroup && groupData && !isEveryoneGroup(groupData) && ( + { + try { + await addMemberMutation.mutateAsync({ + groupId, + userId: member.user_id, + }); + reset(); + await groupQuery.refetch(); + } catch (error) { + displayError(getErrorMessage(error, "Failed to add member.")); + } + }} + /> + )} + + + + + +
+ + + User + Status + + + - - {groupData?.members.length === 0 ? ( - - - - - - ) : ( - groupData?.members.map((member) => ( - + {groupData?.members.length === 0 ? ( + + + - )) - )} - -
-
-
-
+ + + ) : ( + groupData?.members.map((member) => ( + { + try { + await removeMemberMutation.mutateAsync({ + groupId: groupData.id, + userId: member.id, + }); + await groupQuery.refetch(); + displaySuccess("Member removed successfully."); + } catch (error) { + displayError( + getErrorMessage(error, "Failed to remove member."), + ); + } + }} + /> + )) + )} + + + + {groupQuery.data && ( { try { await deleteGroupMutation.mutateAsync(groupId); displaySuccess("Group deleted successfully."); - navigate("/deployment/groups"); + navigate(".."); } catch (error) { displayError(getErrorMessage(error, "Failed to delete group.")); } @@ -227,11 +235,17 @@ export const GroupPage: FC = () => { interface AddGroupMemberProps { isLoading: boolean; - onSubmit: (user: User, reset: () => void) => void; + onSubmit: (user: OrganizationMemberWithUserData, reset: () => void) => void; + organizationId: string; } -const AddGroupMember: FC = ({ isLoading, onSubmit }) => { - const [selectedUser, setSelectedUser] = useState(null); +const AddGroupMember: FC = ({ + isLoading, + onSubmit, + organizationId, +}) => { + const [selectedUser, setSelectedUser] = + useState(null); const resetValues = () => { setSelectedUser(null); @@ -248,9 +262,10 @@ const AddGroupMember: FC = ({ isLoading, onSubmit }) => { }} > - { setSelectedUser(newValue); }} @@ -274,16 +289,15 @@ interface GroupMemberRowProps { member: ReducedUser; group: Group; canUpdate: boolean; + onRemove: () => void; } const GroupMemberRow: FC = ({ member, group, canUpdate, + onRemove, }) => { - const queryClient = useQueryClient(); - const removeMemberMutation = useMutation(removeMember(queryClient)); - return ( @@ -309,19 +323,7 @@ const GroupMemberRow: FC = ({ { - try { - await removeMemberMutation.mutateAsync({ - groupId: group.id, - userId: member.id, - }); - displaySuccess("Member removed successfully."); - } catch (error) { - displayError( - getErrorMessage(error, "Failed to remove member."), - ); - } - }} + onClick={onRemove} disabled={group.id === group.organization_id} > Remove diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPage.tsx b/site/src/pages/GroupsPage/GroupSettingsPage.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPage.tsx rename to site/src/pages/GroupsPage/GroupSettingsPage.tsx diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.stories.tsx b/site/src/pages/GroupsPage/GroupSettingsPageView.stories.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.stories.tsx rename to site/src/pages/GroupsPage/GroupSettingsPageView.stories.tsx diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.tsx b/site/src/pages/GroupsPage/GroupSettingsPageView.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPageView.tsx rename to site/src/pages/GroupsPage/GroupSettingsPageView.tsx diff --git a/site/src/pages/GroupsPage/GroupsPage.tsx b/site/src/pages/GroupsPage/GroupsPage.tsx index 6313b8e450c9e..a99ec44334530 100644 --- a/site/src/pages/GroupsPage/GroupsPage.tsx +++ b/site/src/pages/GroupsPage/GroupsPage.tsx @@ -1,18 +1,34 @@ +import GroupAdd from "@mui/icons-material/GroupAddOutlined"; import { getErrorMessage } from "api/errors"; import { groupsByOrganization } from "api/queries/groups"; +import { organizationsPermissions } from "api/queries/organizations"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { Button } from "components/Button/Button"; +import { EmptyState } from "components/EmptyState/EmptyState"; import { displayError } from "components/GlobalSnackbar/utils"; -import { useAuthenticated } from "contexts/auth/RequireAuth"; +import { Loader } from "components/Loader/Loader"; +import { SettingsHeader } from "components/SettingsHeader/SettingsHeader"; +import { Stack } from "components/Stack/Stack"; import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; import { type FC, useEffect } from "react"; import { Helmet } from "react-helmet-async"; import { useQuery } from "react-query"; +import { Link as RouterLink } from "react-router-dom"; import { pageTitle } from "utils/page"; +import { useGroupsSettings } from "./GroupsPageProvider"; import GroupsPageView from "./GroupsPageView"; export const GroupsPage: FC = () => { - const { permissions } = useAuthenticated(); - const { template_rbac: isTemplateRBACEnabled } = useFeatureVisibility(); - const groupsQuery = useQuery(groupsByOrganization("default")); + const { template_rbac: groupsEnabled } = useFeatureVisibility(); + const { organization, showOrganizations } = useGroupsSettings(); + const groupsQuery = useQuery( + organization ? groupsByOrganization(organization.name) : { enabled: false }, + ); + const permissionsQuery = useQuery( + organization + ? organizationsPermissions([organization.id]) + : { enabled: false }, + ); useEffect(() => { if (groupsQuery.error) { @@ -22,16 +38,56 @@ export const GroupsPage: FC = () => { } }, [groupsQuery.error]); + useEffect(() => { + if (permissionsQuery.error) { + displayError( + getErrorMessage(permissionsQuery.error, "Unable to load permissions."), + ); + } + }, [permissionsQuery.error]); + + if (!organization) { + return ; + } + + if (permissionsQuery.isLoading) { + return ; + } + + const permissions = permissionsQuery.data?.[organization.id]; + if (!permissions) { + return ; + } + return ( <> {pageTitle("Groups")} + + + {groupsEnabled && permissions.createGroup && ( + + )} + + ); diff --git a/site/src/pages/GroupsPage/GroupsPageProvider.tsx b/site/src/pages/GroupsPage/GroupsPageProvider.tsx new file mode 100644 index 0000000000000..3697705aebc4b --- /dev/null +++ b/site/src/pages/GroupsPage/GroupsPageProvider.tsx @@ -0,0 +1,57 @@ +import type { Organization } from "api/typesGenerated"; +import { useDashboard } from "modules/dashboard/useDashboard"; +import { type FC, createContext, useContext } from "react"; +import { Navigate, Outlet, useParams } from "react-router-dom"; + +export const GroupsPageContext = createContext< + OrganizationSettingsValue | undefined +>(undefined); + +type OrganizationSettingsValue = Readonly<{ + organization?: Organization; + showOrganizations: boolean; +}>; + +export const useGroupsSettings = (): OrganizationSettingsValue => { + const context = useContext(GroupsPageContext); + if (!context) { + throw new Error( + "useGroupsSettings should be used inside of GroupsPageContext", + ); + } + + return context; +}; + +const GroupsPageProvider: FC = () => { + const { organizations, showOrganizations } = useDashboard(); + const { organization: orgName } = useParams() as { + organization?: string; + }; + + const organization = orgName + ? organizations.find((org) => org.name === orgName) + : getOrganizationByDefault(organizations); + + if ( + location.pathname.startsWith("/deployment/groups") && + showOrganizations && + organization + ) { + return ( + + ); + } + + return ( + + + + ); +}; + +export default GroupsPageProvider; + +const getOrganizationByDefault = (organizations: readonly Organization[]) => { + return organizations.find((org) => org.is_default); +}; diff --git a/site/src/pages/GroupsPage/GroupsPageView.stories.tsx b/site/src/pages/GroupsPage/GroupsPageView.stories.tsx index a179a830e4652..466ee2b149524 100644 --- a/site/src/pages/GroupsPage/GroupsPageView.stories.tsx +++ b/site/src/pages/GroupsPage/GroupsPageView.stories.tsx @@ -3,7 +3,7 @@ import { MockGroup } from "testHelpers/entities"; import { GroupsPageView } from "./GroupsPageView"; const meta: Meta = { - title: "pages/GroupsPage", + title: "pages/OrganizationGroupsPage", component: GroupsPageView, }; @@ -14,7 +14,7 @@ export const NotEnabled: Story = { args: { groups: [MockGroup], canCreateGroup: true, - isTemplateRBACEnabled: false, + groupsEnabled: false, }, }; @@ -22,7 +22,7 @@ export const WithGroups: Story = { args: { groups: [MockGroup], canCreateGroup: true, - isTemplateRBACEnabled: true, + groupsEnabled: true, }, }; @@ -30,7 +30,7 @@ export const WithDisplayGroup: Story = { args: { groups: [{ ...MockGroup, name: "front-end" }], canCreateGroup: true, - isTemplateRBACEnabled: true, + groupsEnabled: true, }, }; @@ -38,7 +38,7 @@ export const EmptyGroup: Story = { args: { groups: [], canCreateGroup: false, - isTemplateRBACEnabled: true, + groupsEnabled: true, }, }; @@ -46,6 +46,6 @@ export const EmptyGroupWithPermission: Story = { args: { groups: [], canCreateGroup: true, - isTemplateRBACEnabled: true, + groupsEnabled: true, }, }; diff --git a/site/src/pages/GroupsPage/GroupsPageView.tsx b/site/src/pages/GroupsPage/GroupsPageView.tsx index bd2d2ef981419..22ccd35515064 100644 --- a/site/src/pages/GroupsPage/GroupsPageView.tsx +++ b/site/src/pages/GroupsPage/GroupsPageView.tsx @@ -2,7 +2,6 @@ import type { Interpolation, Theme } from "@emotion/react"; import AddOutlined from "@mui/icons-material/AddOutlined"; import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight"; import AvatarGroup from "@mui/material/AvatarGroup"; -import Button from "@mui/material/Button"; import Skeleton from "@mui/material/Skeleton"; import Table from "@mui/material/Table"; import TableBody from "@mui/material/TableBody"; @@ -14,6 +13,7 @@ import type { Group } from "api/typesGenerated"; import { Avatar } from "components/Avatar/Avatar"; import { AvatarData } from "components/Avatar/AvatarData"; import { AvatarDataSkeleton } from "components/Avatar/AvatarDataSkeleton"; +import { Button } from "components/Button/Button"; import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; import { EmptyState } from "components/EmptyState/EmptyState"; import { Paywall } from "components/Paywall/Paywall"; @@ -21,6 +21,7 @@ import { TableLoaderSkeleton, TableRowSkeleton, } from "components/TableLoader/TableLoader"; +import { useClickableTableRow } from "hooks"; import type { FC } from "react"; import { Link as RouterLink, useNavigate } from "react-router-dom"; import { docs } from "utils/docs"; @@ -28,25 +29,24 @@ import { docs } from "utils/docs"; export type GroupsPageViewProps = { groups: Group[] | undefined; canCreateGroup: boolean; - isTemplateRBACEnabled: boolean; + groupsEnabled: boolean; }; export const GroupsPageView: FC = ({ groups, canCreateGroup, - isTemplateRBACEnabled, + groupsEnabled, }) => { const isLoading = Boolean(groups === undefined); const isEmpty = Boolean(groups && groups.length === 0); - const navigate = useNavigate(); return ( <> - + @@ -78,13 +78,11 @@ export const GroupsPageView: FC = ({ } cta={ canCreateGroup && ( - ) } @@ -94,63 +92,9 @@ export const GroupsPageView: FC = ({ - {groups?.map((group) => { - const groupPageLink = `/deployment/groups/${group.name}`; - - return ( - { - navigate(groupPageLink); - }} - onKeyDown={(event) => { - if (event.key === "Enter") { - navigate(groupPageLink); - } - }} - css={styles.clickableTableRow} - > - - - } - title={group.display_name || group.name} - subtitle={`${group.members.length} members`} - /> - - - - {group.members.length === 0 && "-"} - - {group.members.map((member) => ( - - ))} - - - - -
- -
-
-
- ); - })} + {groups?.map((group) => ( + + ))}
@@ -162,7 +106,58 @@ export const GroupsPageView: FC = ({ ); }; -const TableLoader = () => { +interface GroupRowProps { + group: Group; +} + +const GroupRow: FC = ({ group }) => { + const navigate = useNavigate(); + const rowProps = useClickableTableRow({ + onClick: () => navigate(group.name), + }); + + return ( + + + + } + title={group.display_name || group.name} + subtitle={`${group.members.length} members`} + /> + + + + {group.members.length === 0 && "-"} + + {group.members.map((member) => ( + + ))} + + + + +
+ +
+
+
+ ); +}; + +const TableLoader: FC = () => { return ( @@ -183,21 +178,6 @@ const TableLoader = () => { }; const styles = { - clickableTableRow: (theme) => ({ - cursor: "pointer", - - "&:hover td": { - backgroundColor: theme.palette.action.hover, - }, - - "&:focus": { - outline: `1px solid ${theme.palette.primary.main}`, - }, - - "& .MuiTableCell-root:last-child": { - paddingRight: "16px !important", - }, - }), arrowRight: (theme) => ({ color: theme.palette.text.secondary, width: 20, diff --git a/site/src/pages/GroupsPage/SettingsGroupPage.tsx b/site/src/pages/GroupsPage/SettingsGroupPage.tsx deleted file mode 100644 index 5b44d5c99457f..0000000000000 --- a/site/src/pages/GroupsPage/SettingsGroupPage.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { getErrorMessage } from "api/errors"; -import { group, patchGroup } from "api/queries/groups"; -import { ErrorAlert } from "components/Alert/ErrorAlert"; -import { displayError } from "components/GlobalSnackbar/utils"; -import { Loader } from "components/Loader/Loader"; -import type { FC } from "react"; -import { Helmet } from "react-helmet-async"; -import { useMutation, useQuery, useQueryClient } from "react-query"; -import { useNavigate, useParams } from "react-router-dom"; -import { pageTitle } from "utils/page"; -import SettingsGroupPageView from "./SettingsGroupPageView"; - -export const SettingsGroupPage: FC = () => { - const { groupName } = useParams() as { groupName: string }; - const queryClient = useQueryClient(); - const groupQuery = useQuery(group("default", groupName)); - const patchGroupMutation = useMutation(patchGroup(queryClient)); - const navigate = useNavigate(); - - const navigateToGroup = () => { - navigate(`/deployment/groups/${groupName}`); - }; - - const helmet = ( - - {pageTitle("Settings Group")} - - ); - - if (groupQuery.error) { - return ; - } - - if (groupQuery.isLoading || !groupQuery.data) { - return ( - <> - {helmet} - - - ); - } - - const groupId = groupQuery.data.id; - - return ( - <> - {helmet} - - { - try { - await patchGroupMutation.mutateAsync({ - groupId, - ...data, - add_users: [], - remove_users: [], - }); - navigate(`/deployment/groups/${data.name}`, { replace: true }); - } catch (error) { - displayError(getErrorMessage(error, "Failed to update group")); - } - }} - group={groupQuery.data} - formErrors={groupQuery.error} - isLoading={groupQuery.isLoading} - isUpdating={patchGroupMutation.isLoading} - /> - - ); -}; -export default SettingsGroupPage; diff --git a/site/src/pages/GroupsPage/SettingsGroupPageView.stories.tsx b/site/src/pages/GroupsPage/SettingsGroupPageView.stories.tsx deleted file mode 100644 index 78f4ead3ef6d0..0000000000000 --- a/site/src/pages/GroupsPage/SettingsGroupPageView.stories.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { action } from "@storybook/addon-actions"; -import type { Meta, StoryObj } from "@storybook/react"; -import { MockGroup } from "testHelpers/entities"; -import { SettingsGroupPageView } from "./SettingsGroupPageView"; - -const meta: Meta = { - title: "pages/GroupsPage/SettingsGroupPageView", - component: SettingsGroupPageView, - args: { - onCancel: action("onCancel"), - group: MockGroup, - isLoading: false, - }, -}; - -export default meta; -type Story = StoryObj; - -const Example: Story = {}; - -export { Example as SettingsGroupPageView }; diff --git a/site/src/pages/GroupsPage/SettingsGroupPageView.tsx b/site/src/pages/GroupsPage/SettingsGroupPageView.tsx deleted file mode 100644 index 3877cabc0beb6..0000000000000 --- a/site/src/pages/GroupsPage/SettingsGroupPageView.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import TextField from "@mui/material/TextField"; -import type { Group } from "api/typesGenerated"; -import { Button } from "components/Button/Button"; -import { FormFooter } from "components/Form/Form"; -import { FullPageForm } from "components/FullPageForm/FullPageForm"; -import { IconField } from "components/IconField/IconField"; -import { Loader } from "components/Loader/Loader"; -import { Margins } from "components/Margins/Margins"; -import { Spinner } from "components/Spinner/Spinner"; -import { Stack } from "components/Stack/Stack"; -import { useFormik } from "formik"; -import type { FC } from "react"; -import { - getFormHelpers, - nameValidator, - onChangeTrimmed, -} from "utils/formUtils"; -import { isEveryoneGroup } from "utils/groups"; -import * as Yup from "yup"; - -type FormData = { - name: string; - display_name: string; - avatar_url: string; - quota_allowance: number; -}; - -const validationSchema = Yup.object({ - name: nameValidator("Name"), - quota_allowance: Yup.number().required().min(0).integer(), -}); - -interface UpdateGroupFormProps { - group: Group; - errors: unknown; - onSubmit: (data: FormData) => void; - onCancel: () => void; - isLoading: boolean; -} - -const UpdateGroupForm: FC = ({ - group, - errors, - onSubmit, - onCancel, - isLoading, -}) => { - const form = useFormik({ - initialValues: { - name: group.name, - display_name: group.display_name, - avatar_url: group.avatar_url, - quota_allowance: group.quota_allowance, - }, - validationSchema, - onSubmit, - }); - const getFieldHelpers = getFormHelpers(form, errors); - - return ( - -
- - - {isEveryoneGroup(group) ? ( - <> - ) : ( - <> - - form.setFieldValue("avatar_url", value)} - /> - - )} - - - - - - - - -
-
- ); -}; - -export type SettingsGroupPageViewProps = { - onCancel: () => void; - onSubmit: (data: FormData) => void; - group: Group | undefined; - formErrors: unknown; - isLoading: boolean; - isUpdating: boolean; -}; - -export const SettingsGroupPageView: FC = ({ - onCancel, - onSubmit, - group, - formErrors, - isLoading, - isUpdating, -}) => { - if (isLoading) { - return ; - } - - return ( - - - - ); -}; - -export default SettingsGroupPageView; diff --git a/site/src/pages/HealthPage/HealthLayout.tsx b/site/src/pages/HealthPage/HealthLayout.tsx index 2c566500d892b..c520fd764fea1 100644 --- a/site/src/pages/HealthPage/HealthLayout.tsx +++ b/site/src/pages/HealthPage/HealthLayout.tsx @@ -7,6 +7,7 @@ import IconButton from "@mui/material/IconButton"; import Tooltip from "@mui/material/Tooltip"; import { health, refreshHealth } from "api/queries/debug"; import type { HealthSeverity } from "api/typesGenerated"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Loader } from "components/Loader/Loader"; import { type ClassName, useClassName } from "hooks/useClassName"; import kebabCase from "lodash/fp/kebabCase"; @@ -22,7 +23,11 @@ import { HealthIcon } from "./Content"; export const HealthLayout: FC = () => { const theme = useTheme(); const queryClient = useQueryClient(); - const { data: healthStatus } = useQuery({ + const { + data: healthStatus, + isLoading, + error, + } = useQuery({ ...health(), refetchInterval: 30_000, }); @@ -42,161 +47,171 @@ export const HealthLayout: FC = () => { const link = useClassName(classNames.link, []); const activeLink = useClassName(classNames.activeLink, []); + if (isLoading) { + return ( +
+ +
+ ); + } + + if (error || !healthStatus) { + return ( +
+ +
+ ); + } + return ( <> {pageTitle("Health")} - {healthStatus ? ( - + +
-
-
-
- - - - { - forceRefresh(); - }} - > - {isRefreshing ? ( - - ) : ( - - )} - - -
-
- {healthStatus.healthy ? "Healthy" : "Unhealthy"} -
-
- {healthStatus.healthy - ? Object.keys(visibleSections).some((key) => { - const section = - healthStatus[key as keyof typeof visibleSections]; - return ( - section.warnings && section.warnings.length > 0 - ); - }) - ? "All systems operational, but performance might be degraded" - : "All systems operational" - : "Some issues have been detected"} -
-
+
+
+ -
- Last check - - {createDayString(healthStatus.time)} - + + { + forceRefresh(); + }} + > + {isRefreshing ? ( + + ) : ( + + )} + +
- -
- Version - - {healthStatus.coder_version} - +
+ {healthStatus.healthy ? "Healthy" : "Unhealthy"} +
+
+ {healthStatus.healthy + ? Object.keys(visibleSections).some((key) => { + const section = + healthStatus[key as keyof typeof visibleSections]; + return section.warnings && section.warnings.length > 0; + }) + ? "All systems operational, but performance might be degraded" + : "All systems operational" + : "Some issues have been detected"}
- -
+
+ Last check + + {createDayString(healthStatus.time)} + +
-
- }> - - +
+ Version + + {healthStatus.coder_version} + +
+ +
- - ) : ( - - )} + +
+ }> + + +
+
+ ); }; diff --git a/site/src/pages/LoginOAuthDevicePage/LoginOAuthDevicePage.tsx b/site/src/pages/LoginOAuthDevicePage/LoginOAuthDevicePage.tsx new file mode 100644 index 0000000000000..db7b267a2e99a --- /dev/null +++ b/site/src/pages/LoginOAuthDevicePage/LoginOAuthDevicePage.tsx @@ -0,0 +1,87 @@ +import type { ApiErrorResponse } from "api/errors"; +import { + getGitHubDevice, + getGitHubDeviceFlowCallback, +} from "api/queries/oauth2"; +import { isAxiosError } from "axios"; +import { SignInLayout } from "components/SignInLayout/SignInLayout"; +import { Welcome } from "components/Welcome/Welcome"; +import { useEffect } from "react"; +import type { FC } from "react"; +import { useQuery } from "react-query"; +import { useSearchParams } from "react-router-dom"; +import LoginOAuthDevicePageView from "./LoginOAuthDevicePageView"; + +const isErrorRetryable = (error: unknown) => { + if (!isAxiosError(error)) { + return false; + } + return error.response?.data?.detail === "authorization_pending"; +}; + +// The page is hardcoded to only use GitHub, +// as that's the only OAuth2 login provider in our backend +// that currently supports the device flow. +const LoginOAuthDevicePage: FC = () => { + const [searchParams] = useSearchParams(); + + const state = searchParams.get("state"); + if (!state) { + return ( + + Missing OAuth2 state + + ); + } + + const externalAuthDeviceQuery = useQuery({ + ...getGitHubDevice(), + refetchOnMount: false, + }); + const exchangeExternalAuthDeviceQuery = useQuery({ + ...getGitHubDeviceFlowCallback( + externalAuthDeviceQuery.data?.device_code ?? "", + state, + ), + enabled: Boolean(externalAuthDeviceQuery.data), + retry: (_, error) => isErrorRetryable(error), + retryDelay: (externalAuthDeviceQuery.data?.interval || 5) * 1000, + refetchOnWindowFocus: (query) => + query.state.status === "success" || + (query.state.error != null && !isErrorRetryable(query.state.error)) + ? false + : "always", + }); + + useEffect(() => { + if (!exchangeExternalAuthDeviceQuery.isSuccess) { + return; + } + // We use window.location.href in lieu of a navigate hook + // because we need to refresh the page after the GitHub + // callback query sets a session cookie. + window.location.href = exchangeExternalAuthDeviceQuery.data.redirect_url; + }, [ + exchangeExternalAuthDeviceQuery.isSuccess, + exchangeExternalAuthDeviceQuery.data?.redirect_url, + ]); + + let deviceExchangeError: ApiErrorResponse | undefined; + if (isAxiosError(exchangeExternalAuthDeviceQuery.failureReason)) { + deviceExchangeError = + exchangeExternalAuthDeviceQuery.failureReason.response?.data; + } else if (isAxiosError(externalAuthDeviceQuery.failureReason)) { + deviceExchangeError = externalAuthDeviceQuery.failureReason.response?.data; + } + + return ( + + ); +}; + +export default LoginOAuthDevicePage; diff --git a/site/src/pages/LoginOAuthDevicePage/LoginOAuthDevicePageView.tsx b/site/src/pages/LoginOAuthDevicePage/LoginOAuthDevicePageView.tsx new file mode 100644 index 0000000000000..9cdea2ed0aacb --- /dev/null +++ b/site/src/pages/LoginOAuthDevicePage/LoginOAuthDevicePageView.tsx @@ -0,0 +1,57 @@ +import type { Interpolation, Theme } from "@emotion/react"; +import type { ApiErrorResponse } from "api/errors"; +import type { ExternalAuthDevice } from "api/typesGenerated"; +import { GitDeviceAuth } from "components/GitDeviceAuth/GitDeviceAuth"; +import { SignInLayout } from "components/SignInLayout/SignInLayout"; +import { Welcome } from "components/Welcome/Welcome"; +import type { FC } from "react"; + +export interface LoginOAuthDevicePageViewProps { + authenticated: boolean; + redirectUrl: string; + externalAuthDevice?: ExternalAuthDevice; + deviceExchangeError?: ApiErrorResponse; +} + +const LoginOAuthDevicePageView: FC = ({ + authenticated, + redirectUrl, + deviceExchangeError, + externalAuthDevice, +}) => { + if (!authenticated) { + return ( + + Authenticate with GitHub + + + + ); + } + + return ( + + You've authenticated with GitHub! + +

+ If you're not redirected automatically,{" "} + click here. +

+
+ ); +}; + +export default LoginOAuthDevicePageView; + +const styles = { + text: (theme) => ({ + fontSize: 16, + color: theme.palette.text.secondary, + textAlign: "center", + lineHeight: "160%", + margin: 0, + }), +} satisfies Record>; diff --git a/site/src/pages/LoginPage/SignInForm.stories.tsx b/site/src/pages/LoginPage/SignInForm.stories.tsx index 8e02ccfb3cfdc..125e912e08e70 100644 --- a/site/src/pages/LoginPage/SignInForm.stories.tsx +++ b/site/src/pages/LoginPage/SignInForm.stories.tsx @@ -20,7 +20,7 @@ export const SigningIn: Story = { isSigningIn: true, authMethods: { password: { enabled: true }, - github: { enabled: true }, + github: { enabled: true, default_provider_configured: false }, oidc: { enabled: false, signInText: "", iconUrl: "" }, }, }, @@ -44,7 +44,7 @@ export const WithGithub: Story = { args: { authMethods: { password: { enabled: true }, - github: { enabled: true }, + github: { enabled: true, default_provider_configured: false }, oidc: { enabled: false, signInText: "", iconUrl: "" }, }, }, @@ -54,7 +54,7 @@ export const WithOIDC: Story = { args: { authMethods: { password: { enabled: true }, - github: { enabled: false }, + github: { enabled: false, default_provider_configured: false }, oidc: { enabled: true, signInText: "", iconUrl: "" }, }, }, @@ -64,7 +64,7 @@ export const WithOIDCWithoutPassword: Story = { args: { authMethods: { password: { enabled: false }, - github: { enabled: false }, + github: { enabled: false, default_provider_configured: false }, oidc: { enabled: true, signInText: "", iconUrl: "" }, }, }, @@ -74,7 +74,7 @@ export const WithoutAny: Story = { args: { authMethods: { password: { enabled: false }, - github: { enabled: false }, + github: { enabled: false, default_provider_configured: false }, oidc: { enabled: false, signInText: "", iconUrl: "" }, }, }, @@ -84,7 +84,7 @@ export const WithGithubAndOIDC: Story = { args: { authMethods: { password: { enabled: true }, - github: { enabled: true }, + github: { enabled: true, default_provider_configured: false }, oidc: { enabled: true, signInText: "", iconUrl: "" }, }, }, diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPage.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPage.tsx deleted file mode 100644 index 257a404a3b7ea..0000000000000 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPage.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { createGroup } from "api/queries/groups"; -import type { FC } from "react"; -import { Helmet } from "react-helmet-async"; -import { useMutation, useQueryClient } from "react-query"; -import { useNavigate, useParams } from "react-router-dom"; -import { pageTitle } from "utils/page"; -import CreateGroupPageView from "./CreateGroupPageView"; - -export const CreateGroupPage: FC = () => { - const queryClient = useQueryClient(); - const navigate = useNavigate(); - const { organization } = useParams() as { organization: string }; - const createGroupMutation = useMutation( - createGroup(queryClient, organization ?? "default"), - ); - - return ( - <> - - {pageTitle("Create Group")} - - { - const newGroup = await createGroupMutation.mutateAsync(data); - navigate( - organization - ? `/organizations/${organization}/groups/${newGroup.name}` - : `/deployment/groups/${newGroup.name}`, - ); - }} - error={createGroupMutation.error} - isLoading={createGroupMutation.isLoading} - /> - - ); -}; -export default CreateGroupPage; diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.stories.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.stories.tsx deleted file mode 100644 index ea8dfcc3f3e02..0000000000000 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.stories.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { userEvent, within } from "@storybook/test"; -import { mockApiError } from "testHelpers/entities"; -import { CreateGroupPageView } from "./CreateGroupPageView"; - -const meta: Meta = { - title: "pages/OrganizationGroupsPage/CreateGroupPageView", - component: CreateGroupPageView, -}; - -export default meta; -type Story = StoryObj; - -export const Example: Story = {}; - -export const WithError: Story = { - args: { - error: mockApiError({ - message: "A group named new-group already exists.", - validations: [{ field: "name", detail: "Group names must be unique" }], - }), - }, - play: async ({ canvasElement, step }) => { - const canvas = within(canvasElement); - - await step("Enter name", async () => { - const input = canvas.getByLabelText("Name"); - await userEvent.type(input, "new-group"); - input.blur(); - }); - }, -}; - -export const InvalidName: Story = { - play: async ({ canvasElement }) => { - const user = userEvent.setup(); - const body = within(canvasElement.ownerDocument.body); - const input = await body.findByLabelText("Name"); - await user.type(input, "$om3 !nv@lid Name"); - input.blur(); - }, -}; diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.tsx deleted file mode 100644 index 5557abd39dc1f..0000000000000 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPageView.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import TextField from "@mui/material/TextField"; -import { isApiValidationError } from "api/errors"; -import type { CreateGroupRequest } from "api/typesGenerated"; -import { ErrorAlert } from "components/Alert/ErrorAlert"; -import { Button } from "components/Button/Button"; -import { - FormFields, - FormFooter, - FormSection, - HorizontalForm, -} from "components/Form/Form"; -import { IconField } from "components/IconField/IconField"; -import { SettingsHeader } from "components/SettingsHeader/SettingsHeader"; -import { Spinner } from "components/Spinner/Spinner"; -import { useFormik } from "formik"; -import type { FC } from "react"; -import { useNavigate } from "react-router-dom"; -import { - getFormHelpers, - nameValidator, - onChangeTrimmed, -} from "utils/formUtils"; -import * as Yup from "yup"; - -const validationSchema = Yup.object({ - name: nameValidator("Name"), -}); - -export type CreateGroupPageViewProps = { - onSubmit: (data: CreateGroupRequest) => void; - error?: unknown; - isLoading: boolean; -}; - -export const CreateGroupPageView: FC = ({ - onSubmit, - error, - isLoading, -}) => { - const navigate = useNavigate(); - const form = useFormik({ - initialValues: { - name: "", - display_name: "", - avatar_url: "", - quota_allowance: 0, - }, - validationSchema, - onSubmit, - }); - const getFieldHelpers = getFormHelpers(form, error); - const onCancel = () => navigate(-1); - - return ( - <> - - - - - - {Boolean(error) && !isApiValidationError(error) && ( - - )} - - - - form.setFieldValue("avatar_url", value)} - /> - - - - - - - - - - - ); -}; -export default CreateGroupPageView; diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx deleted file mode 100644 index 6c226a1dba9ff..0000000000000 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx +++ /dev/null @@ -1,357 +0,0 @@ -import type { Interpolation, Theme } from "@emotion/react"; -import DeleteOutline from "@mui/icons-material/DeleteOutline"; -import PersonAdd from "@mui/icons-material/PersonAdd"; -import SettingsOutlined from "@mui/icons-material/SettingsOutlined"; -import LoadingButton from "@mui/lab/LoadingButton"; -import Button from "@mui/material/Button"; -import Table from "@mui/material/Table"; -import TableBody from "@mui/material/TableBody"; -import TableCell from "@mui/material/TableCell"; -import TableContainer from "@mui/material/TableContainer"; -import TableHead from "@mui/material/TableHead"; -import TableRow from "@mui/material/TableRow"; -import { getErrorMessage } from "api/errors"; -import { - addMember, - deleteGroup, - group, - groupPermissions, - removeMember, -} from "api/queries/groups"; -import type { - Group, - OrganizationMemberWithUserData, - ReducedUser, -} from "api/typesGenerated"; -import { ErrorAlert } from "components/Alert/ErrorAlert"; -import { Avatar } from "components/Avatar/Avatar"; -import { AvatarData } from "components/Avatar/AvatarData"; -import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog"; -import { EmptyState } from "components/EmptyState/EmptyState"; -import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"; -import { LastSeen } from "components/LastSeen/LastSeen"; -import { Loader } from "components/Loader/Loader"; -import { - MoreMenu, - MoreMenuContent, - MoreMenuItem, - MoreMenuTrigger, - ThreeDotsButton, -} from "components/MoreMenu/MoreMenu"; -import { SettingsHeader } from "components/SettingsHeader/SettingsHeader"; -import { Stack } from "components/Stack/Stack"; -import { - PaginationStatus, - TableToolbar, -} from "components/TableToolbar/TableToolbar"; -import { MemberAutocomplete } from "components/UserAutocomplete/UserAutocomplete"; -import { type FC, useState } from "react"; -import { Helmet } from "react-helmet-async"; -import { useMutation, useQuery, useQueryClient } from "react-query"; -import { Link as RouterLink, useNavigate, useParams } from "react-router-dom"; -import { isEveryoneGroup } from "utils/groups"; -import { pageTitle } from "utils/page"; - -export const GroupPage: FC = () => { - const { organization = "default", groupName } = useParams() as { - organization?: string; - groupName: string; - }; - const queryClient = useQueryClient(); - const navigate = useNavigate(); - const groupQuery = useQuery(group(organization, groupName)); - const groupData = groupQuery.data; - const { data: permissions } = useQuery( - groupData ? groupPermissions(groupData.id) : { enabled: false }, - ); - const addMemberMutation = useMutation(addMember(queryClient)); - const removeMemberMutation = useMutation(removeMember(queryClient)); - const deleteGroupMutation = useMutation(deleteGroup(queryClient)); - const [isDeletingGroup, setIsDeletingGroup] = useState(false); - const isLoading = groupQuery.isLoading || !groupData || !permissions; - const canUpdateGroup = permissions ? permissions.canUpdateGroup : false; - - const helmet = ( - - - {pageTitle( - (groupData?.display_name || groupData?.name) ?? "Loading...", - )} - - - ); - - if (groupQuery.error) { - return ; - } - - if (isLoading) { - return ( - <> - {helmet} - - - ); - } - const groupId = groupData.id; - - return ( - <> - {helmet} - - - - {canUpdateGroup && ( - - - - - )} - - - - {canUpdateGroup && groupData && !isEveryoneGroup(groupData) && ( - { - try { - await addMemberMutation.mutateAsync({ - groupId, - userId: member.user_id, - }); - reset(); - await groupQuery.refetch(); - } catch (error) { - displayError(getErrorMessage(error, "Failed to add member.")); - } - }} - /> - )} - - - - - - - - - User - Status - - - - - - {groupData?.members.length === 0 ? ( - - - - - - ) : ( - groupData?.members.map((member) => ( - { - try { - await removeMemberMutation.mutateAsync({ - groupId: groupData.id, - userId: member.id, - }); - await groupQuery.refetch(); - displaySuccess("Member removed successfully."); - } catch (error) { - displayError( - getErrorMessage(error, "Failed to remove member."), - ); - } - }} - /> - )) - )} - -
-
-
- - {groupQuery.data && ( - { - try { - await deleteGroupMutation.mutateAsync(groupId); - displaySuccess("Group deleted successfully."); - navigate(".."); - } catch (error) { - displayError(getErrorMessage(error, "Failed to delete group.")); - } - }} - onCancel={() => { - setIsDeletingGroup(false); - }} - /> - )} - - ); -}; - -interface AddGroupMemberProps { - isLoading: boolean; - onSubmit: (user: OrganizationMemberWithUserData, reset: () => void) => void; - organizationId: string; -} - -const AddGroupMember: FC = ({ - isLoading, - onSubmit, - organizationId, -}) => { - const [selectedUser, setSelectedUser] = - useState(null); - - const resetValues = () => { - setSelectedUser(null); - }; - - return ( -
{ - e.preventDefault(); - - if (selectedUser) { - onSubmit(selectedUser, resetValues); - } - }} - > - - { - setSelectedUser(newValue); - }} - /> - - } - loading={isLoading} - > - Add user - - -
- ); -}; - -interface GroupMemberRowProps { - member: ReducedUser; - group: Group; - canUpdate: boolean; - onRemove: () => void; -} - -const GroupMemberRow: FC = ({ - member, - group, - canUpdate, - onRemove, -}) => { - return ( - - - } - title={member.username} - subtitle={member.email} - /> - - -
{member.status}
- -
- - {canUpdate && ( - - - - - - - Remove - - - - )} - -
- ); -}; - -const styles = { - autoComplete: { - width: 300, - }, - removeButton: (theme) => ({ - color: theme.palette.error.main, - "&:hover": { - backgroundColor: "transparent", - }, - }), - status: { - textTransform: "capitalize", - }, - suspended: (theme) => ({ - color: theme.palette.text.secondary, - }), -} satisfies Record>; - -export default GroupPage; diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPage.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPage.tsx deleted file mode 100644 index 0e31af80e359a..0000000000000 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPage.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import GroupAdd from "@mui/icons-material/GroupAddOutlined"; -import Button from "@mui/material/Button"; -import { getErrorMessage } from "api/errors"; -import { groupsByOrganization } from "api/queries/groups"; -import { organizationPermissions } from "api/queries/organizations"; -import type { Organization } from "api/typesGenerated"; -import { EmptyState } from "components/EmptyState/EmptyState"; -import { displayError } from "components/GlobalSnackbar/utils"; -import { Loader } from "components/Loader/Loader"; -import { SettingsHeader } from "components/SettingsHeader/SettingsHeader"; -import { Stack } from "components/Stack/Stack"; -import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; -import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; -import { type FC, useEffect } from "react"; -import { Helmet } from "react-helmet-async"; -import { useQuery } from "react-query"; -import { Navigate, Link as RouterLink, useParams } from "react-router-dom"; -import { pageTitle } from "utils/page"; -import GroupsPageView from "./GroupsPageView"; - -export const GroupsPage: FC = () => { - const feats = useFeatureVisibility(); - const { organization: organizationName } = useParams() as { - organization: string; - }; - const groupsQuery = useQuery(groupsByOrganization(organizationName)); - const { organizations } = useOrganizationSettings(); - const organization = organizations?.find((o) => o.name === organizationName); - const permissionsQuery = useQuery(organizationPermissions(organization?.id)); - - useEffect(() => { - if (groupsQuery.error) { - displayError( - getErrorMessage(groupsQuery.error, "Unable to load groups."), - ); - } - }, [groupsQuery.error]); - - useEffect(() => { - if (permissionsQuery.error) { - displayError( - getErrorMessage(permissionsQuery.error, "Unable to load permissions."), - ); - } - }, [permissionsQuery.error]); - - if (!organizations) { - return ; - } - - if (!organizationName) { - const defaultName = getOrganizationNameByDefault(organizations); - if (defaultName) { - return ; - } - // We expect there to always be a default organization. - throw new Error("No default organization found"); - } - - if (!organization) { - return ; - } - - const permissions = permissionsQuery.data; - if (!permissions) { - return ; - } - - return ( - <> - - - {pageTitle("Groups", organization.display_name || organization.name)} - - - - - - {permissions.createGroup && feats.template_rbac && ( - - )} - - - - - ); -}; - -export default GroupsPage; - -export const getOrganizationNameByDefault = ( - organizations: readonly Organization[], -) => { - return organizations.find((org) => org.is_default)?.name; -}; diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPageView.stories.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPageView.stories.tsx deleted file mode 100644 index 8198243ca2de5..0000000000000 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPageView.stories.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { MockGroup } from "testHelpers/entities"; -import { GroupsPageView } from "./GroupsPageView"; - -const meta: Meta = { - title: "pages/OrganizationGroupsPage", - component: GroupsPageView, -}; - -export default meta; -type Story = StoryObj; - -export const NotEnabled: Story = { - args: { - groups: [MockGroup], - canCreateGroup: true, - isTemplateRBACEnabled: false, - }, -}; - -export const WithGroups: Story = { - args: { - groups: [MockGroup], - canCreateGroup: true, - isTemplateRBACEnabled: true, - }, -}; - -export const WithDisplayGroup: Story = { - args: { - groups: [{ ...MockGroup, name: "front-end" }], - canCreateGroup: true, - isTemplateRBACEnabled: true, - }, -}; - -export const EmptyGroup: Story = { - args: { - groups: [], - canCreateGroup: false, - isTemplateRBACEnabled: true, - }, -}; - -export const EmptyGroupWithPermission: Story = { - args: { - groups: [], - canCreateGroup: true, - isTemplateRBACEnabled: true, - }, -}; diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPageView.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPageView.tsx deleted file mode 100644 index fe109d0ea5718..0000000000000 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPageView.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import type { Interpolation, Theme } from "@emotion/react"; -import AddOutlined from "@mui/icons-material/AddOutlined"; -import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight"; -import AvatarGroup from "@mui/material/AvatarGroup"; -import Button from "@mui/material/Button"; -import Skeleton from "@mui/material/Skeleton"; -import Table from "@mui/material/Table"; -import TableBody from "@mui/material/TableBody"; -import TableCell from "@mui/material/TableCell"; -import TableContainer from "@mui/material/TableContainer"; -import TableHead from "@mui/material/TableHead"; -import TableRow from "@mui/material/TableRow"; -import type { Group } from "api/typesGenerated"; -import { Avatar } from "components/Avatar/Avatar"; -import { AvatarData } from "components/Avatar/AvatarData"; -import { AvatarDataSkeleton } from "components/Avatar/AvatarDataSkeleton"; -import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; -import { EmptyState } from "components/EmptyState/EmptyState"; -import { Paywall } from "components/Paywall/Paywall"; -import { - TableLoaderSkeleton, - TableRowSkeleton, -} from "components/TableLoader/TableLoader"; -import { useClickableTableRow } from "hooks"; -import type { FC } from "react"; -import { Link as RouterLink, useNavigate } from "react-router-dom"; -import { docs } from "utils/docs"; - -export type GroupsPageViewProps = { - groups: Group[] | undefined; - canCreateGroup: boolean; - isTemplateRBACEnabled: boolean; -}; - -export const GroupsPageView: FC = ({ - groups, - canCreateGroup, - isTemplateRBACEnabled, -}) => { - const isLoading = Boolean(groups === undefined); - const isEmpty = Boolean(groups && groups.length === 0); - - return ( - <> - - - - - - - - - - Name - Users - - - - - - - - - - - - - } - variant="contained" - > - Create group - - ) - } - /> - - - - - - {groups?.map((group) => ( - - ))} - - - -
-
-
-
- - ); -}; - -interface GroupRowProps { - group: Group; -} - -const GroupRow: FC = ({ group }) => { - const navigate = useNavigate(); - const rowProps = useClickableTableRow({ - onClick: () => navigate(group.name), - }); - - return ( - - - - } - title={group.display_name || group.name} - subtitle={`${group.members.length} members`} - /> - - - - {group.members.length === 0 && "-"} - - {group.members.map((member) => ( - - ))} - - - - -
- -
-
-
- ); -}; - -const TableLoader: FC = () => { - return ( - - - -
- -
-
- - - - - - -
-
- ); -}; - -const styles = { - arrowRight: (theme) => ({ - color: theme.palette.text.secondary, - width: 20, - height: 20, - }), - arrowCell: { - display: "flex", - }, -} satisfies Record>; - -export default GroupsPageView; diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpMappingTable.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpMappingTable.tsx deleted file mode 100644 index c277c7a14e1c9..0000000000000 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpMappingTable.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import Table from "@mui/material/Table"; -import TableBody from "@mui/material/TableBody"; -import TableCell from "@mui/material/TableCell"; -import TableContainer from "@mui/material/TableContainer"; -import TableHead from "@mui/material/TableHead"; -import TableRow from "@mui/material/TableRow"; -import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; -import { EmptyState } from "components/EmptyState/EmptyState"; -import { Link } from "components/Link/Link"; -import type { FC } from "react"; -import { docs } from "utils/docs"; - -interface IdpMappingTableProps { - type: "Role" | "Group"; - rowCount: number; - children: React.ReactNode; -} - -export const IdpMappingTable: FC = ({ - type, - rowCount, - children, -}) => { - return ( -
- - - - - IdP {type.toLocaleLowerCase()} - - Coder {type.toLocaleLowerCase()} - - - - - - - - - - - How to setup IdP {type.toLocaleLowerCase()} sync - - } - /> - - - - {children} - - -
-
-
-
- Showing {rowCount}{" "} - groups -
-
-
- ); -}; diff --git a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.stories.tsx b/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.stories.tsx deleted file mode 100644 index f6b6b49c88d37..0000000000000 --- a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.stories.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { reactRouterParameters } from "storybook-addon-remix-react-router"; -import { - MockDefaultOrganization, - MockOrganization, - MockOrganization2, - MockUser, -} from "testHelpers/entities"; -import { - withAuthProvider, - withDashboardProvider, - withManagementSettingsProvider, -} from "testHelpers/storybook"; -import OrganizationSettingsPage from "./OrganizationSettingsPage"; - -const meta: Meta = { - title: "pages/OrganizationSettingsPage", - component: OrganizationSettingsPage, - decorators: [ - withAuthProvider, - withDashboardProvider, - withManagementSettingsProvider, - ], - parameters: { - showOrganizations: true, - user: MockUser, - features: ["multiple_organizations"], - permissions: { viewDeploymentValues: true }, - queries: [ - { - key: ["organizations", [MockDefaultOrganization.id], "permissions"], - data: {}, - }, - ], - }, -}; - -export default meta; -type Story = StoryObj; - -export const NoRedirectableOrganizations: Story = {}; - -export const OrganizationDoesNotExist: Story = { - parameters: { - reactRouter: reactRouterParameters({ - location: { pathParams: { organization: "does-not-exist" } }, - routing: { path: "/organizations/:organization" }, - }), - }, -}; - -export const CannotEditOrganization: Story = { - parameters: { - reactRouter: reactRouterParameters({ - location: { pathParams: { organization: MockDefaultOrganization.name } }, - routing: { path: "/organizations/:organization" }, - }), - }, -}; - -export const CanEditOrganization: Story = { - parameters: { - reactRouter: reactRouterParameters({ - location: { pathParams: { organization: MockDefaultOrganization.name } }, - routing: { path: "/organizations/:organization" }, - }), - queries: [ - { - key: ["organizations", [MockDefaultOrganization.id], "permissions"], - data: { - [MockDefaultOrganization.id]: { - editOrganization: true, - }, - }, - }, - ], - }, -}; - -export const CanEditOrganizationNotEntitled: Story = { - parameters: { - reactRouter: reactRouterParameters({ - location: { pathParams: { organization: MockDefaultOrganization.name } }, - routing: { path: "/organizations/:organization" }, - }), - features: [], - queries: [ - { - key: ["organizations", [MockDefaultOrganization.id], "permissions"], - data: { - [MockDefaultOrganization.id]: { - editOrganization: true, - }, - }, - }, - ], - }, -}; diff --git a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx b/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx deleted file mode 100644 index 698f2ee75822f..0000000000000 --- a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { - deleteOrganization, - organizationsPermissions, - updateOrganization, -} from "api/queries/organizations"; -import { ErrorAlert } from "components/Alert/ErrorAlert"; -import { EmptyState } from "components/EmptyState/EmptyState"; -import { displaySuccess } from "components/GlobalSnackbar/utils"; -import { Loader } from "components/Loader/Loader"; -import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; -import { canEditOrganization } from "modules/management/OrganizationSettingsLayout"; -import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; -import type { FC } from "react"; -import { useMutation, useQuery, useQueryClient } from "react-query"; -import { Navigate, useNavigate, useParams } from "react-router-dom"; -import { OrganizationSettingsPageView } from "./OrganizationSettingsPageView"; -import { OrganizationSummaryPageView } from "./OrganizationSummaryPageView"; - -const OrganizationSettingsPage: FC = () => { - const { organization: organizationName } = useParams() as { - organization?: string; - }; - const { organizations } = useOrganizationSettings(); - const feats = useFeatureVisibility(); - - const navigate = useNavigate(); - const queryClient = useQueryClient(); - const updateOrganizationMutation = useMutation( - updateOrganization(queryClient), - ); - const deleteOrganizationMutation = useMutation( - deleteOrganization(queryClient), - ); - - const organization = organizations?.find((o) => o.name === organizationName); - const permissionsQuery = useQuery( - organizationsPermissions(organizations?.map((o) => o.id)), - ); - - if (permissionsQuery.isLoading) { - return ; - } - - const permissions = permissionsQuery.data; - if (permissionsQuery.error || !permissions) { - return ; - } - - // Redirect /organizations => /organizations/default-org, or if they cannot edit - // the default org, then the first org they can edit, if any. - if (!organizationName) { - // .find will stop at the first match found; make sure default - // organizations are placed first - const editableOrg = [...organizations] - .sort((a, b) => (b.is_default ? 1 : 0) - (a.is_default ? 1 : 0)) - .find((org) => canEditOrganization(permissions[org.id])); - if (editableOrg) { - return ; - } - return ; - } - - if (!organization) { - return ; - } - - // The user may not be able to edit this org but they can still see it because - // they can edit members, etc. In this case they will be shown a read-only - // summary page instead of the settings form. - // Similarly, if the feature is not entitled then the user will not be able to - // edit the organization. - if ( - !permissions[organization.id]?.editOrganization || - !feats.multiple_organizations - ) { - return ; - } - - const error = - updateOrganizationMutation.error ?? deleteOrganizationMutation.error; - - return ( - { - const updatedOrganization = - await updateOrganizationMutation.mutateAsync({ - organizationId: organization.id, - req: values, - }); - navigate(`/organizations/${updatedOrganization.name}/settings`); - displaySuccess("Organization settings updated."); - }} - onDeleteOrganization={() => { - deleteOrganizationMutation.mutate(organization.id); - displaySuccess("Organization deleted."); - navigate("/organizations"); - }} - /> - ); -}; - -export default OrganizationSettingsPage; diff --git a/site/src/pages/ManagementSettingsPage/OrganizationSummaryPageView.stories.tsx b/site/src/pages/ManagementSettingsPage/OrganizationSummaryPageView.stories.tsx deleted file mode 100644 index 92567ad99fac4..0000000000000 --- a/site/src/pages/ManagementSettingsPage/OrganizationSummaryPageView.stories.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { - MockDefaultOrganization, - MockOrganization, -} from "testHelpers/entities"; -import { OrganizationSummaryPageView } from "./OrganizationSummaryPageView"; - -const meta: Meta = { - title: "pages/OrganizationSummaryPageView", - component: OrganizationSummaryPageView, - args: { - organization: MockOrganization, - }, -}; - -export default meta; -type Story = StoryObj; - -export const DefaultOrg: Story = { - args: { - organization: MockDefaultOrganization, - }, -}; diff --git a/site/src/pages/ManagementSettingsPage/OrganizationSummaryPageView.tsx b/site/src/pages/ManagementSettingsPage/OrganizationSummaryPageView.tsx deleted file mode 100644 index c12b3c13a416c..0000000000000 --- a/site/src/pages/ManagementSettingsPage/OrganizationSummaryPageView.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import type { Organization } from "api/typesGenerated"; -import { Avatar } from "components/Avatar/Avatar"; -import { - PageHeader, - PageHeaderSubtitle, - PageHeaderTitle, -} from "components/PageHeader/PageHeader"; -import { Stack } from "components/Stack/Stack"; -import type { FC } from "react"; - -interface OrganizationSummaryPageViewProps { - organization: Organization; -} - -export const OrganizationSummaryPageView: FC< - OrganizationSummaryPageViewProps -> = ({ organization }) => { - return ( -
- - - - -
- - {organization.display_name || organization.name} - - {organization.description && ( - - {organization.description} - - )} -
-
-
- You are a member of this organization. -
- ); -}; diff --git a/site/src/pages/ManagementSettingsPage/CreateOrganizationPage.tsx b/site/src/pages/OrganizationSettingsPage/CreateOrganizationPage.tsx similarity index 58% rename from site/src/pages/ManagementSettingsPage/CreateOrganizationPage.tsx rename to site/src/pages/OrganizationSettingsPage/CreateOrganizationPage.tsx index d685028d98256..cecfae677f4b9 100644 --- a/site/src/pages/ManagementSettingsPage/CreateOrganizationPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/CreateOrganizationPage.tsx @@ -1,5 +1,7 @@ import { createOrganization } from "api/queries/organizations"; import { displaySuccess } from "components/GlobalSnackbar/utils"; +import { useAuthenticated } from "contexts/auth/RequireAuth"; +import { RequirePermission } from "contexts/auth/RequirePermission"; import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; import type { FC } from "react"; import { useMutation, useQueryClient } from "react-query"; @@ -9,6 +11,7 @@ import { CreateOrganizationPageView } from "./CreateOrganizationPageView"; const CreateOrganizationPage: FC = () => { const navigate = useNavigate(); const feats = useFeatureVisibility(); + const { permissions } = useAuthenticated(); const queryClient = useQueryClient(); const createOrganizationMutation = useMutation( @@ -19,15 +22,17 @@ const CreateOrganizationPage: FC = () => { return (
- { - await createOrganizationMutation.mutateAsync(values); - displaySuccess("Organization created."); - navigate(`/organizations/${values.name}`); - }} - /> + + { + await createOrganizationMutation.mutateAsync(values); + displaySuccess("Organization created."); + navigate(`/organizations/${values.name}`); + }} + /> +
); }; diff --git a/site/src/pages/ManagementSettingsPage/CreateOrganizationPageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/CreateOrganizationPageView.stories.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/CreateOrganizationPageView.stories.tsx rename to site/src/pages/OrganizationSettingsPage/CreateOrganizationPageView.stories.tsx diff --git a/site/src/pages/ManagementSettingsPage/CreateOrganizationPageView.tsx b/site/src/pages/OrganizationSettingsPage/CreateOrganizationPageView.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/CreateOrganizationPageView.tsx rename to site/src/pages/OrganizationSettingsPage/CreateOrganizationPageView.tsx diff --git a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CreateEditRolePage.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePage.tsx similarity index 81% rename from site/src/pages/ManagementSettingsPage/CustomRolesPage/CreateEditRolePage.tsx rename to site/src/pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePage.tsx index 9bb27679689fa..43ae73598059e 100644 --- a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CreateEditRolePage.tsx +++ b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePage.tsx @@ -1,13 +1,14 @@ import { getErrorMessage } from "api/errors"; -import { organizationPermissions } from "api/queries/organizations"; import { createOrganizationRole, organizationRoles, updateOrganizationRole, } from "api/queries/roles"; import type { CustomRoleRequest } from "api/typesGenerated"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; import { displayError } from "components/GlobalSnackbar/utils"; import { Loader } from "components/Loader/Loader"; +import { RequirePermission } from "contexts/auth/RequirePermission"; import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; import type { FC } from "react"; import { Helmet } from "react-helmet-async"; @@ -24,9 +25,7 @@ export const CreateEditRolePage: FC = () => { organization: string; roleName: string; }; - const { organizations } = useOrganizationSettings(); - const organization = organizations?.find((o) => o.name === organizationName); - const permissionsQuery = useQuery(organizationPermissions(organization?.id)); + const { organizationPermissions } = useOrganizationSettings(); const createOrganizationRoleMutation = useMutation( createOrganizationRole(queryClient, organizationName), ); @@ -37,14 +36,22 @@ export const CreateEditRolePage: FC = () => { organizationRoles(organizationName), ); const role = roleData?.find((role) => role.name === roleName); - const permissions = permissionsQuery.data; - if (isLoading || !permissions) { + if (isLoading) { return ; } + if (!organizationPermissions) { + return ; + } + return ( - <> + {pageTitle( @@ -80,9 +87,9 @@ export const CreateEditRolePage: FC = () => { : createOrganizationRoleMutation.isLoading } organizationName={organizationName} - canAssignOrgRole={permissions.assignOrgRole} + canAssignOrgRole={organizationPermissions.assignOrgRoles} /> - </> + </RequirePermission> ); }; diff --git a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CreateEditRolePageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePageView.stories.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/CustomRolesPage/CreateEditRolePageView.stories.tsx rename to site/src/pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePageView.stories.tsx diff --git a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CreateEditRolePageView.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePageView.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/CustomRolesPage/CreateEditRolePageView.tsx rename to site/src/pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePageView.tsx diff --git a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPage.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx similarity index 85% rename from site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPage.tsx rename to site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx index 905e67ebd26e3..4e7b8c386120a 100644 --- a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx @@ -1,5 +1,4 @@ import { getErrorMessage } from "api/errors"; -import { organizationPermissions } from "api/queries/organizations"; import { deleteOrganizationRole, organizationRoles } from "api/queries/roles"; import type { Role } from "api/typesGenerated"; import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog"; @@ -7,6 +6,7 @@ import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"; import { Loader } from "components/Loader/Loader"; import { SettingsHeader } from "components/SettingsHeader/SettingsHeader"; import { Stack } from "components/Stack/Stack"; +import { RequirePermission } from "contexts/auth/RequirePermission"; import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; import { type FC, useEffect, useState } from "react"; @@ -22,13 +22,10 @@ export const CustomRolesPage: FC = () => { const { organization: organizationName } = useParams() as { organization: string; }; - const { organizations } = useOrganizationSettings(); - const organization = organizations?.find((o) => o.name === organizationName); - const permissionsQuery = useQuery(organizationPermissions(organization?.id)); - const deleteRoleMutation = useMutation( - deleteOrganizationRole(queryClient, organizationName), - ); + const { organizationPermissions } = useOrganizationSettings(); + const [roleToDelete, setRoleToDelete] = useState<Role>(); + const organizationRolesQuery = useQuery(organizationRoles(organizationName)); const builtInRoles = organizationRolesQuery.data?.filter( (role) => role.built_in, @@ -36,7 +33,10 @@ export const CustomRolesPage: FC = () => { const customRoles = organizationRolesQuery.data?.filter( (role) => !role.built_in, ); - const permissions = permissionsQuery.data; + + const deleteRoleMutation = useMutation( + deleteOrganizationRole(queryClient, organizationName), + ); useEffect(() => { if (organizationRolesQuery.error) { @@ -49,12 +49,18 @@ export const CustomRolesPage: FC = () => { } }, [organizationRolesQuery.error]); - if (!permissions) { + if (!organizationPermissions) { return <Loader />; } return ( - <> + <RequirePermission + isFeatureVisible={ + organizationPermissions.assignOrgRoles || + organizationPermissions.createOrgRoles || + organizationPermissions.viewOrgRoles + } + > <Helmet> <title>{pageTitle("Custom Roles")} @@ -74,7 +80,8 @@ export const CustomRolesPage: FC = () => { builtInRoles={builtInRoles} customRoles={customRoles} onDeleteRole={setRoleToDelete} - canAssignOrgRole={permissions.assignOrgRole} + canAssignOrgRole={organizationPermissions.assignOrgRoles} + canCreateOrgRole={organizationPermissions.createOrgRoles} isCustomRolesEnabled={isCustomRolesEnabled} /> @@ -100,7 +107,7 @@ export const CustomRolesPage: FC = () => { } }} /> - + ); }; diff --git a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.stories.tsx similarity index 71% rename from site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPageView.stories.tsx rename to site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.stories.tsx index f37e23a1e989a..79319c888647f 100644 --- a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPageView.stories.tsx +++ b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.stories.tsx @@ -8,44 +8,38 @@ import { CustomRolesPageView } from "./CustomRolesPageView"; const meta: Meta = { title: "pages/OrganizationCustomRolesPage", component: CustomRolesPageView, + args: { + builtInRoles: [MockRoleWithOrgPermissions], + customRoles: [MockRoleWithOrgPermissions], + canAssignOrgRole: true, + canCreateOrgRole: true, + isCustomRolesEnabled: true, + }, }; export default meta; type Story = StoryObj; +export const Enabled: Story = {}; + export const NotEnabled: Story = { args: { - builtInRoles: [MockRoleWithOrgPermissions], - customRoles: [MockRoleWithOrgPermissions], - canAssignOrgRole: true, isCustomRolesEnabled: false, }, }; export const NotEnabledEmptyTable: Story = { args: { - builtInRoles: [MockRoleWithOrgPermissions], customRoles: [], canAssignOrgRole: true, isCustomRolesEnabled: false, }, }; -export const Enabled: Story = { - args: { - builtInRoles: [MockRoleWithOrgPermissions], - customRoles: [MockRoleWithOrgPermissions], - canAssignOrgRole: true, - isCustomRolesEnabled: true, - }, -}; - export const RoleWithoutPermissions: Story = { args: { builtInRoles: [MockOrganizationAuditorRole], customRoles: [MockOrganizationAuditorRole], - canAssignOrgRole: true, - isCustomRolesEnabled: true, }, }; @@ -58,26 +52,19 @@ export const EmptyDisplayName: Story = { display_name: "", }, ], - builtInRoles: [MockRoleWithOrgPermissions], - canAssignOrgRole: true, - isCustomRolesEnabled: true, }, }; export const EmptyTableUserWithoutPermission: Story = { args: { - builtInRoles: [MockRoleWithOrgPermissions], customRoles: [], canAssignOrgRole: false, - isCustomRolesEnabled: true, + canCreateOrgRole: false, }, }; export const EmptyTableUserWithPermission: Story = { args: { - builtInRoles: [MockRoleWithOrgPermissions], customRoles: [], - canAssignOrgRole: true, - isCustomRolesEnabled: true, }, }; diff --git a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPageView.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.tsx similarity index 98% rename from site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPageView.tsx rename to site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.tsx index c1aa2223703d2..1bb1f049aa804 100644 --- a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPageView.tsx +++ b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.tsx @@ -35,6 +35,7 @@ interface CustomRolesPageViewProps { customRoles: AssignableRoles[] | undefined; onDeleteRole: (role: Role) => void; canAssignOrgRole: boolean; + canCreateOrgRole: boolean; isCustomRolesEnabled: boolean; } @@ -43,6 +44,7 @@ export const CustomRolesPageView: FC = ({ customRoles, onDeleteRole, canAssignOrgRole, + canCreateOrgRole, isCustomRolesEnabled, }) => { return ( @@ -66,7 +68,7 @@ export const CustomRolesPageView: FC = ({ permissions. - {canAssignOrgRole && isCustomRolesEnabled && ( + {canCreateOrgRole && isCustomRolesEnabled && ( diff --git a/site/src/pages/ManagementSettingsPage/CustomRolesPage/PermissionPillsList.stories.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/PermissionPillsList.stories.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/CustomRolesPage/PermissionPillsList.stories.tsx rename to site/src/pages/OrganizationSettingsPage/CustomRolesPage/PermissionPillsList.stories.tsx diff --git a/site/src/pages/ManagementSettingsPage/CustomRolesPage/PermissionPillsList.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/PermissionPillsList.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/CustomRolesPage/PermissionPillsList.tsx rename to site/src/pages/OrganizationSettingsPage/CustomRolesPage/PermissionPillsList.tsx diff --git a/site/src/pages/ManagementSettingsPage/Horizontal.tsx b/site/src/pages/OrganizationSettingsPage/Horizontal.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/Horizontal.tsx rename to site/src/pages/OrganizationSettingsPage/Horizontal.tsx diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx rename to site/src/pages/OrganizationSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.tsx b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/ExportPolicyButton.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.tsx rename to site/src/pages/OrganizationSettingsPage/IdpSyncPage/ExportPolicyButton.tsx diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx similarity index 78% rename from site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx rename to site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx index 2f1c0be7fa602..5340ec99dda79 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx +++ b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx @@ -1,11 +1,10 @@ -import TableCell from "@mui/material/TableCell"; -import TableRow from "@mui/material/TableRow"; import type { Group, GroupSyncSettings, Organization, } from "api/typesGenerated"; import { Button } from "components/Button/Button"; +import { Combobox } from "components/Combobox/Combobox"; import { HelpTooltip, HelpTooltipContent, @@ -22,9 +21,16 @@ import { } from "components/MultiSelectCombobox/MultiSelectCombobox"; import { Spinner } from "components/Spinner/Spinner"; import { Switch } from "components/Switch/Switch"; +import { TableCell, TableRow } from "components/Table/Table"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "components/Tooltip/Tooltip"; import { useFormik } from "formik"; -import { Plus, Trash } from "lucide-react"; -import { type FC, useId, useState } from "react"; +import { Plus, Trash, TriangleAlert } from "lucide-react"; +import { type FC, type KeyboardEventHandler, useId, useState } from "react"; import { docs } from "utils/docs"; import { isUUID } from "utils/uuid"; import * as Yup from "yup"; @@ -32,16 +38,6 @@ import { ExportPolicyButton } from "./ExportPolicyButton"; import { IdpMappingTable } from "./IdpMappingTable"; import { IdpPillList } from "./IdpPillList"; -interface IdpGroupSyncFormProps { - groupSyncSettings: GroupSyncSettings; - groupsMap: Map; - groups: Group[]; - groupMappingCount: number; - legacyGroupMappingCount: number; - organization: Organization; - onSubmit: (data: GroupSyncSettings) => void; -} - const groupSyncValidationSchema = Yup.object({ field: Yup.string().trim(), regex_filter: Yup.string().trim(), @@ -65,15 +61,29 @@ const groupSyncValidationSchema = Yup.object({ .default({}), }); -export const IdpGroupSyncForm = ({ +interface IdpGroupSyncFormProps { + groupSyncSettings: GroupSyncSettings; + claimFieldValues: readonly string[] | undefined; + groupsMap: Map; + groups: Group[]; + groupMappingCount: number; + legacyGroupMappingCount: number; + organization: Organization; + onSubmit: (data: GroupSyncSettings) => void; + onSyncFieldChange: (value: string) => void; +} + +export const IdpGroupSyncForm: FC = ({ groupSyncSettings, + claimFieldValues, groupMappingCount, legacyGroupMappingCount, groups, groupsMap, organization, onSubmit, -}: IdpGroupSyncFormProps) => { + onSyncFieldChange, +}) => { const form = useFormik({ initialValues: { field: groupSyncSettings?.field ?? "", @@ -89,6 +99,8 @@ export const IdpGroupSyncForm = ({ const [idpGroupName, setIdpGroupName] = useState(""); const [coderGroups, setCoderGroups] = useState([]); const id = useId(); + const [comboInputValue, setComboInputValue] = useState(""); + const [open, setOpen] = useState(false); const getGroupNames = (groupIds: readonly string[]) => { return groupIds.map((groupId) => groupsMap.get(groupId) || groupId); @@ -108,6 +120,21 @@ export const IdpGroupSyncForm = ({ form.handleSubmit(); }; + const handleKeyDown: KeyboardEventHandler = (event) => { + if ( + event.key === "Enter" && + comboInputValue && + !claimFieldValues?.some( + (value) => value === comboInputValue.toLowerCase(), + ) + ) { + event.preventDefault(); + setIdpGroupName(comboInputValue); + setComboInputValue(""); + setOpen(false); + } + }; + return (
{ void form.setFieldValue("field", event.target.value); + onSyncFieldChange(event.target.value); }} className="w-72" /> @@ -194,14 +222,31 @@ export const IdpGroupSyncForm = ({ - { - setIdpGroupName(event.target.value); - }} - /> + {claimFieldValues ? ( + { + setIdpGroupName(value); + setOpen(false); + }} + /> + ) : ( + { + setIdpGroupName(event.target.value); + }} + /> + )}
diff --git a/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.test.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.test.tsx similarity index 94% rename from site/src/pages/ManagementSettingsPage/OrganizationMembersPage.test.tsx rename to site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.test.tsx index 0c9c7d44bd15a..1270f78484dc7 100644 --- a/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.test.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.test.tsx @@ -6,6 +6,7 @@ import { MockEntitlementsWithMultiOrg, MockOrganization, MockOrganizationAuditorRole, + MockOrganizationPermissions, MockUser, } from "testHelpers/entities"; import { @@ -23,10 +24,14 @@ beforeEach(() => { return HttpResponse.json(MockEntitlementsWithMultiOrg); }), http.post("/api/v2/authcheck", async () => { - return HttpResponse.json({ - editMembers: true, - viewDeploymentValues: true, - }); + return HttpResponse.json( + Object.fromEntries( + Object.entries(MockOrganizationPermissions).map(([key, value]) => [ + `${MockOrganization.id}.${key}`, + value, + ]), + ), + ); }), ); }); diff --git a/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx similarity index 90% rename from site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx rename to site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx index ac90365ea4d43..7ae0eb72bec91 100644 --- a/site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx @@ -4,15 +4,14 @@ import { groupsByUserIdInOrganization } from "api/queries/groups"; import { addOrganizationMember, organizationMembers, - organizationPermissions, removeOrganizationMember, updateOrganizationMemberRoles, } from "api/queries/organizations"; import { organizationRoles } from "api/queries/roles"; import type { OrganizationMemberWithUserData, User } from "api/typesGenerated"; import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; +import { EmptyState } from "components/EmptyState/EmptyState"; import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"; -import { Loader } from "components/Loader/Loader"; import { Stack } from "components/Stack/Stack"; import { useAuthenticated } from "contexts/auth/RequireAuth"; import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; @@ -25,18 +24,18 @@ import { OrganizationMembersPageView } from "./OrganizationMembersPageView"; const OrganizationMembersPage: FC = () => { const queryClient = useQueryClient(); + const { user: me } = useAuthenticated(); const { organization: organizationName } = useParams() as { organization: string; }; - const { user: me } = useAuthenticated(); + const { organization, organizationPermissions } = useOrganizationSettings(); + const membersQuery = useQuery(organizationMembers(organizationName)); + const organizationRolesQuery = useQuery(organizationRoles(organizationName)); const groupsByUserIdQuery = useQuery( groupsByUserIdInOrganization(organizationName), ); - const membersQuery = useQuery(organizationMembers(organizationName)); - const organizationRolesQuery = useQuery(organizationRoles(organizationName)); - const members = membersQuery.data?.map((member) => { const groups = groupsByUserIdQuery.data?.get(member.user_id) ?? []; return { ...member, groups }; @@ -52,19 +51,14 @@ const OrganizationMembersPage: FC = () => { updateOrganizationMemberRoles(queryClient, organizationName), ); - const { organizations } = useOrganizationSettings(); - const organization = organizations?.find((o) => o.name === organizationName); - const permissionsQuery = useQuery(organizationPermissions(organization?.id)); - const [memberToDelete, setMemberToDelete] = useState(); - const permissions = permissionsQuery.data; - if (!permissions) { - return ; + if (!organization || !organizationPermissions) { + return ; } - const helmet = organization && ( + const helmet = ( {pageTitle("Members", organization.display_name || organization.name)} @@ -77,9 +71,12 @@ const OrganizationMembersPage: FC = () => { {helmet} <OrganizationMembersPageView allAvailableRoles={organizationRolesQuery.data} - canEditMembers={permissions.editMembers} + canEditMembers={organizationPermissions.editMembers} + canViewMembers={organizationPermissions.viewMembers} error={ membersQuery.error ?? + organizationRolesQuery.error ?? + groupsByUserIdQuery.error ?? addMemberMutation.error ?? removeMemberMutation.error ?? updateMemberRolesMutation.error diff --git a/site/src/pages/ManagementSettingsPage/OrganizationMembersPageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.stories.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/OrganizationMembersPageView.stories.tsx rename to site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.stories.tsx diff --git a/site/src/pages/ManagementSettingsPage/OrganizationMembersPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx similarity index 51% rename from site/src/pages/ManagementSettingsPage/OrganizationMembersPageView.tsx rename to site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx index 1cc009e1f4de7..743e8a9381e15 100644 --- a/site/src/pages/ManagementSettingsPage/OrganizationMembersPageView.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx @@ -1,14 +1,6 @@ -import type { Interpolation, Theme } from "@emotion/react"; import PersonAdd from "@mui/icons-material/PersonAdd"; import LoadingButton from "@mui/lab/LoadingButton"; -import Table from "@mui/material/Table"; -import TableBody from "@mui/material/TableBody"; -import TableCell from "@mui/material/TableCell"; -import TableContainer from "@mui/material/TableContainer"; -import TableHead from "@mui/material/TableHead"; -import TableRow from "@mui/material/TableRow"; import { getErrorMessage } from "api/errors"; -import type { GroupsByUserId } from "api/queries/groups"; import type { Group, OrganizationMemberWithUserData, @@ -28,7 +20,15 @@ import { } from "components/MoreMenu/MoreMenu"; import { SettingsHeader } from "components/SettingsHeader/SettingsHeader"; import { Stack } from "components/Stack/Stack"; +import { + Table, + TableBody, + TableCell, + TableHeader, + TableRow, +} from "components/Table/Table"; import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete"; +import { TriangleAlert } from "lucide-react"; import { UserGroupsCell } from "pages/UsersPage/UsersTable/UserGroupsCell"; import { type FC, useState } from "react"; import { TableColumnHelpTooltip } from "./UserTable/TableColumnHelpTooltip"; @@ -37,6 +37,7 @@ import { UserRoleCell } from "./UserTable/UserRoleCell"; interface OrganizationMembersPageViewProps { allAvailableRoles: readonly SlimRole[] | undefined; canEditMembers: boolean; + canViewMembers: boolean; error: unknown; isAddingMember: boolean; isUpdatingMemberRoles: boolean; @@ -59,6 +60,7 @@ export const OrganizationMembersPageView: FC< > = ({ allAvailableRoles, canEditMembers, + canViewMembers, error, isAddingMember, isUpdatingMemberRoles, @@ -71,7 +73,7 @@ export const OrganizationMembersPageView: FC< return ( <div> <SettingsHeader title="Members" /> - <Stack> + <div className="flex flex-col gap-4"> {Boolean(error) && <ErrorAlert error={error} />} {canEditMembers && ( @@ -81,83 +83,90 @@ export const OrganizationMembersPageView: FC< /> )} - <TableContainer> - <Table> - <TableHead> - <TableRow> - <TableCell width="33%">User</TableCell> - <TableCell width="33%"> - <Stack direction="row" spacing={1} alignItems="center"> - <span>Roles</span> - <TableColumnHelpTooltip variant="roles" /> - </Stack> + {!canViewMembers && ( + <div className="flex flex-row text-content-warning gap-2 items-center text-sm font-medium"> + <TriangleAlert className="size-icon-sm" /> + <p> + You do not have permission to view members other than yourself. + </p> + </div> + )} + + <Table> + <TableHeader> + <TableRow> + <TableCell width="33%">User</TableCell> + <TableCell width="33%"> + <Stack direction="row" spacing={1} alignItems="center"> + <span>Roles</span> + <TableColumnHelpTooltip variant="roles" /> + </Stack> + </TableCell> + <TableCell width="33%"> + <Stack direction="row" spacing={1} alignItems="center"> + <span>Groups</span> + <TableColumnHelpTooltip variant="groups" /> + </Stack> + </TableCell> + <TableCell width="1%" /> + </TableRow> + </TableHeader> + <TableBody> + {members?.map((member) => ( + <TableRow key={member.user_id} className="align-baseline"> + <TableCell> + <AvatarData + avatar={ + <Avatar + fallback={member.username} + src={member.avatar_url} + /> + } + title={member.name || member.username} + subtitle={member.email} + /> </TableCell> - <TableCell width="33%"> - <Stack direction="row" spacing={1} alignItems="center"> - <span>Groups</span> - <TableColumnHelpTooltip variant="groups" /> - </Stack> + <UserRoleCell + inheritedRoles={member.global_roles} + roles={member.roles} + allAvailableRoles={allAvailableRoles} + oidcRoleSyncEnabled={false} + isLoading={isUpdatingMemberRoles} + canEditUsers={canEditMembers} + onEditRoles={async (roles) => { + try { + await updateMemberRoles(member, roles); + displaySuccess("Roles updated successfully."); + } catch (error) { + displayError( + getErrorMessage(error, "Failed to update roles."), + ); + } + }} + /> + <UserGroupsCell userGroups={member.groups} /> + <TableCell> + {member.user_id !== me.id && canEditMembers && ( + <MoreMenu> + <MoreMenuTrigger> + <ThreeDotsButton /> + </MoreMenuTrigger> + <MoreMenuContent> + <MoreMenuItem + danger + onClick={() => removeMember(member)} + > + Remove + </MoreMenuItem> + </MoreMenuContent> + </MoreMenu> + )} </TableCell> - <TableCell width="1%" /> </TableRow> - </TableHead> - <TableBody> - {members?.map((member) => ( - <TableRow key={member.user_id}> - <TableCell> - <AvatarData - avatar={ - <Avatar - fallback={member.username} - src={member.avatar_url} - /> - } - title={member.name || member.username} - subtitle={member.email} - /> - </TableCell> - <UserRoleCell - inheritedRoles={member.global_roles} - roles={member.roles} - allAvailableRoles={allAvailableRoles} - oidcRoleSyncEnabled={false} - isLoading={isUpdatingMemberRoles} - canEditUsers={canEditMembers} - onEditRoles={async (roles) => { - try { - await updateMemberRoles(member, roles); - displaySuccess("Roles updated successfully."); - } catch (error) { - displayError( - getErrorMessage(error, "Failed to update roles."), - ); - } - }} - /> - <UserGroupsCell userGroups={member.groups} /> - <TableCell> - {member.user_id !== me.id && canEditMembers && ( - <MoreMenu> - <MoreMenuTrigger> - <ThreeDotsButton /> - </MoreMenuTrigger> - <MoreMenuContent> - <MoreMenuItem - danger - onClick={() => removeMember(member)} - > - Remove - </MoreMenuItem> - </MoreMenuContent> - </MoreMenu> - )} - </TableCell> - </TableRow> - ))} - </TableBody> - </Table> - </TableContainer> - </Stack> + ))} + </TableBody> + </Table> + </div> </div> ); }; @@ -190,7 +199,7 @@ const AddOrganizationMember: FC<AddOrganizationMemberProps> = ({ > <Stack direction="row" alignItems="center" spacing={1}> <UserAutocomplete - css={styles.autoComplete} + className="w-[300px]" value={selectedUser} onChange={(newValue) => { setSelectedUser(newValue); @@ -210,17 +219,3 @@ const AddOrganizationMember: FC<AddOrganizationMemberProps> = ({ </form> ); }; - -const styles = { - role: (theme) => ({ - backgroundColor: theme.roles.notice.background, - borderColor: theme.roles.notice.outline, - }), - globalRole: (theme) => ({ - backgroundColor: theme.roles.inactive.background, - borderColor: theme.roles.inactive.outline, - }), - autoComplete: { - width: 300, - }, -} satisfies Record<string, Interpolation<Theme>>; diff --git a/site/src/pages/ManagementSettingsPage/OrganizationProvisionersPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/OrganizationProvisionersPage.tsx rename to site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage.tsx diff --git a/site/src/pages/ManagementSettingsPage/OrganizationProvisionersPageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPageView.stories.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/OrganizationProvisionersPageView.stories.tsx rename to site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPageView.stories.tsx diff --git a/site/src/pages/ManagementSettingsPage/OrganizationProvisionersPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPageView.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/OrganizationProvisionersPageView.tsx rename to site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPageView.tsx diff --git a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.test.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationRedirect.test.tsx similarity index 58% rename from site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.test.tsx rename to site/src/pages/OrganizationSettingsPage/OrganizationRedirect.test.tsx index 2978702ab9651..96e0110d21a80 100644 --- a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.test.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationRedirect.test.tsx @@ -10,19 +10,29 @@ import { waitForLoaderToBeRemoved, } from "testHelpers/renderHelpers"; import { server } from "testHelpers/server"; -import OrganizationSettingsPage from "./OrganizationSettingsPage"; +import OrganizationRedirect from "./OrganizationRedirect"; jest.spyOn(console, "error").mockImplementation(() => {}); const renderPage = async () => { - renderWithOrganizationSettingsLayout(<OrganizationSettingsPage />, { - route: "/organizations", - path: "/organizations/:organization?", - }); + const { router } = renderWithOrganizationSettingsLayout( + <OrganizationRedirect />, + { + route: "/organizations", + path: "/organizations", + extraRoutes: [ + { + path: "/organizations/:organization", + element: <h1>Organization Settings</h1>, + }, + ], + }, + ); await waitForLoaderToBeRemoved(); + return router; }; -describe("OrganizationSettingsPage", () => { +describe("OrganizationRedirect", () => { it("has no editable organizations", async () => { server.use( http.get("/api/v2/entitlements", () => { @@ -32,9 +42,7 @@ describe("OrganizationSettingsPage", () => { return HttpResponse.json([MockDefaultOrganization, MockOrganization2]); }), http.post("/api/v2/authcheck", async () => { - return HttpResponse.json({ - viewDeploymentValues: true, - }); + return HttpResponse.json({}); }), ); await renderPage(); @@ -52,16 +60,19 @@ describe("OrganizationSettingsPage", () => { }), http.post("/api/v2/authcheck", async () => { return HttpResponse.json({ - [`${MockDefaultOrganization.id}.editOrganization`]: true, - [`${MockOrganization2.id}.editOrganization`]: true, - viewDeploymentValues: true, + viewAnyMembers: true, + [`${MockDefaultOrganization.id}.viewMembers`]: true, + [`${MockDefaultOrganization.id}.editMembers`]: true, + [`${MockOrganization2.id}.viewMembers`]: true, + [`${MockOrganization2.id}.editMembers`]: true, }); }), ); - await renderPage(); - const form = screen.getByTestId("org-settings-form"); - expect(within(form).getByRole("textbox", { name: "Slug" })).toHaveValue( - MockDefaultOrganization.name, + const router = await renderPage(); + const form = screen.getByText("Organization Settings"); + expect(form).toBeInTheDocument(); + expect(router.state.location.pathname).toBe( + `/organizations/${MockDefaultOrganization.name}`, ); }); @@ -75,15 +86,18 @@ describe("OrganizationSettingsPage", () => { }), http.post("/api/v2/authcheck", async () => { return HttpResponse.json({ - [`${MockOrganization2.id}.editOrganization`]: true, - viewDeploymentValues: true, + viewAnyMembers: true, + [`${MockDefaultOrganization.id}.viewMembers`]: true, + [`${MockOrganization2.id}.viewMembers`]: true, + [`${MockOrganization2.id}.editMembers`]: true, }); }), ); - await renderPage(); - const form = screen.getByTestId("org-settings-form"); - expect(within(form).getByRole("textbox", { name: "Slug" })).toHaveValue( - MockOrganization2.name, + const router = await renderPage(); + const form = screen.getByText("Organization Settings"); + expect(form).toBeInTheDocument(); + expect(router.state.location.pathname).toBe( + `/organizations/${MockOrganization2.name}`, ); }); }); diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationRedirect.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationRedirect.tsx new file mode 100644 index 0000000000000..b862ad41dc883 --- /dev/null +++ b/site/src/pages/OrganizationSettingsPage/OrganizationRedirect.tsx @@ -0,0 +1,30 @@ +import { EmptyState } from "components/EmptyState/EmptyState"; +import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; +import { canEditOrganization } from "modules/management/organizationPermissions"; +import type { FC } from "react"; +import { Navigate } from "react-router-dom"; + +const OrganizationRedirect: FC = () => { + const { + organizations, + organizationPermissionsByOrganizationId: organizationPermissions, + } = useOrganizationSettings(); + + // Redirect /organizations => /organizations/some-organization-name + // If they can edit the default org, we should redirect to the default. + // If they cannot edit the default, we should redirect to the first org that + // they can edit. + const editableOrg = [...organizations] + .sort((a, b) => (b.is_default ? 1 : 0) - (a.is_default ? 1 : 0)) + .find((org) => canEditOrganization(organizationPermissions[org.id])); + if (editableOrg) { + return <Navigate to={`/organizations/${editableOrg.name}`} replace />; + } + // If they cannot edit any org, just redirect to an org they can read. + if (organizations.length > 0) { + return <Navigate to={`/organizations/${organizations[0].name}`} replace />; + } + return <EmptyState message="No organizations found" />; +}; + +export default OrganizationRedirect; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx new file mode 100644 index 0000000000000..3ae72b701c851 --- /dev/null +++ b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx @@ -0,0 +1,60 @@ +import { getErrorMessage } from "api/errors"; +import { + deleteOrganization, + updateOrganization, +} from "api/queries/organizations"; +import { EmptyState } from "components/EmptyState/EmptyState"; +import { displaySuccess } from "components/GlobalSnackbar/utils"; +import { displayError } from "components/GlobalSnackbar/utils"; +import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; +import type { FC } from "react"; +import { useMutation, useQueryClient } from "react-query"; +import { useNavigate } from "react-router-dom"; +import { OrganizationSettingsPageView } from "./OrganizationSettingsPageView"; + +const OrganizationSettingsPage: FC = () => { + const navigate = useNavigate(); + const queryClient = useQueryClient(); + const { organization, organizationPermissions } = useOrganizationSettings(); + + const updateOrganizationMutation = useMutation( + updateOrganization(queryClient), + ); + const deleteOrganizationMutation = useMutation( + deleteOrganization(queryClient), + ); + + if (!organization || !organizationPermissions?.editSettings) { + return <EmptyState message="Organization not found" />; + } + + const error = + updateOrganizationMutation.error ?? deleteOrganizationMutation.error; + + return ( + <OrganizationSettingsPageView + organization={organization} + error={error} + onSubmit={async (values) => { + const updatedOrganization = + await updateOrganizationMutation.mutateAsync({ + organizationId: organization.id, + req: values, + }); + navigate(`/organizations/${updatedOrganization.name}/settings`); + displaySuccess("Organization settings updated."); + }} + onDeleteOrganization={async () => { + try { + await deleteOrganizationMutation.mutateAsync(organization.id); + displaySuccess("Organization deleted"); + navigate("/organizations"); + } catch (error) { + displayError(getErrorMessage(error, "Failed to delete organization")); + } + }} + /> + ); +}; + +export default OrganizationSettingsPage; diff --git a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPageView.stories.tsx similarity index 100% rename from site/src/pages/ManagementSettingsPage/OrganizationSettingsPageView.stories.tsx rename to site/src/pages/OrganizationSettingsPage/OrganizationSettingsPageView.stories.tsx diff --git a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPageView.tsx similarity index 77% rename from site/src/pages/ManagementSettingsPage/OrganizationSettingsPageView.tsx rename to site/src/pages/OrganizationSettingsPage/OrganizationSettingsPageView.tsx index 7dcf23bf4a4a6..8ca6c517b251e 100644 --- a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPageView.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPageView.tsx @@ -75,7 +75,6 @@ export const OrganizationSettingsPageView: FC< )} <HorizontalForm - data-testid="org-settings-form" onSubmit={form.handleSubmit} aria-label="Organization settings form" > @@ -126,14 +125,18 @@ export const OrganizationSettingsPageView: FC< </HorizontalForm> {!organization.is_default && ( - <HorizontalContainer css={{ marginTop: 48 }}> + <HorizontalContainer className="mt-12"> <HorizontalSection title="Settings" description="Change or delete your organization." > - <div css={styles.dangerSettings}> + <div className="flex bg-surface-orange items-center justify-between border border-solid border-orange-600 rounded-md p-3 pl-4 gap-2 flex-grow"> <span>Deleting an organization is irreversible.</span> - <Button variant="destructive" onClick={() => setIsDeleting(true)}> + <Button + variant="destructive" + onClick={() => setIsDeleting(true)} + className="min-w-fit" + > Delete this organization </Button> </div> @@ -143,7 +146,10 @@ export const OrganizationSettingsPageView: FC< <DeleteDialog isOpen={isDeleting} - onConfirm={onDeleteOrganization} + onConfirm={async () => { + await onDeleteOrganization(); + setIsDeleting(false); + }} onCancel={() => setIsDeleting(false)} entity="organization" name={organization.name} @@ -151,45 +157,3 @@ export const OrganizationSettingsPageView: FC< </div> ); }; - -const styles = { - dangerSettings: (theme) => ({ - display: "flex", - backgroundColor: theme.roles.danger.background, - alignItems: "center", - justifyContent: "space-between", - border: `1px solid ${theme.roles.danger.outline}`, - borderRadius: 8, - padding: 12, - paddingLeft: 18, - gap: 8, - lineHeight: "18px", - flexGrow: 1, - - "& .option": { - color: theme.roles.danger.fill.solid, - "&.Mui-checked": { - color: theme.roles.danger.fill.solid, - }, - }, - - "& .info": { - fontSize: 14, - fontWeight: 600, - color: theme.roles.danger.text, - }, - }), - dangerButton: (theme) => ({ - borderColor: theme.roles.danger.outline, - color: theme.roles.danger.text, - - "&.MuiLoadingButton-loading": { - color: theme.roles.danger.disabled.text, - }, - - "&:hover:not(:disabled)": { - backgroundColor: theme.roles.danger.hover.background, - borderColor: theme.roles.danger.hover.fill.outline, - }, - }), -} satisfies Record<string, Interpolation<Theme>>; diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobButton.stories.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobButton.stories.tsx new file mode 100644 index 0000000000000..337149f17639c --- /dev/null +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobButton.stories.tsx @@ -0,0 +1,46 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { userEvent, waitFor, within } from "@storybook/test"; +import { MockProvisionerJob } from "testHelpers/entities"; +import { CancelJobButton } from "./CancelJobButton"; + +const meta: Meta<typeof CancelJobButton> = { + title: "pages/OrganizationSettingsPage/ProvisionersPage/CancelJobButton", + component: CancelJobButton, + args: { + job: { + ...MockProvisionerJob, + status: "running", + }, + }, +}; + +export default meta; +type Story = StoryObj<typeof CancelJobButton>; + +export const Cancellable: Story = {}; + +export const NotCancellable: Story = { + args: { + job: { + ...MockProvisionerJob, + status: "succeeded", + }, + }, +}; + +export const OnClick: Story = { + parameters: { + chromatic: { disableSnapshot: true }, + }, + play: async ({ canvasElement }) => { + const user = userEvent.setup(); + const canvas = within(canvasElement); + const button = canvas.getByRole("button"); + await user.click(button); + + const body = within(canvasElement.ownerDocument.body); + await waitFor(() => { + body.getByText("Cancel provisioner job"); + }); + }, +}; diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobButton.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobButton.tsx new file mode 100644 index 0000000000000..4c024911ee23f --- /dev/null +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobButton.tsx @@ -0,0 +1,53 @@ +import type { ProvisionerJob } from "api/typesGenerated"; +import { Button } from "components/Button/Button"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "components/Tooltip/Tooltip"; +import { BanIcon } from "lucide-react"; +import { type FC, useState } from "react"; +import { CancelJobConfirmationDialog } from "./CancelJobConfirmationDialog"; + +const CANCELLABLE = ["pending", "running"]; + +type CancelJobButtonProps = { + job: ProvisionerJob; +}; + +export const CancelJobButton: FC<CancelJobButtonProps> = ({ job }) => { + const [isDialogOpen, setIsDialogOpen] = useState(false); + const isCancellable = CANCELLABLE.includes(job.status); + + return ( + <> + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <Button + disabled={!isCancellable} + aria-label="Cancel job" + size="icon" + variant="outline" + onClick={() => { + setIsDialogOpen(true); + }} + > + <BanIcon /> + </Button> + </TooltipTrigger> + <TooltipContent>Cancel job</TooltipContent> + </Tooltip> + </TooltipProvider> + + <CancelJobConfirmationDialog + open={isDialogOpen} + job={job} + onClose={() => { + setIsDialogOpen(false); + }} + /> + </> + ); +}; diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobConfirmationDialog.stories.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobConfirmationDialog.stories.tsx new file mode 100644 index 0000000000000..8d48fe6d80d1a --- /dev/null +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobConfirmationDialog.stories.tsx @@ -0,0 +1,98 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { expect, fn, userEvent, waitFor, within } from "@storybook/test"; +import type { Response } from "api/typesGenerated"; +import { MockProvisionerJob } from "testHelpers/entities"; +import { withGlobalSnackbar } from "testHelpers/storybook"; +import { CancelJobConfirmationDialog } from "./CancelJobConfirmationDialog"; + +const meta: Meta<typeof CancelJobConfirmationDialog> = { + title: + "pages/OrganizationSettingsPage/ProvisionersPage/CancelJobConfirmationDialog", + component: CancelJobConfirmationDialog, + args: { + open: true, + onClose: fn(), + cancelProvisionerJob: fn(), + job: { + ...MockProvisionerJob, + status: "running", + }, + }, +}; + +export default meta; +type Story = StoryObj<typeof CancelJobConfirmationDialog>; + +export const Idle: Story = {}; + +export const OnCancel: Story = { + parameters: { + chromatic: { disableSnapshot: true }, + }, + play: async ({ canvasElement, args }) => { + const user = userEvent.setup(); + const body = within(canvasElement.ownerDocument.body); + const cancelButton = body.getByRole("button", { name: "Discard" }); + user.click(cancelButton); + await waitFor(() => { + expect(args.onClose).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const onConfirmSuccess: Story = { + parameters: { + chromatic: { disableSnapshot: true }, + }, + decorators: [withGlobalSnackbar], + play: async ({ canvasElement, args }) => { + const user = userEvent.setup(); + const body = within(canvasElement.ownerDocument.body); + const confirmButton = body.getByRole("button", { name: "Confirm" }); + + user.click(confirmButton); + await waitFor(() => { + body.getByText("Provisioner job canceled successfully"); + }); + expect(args.cancelProvisionerJob).toHaveBeenCalledTimes(1); + expect(args.cancelProvisionerJob).toHaveBeenCalledWith(args.job); + expect(args.onClose).toHaveBeenCalledTimes(1); + }, +}; + +export const onConfirmFailure: Story = { + parameters: { + chromatic: { disableSnapshot: true }, + }, + decorators: [withGlobalSnackbar], + args: { + cancelProvisionerJob: fn(() => { + throw new Error("API Error"); + }), + }, + play: async ({ canvasElement, args }) => { + const user = userEvent.setup(); + const body = within(canvasElement.ownerDocument.body); + const confirmButton = body.getByRole("button", { name: "Confirm" }); + + user.click(confirmButton); + await waitFor(() => { + body.getByText("Failed to cancel provisioner job"); + }); + expect(args.cancelProvisionerJob).toHaveBeenCalledTimes(1); + expect(args.cancelProvisionerJob).toHaveBeenCalledWith(args.job); + expect(args.onClose).toHaveBeenCalledTimes(0); + }, +}; + +export const Confirming: Story = { + args: { + cancelProvisionerJob: fn(() => new Promise<Response>(() => {})), + }, + play: async ({ canvasElement }) => { + const user = userEvent.setup(); + const body = within(canvasElement.ownerDocument.body); + const confirmButton = body.getByRole("button", { name: "Confirm" }); + user.click(confirmButton); + }, +}; diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobConfirmationDialog.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobConfirmationDialog.tsx new file mode 100644 index 0000000000000..573f7090a1ebb --- /dev/null +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobConfirmationDialog.tsx @@ -0,0 +1,59 @@ +import { API } from "api/api"; +import { + getProvisionerDaemonsKey, + provisionerJobQueryKey, +} from "api/queries/organizations"; +import type { ProvisionerJob } from "api/typesGenerated"; +import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; +import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"; +import type { FC } from "react"; +import { useMutation, useQueryClient } from "react-query"; + +type CancelJobConfirmationDialogProps = { + open: boolean; + onClose: () => void; + job: ProvisionerJob; + cancelProvisionerJob?: typeof API.cancelProvisionerJob; +}; + +export const CancelJobConfirmationDialog: FC< + CancelJobConfirmationDialogProps +> = ({ + job, + cancelProvisionerJob = API.cancelProvisionerJob, + ...dialogProps +}) => { + const queryClient = useQueryClient(); + const cancelMutation = useMutation({ + mutationFn: cancelProvisionerJob, + onSuccess: () => { + queryClient.invalidateQueries( + provisionerJobQueryKey(job.organization_id), + ); + queryClient.invalidateQueries( + getProvisionerDaemonsKey(job.organization_id, job.tags), + ); + }, + }); + + return ( + <ConfirmDialog + {...dialogProps} + type="delete" + title="Cancel provisioner job" + description={`Are you sure you want to cancel the provisioner job "${job.id}"? This operation will result in the associated workspaces not getting created.`} + confirmText="Confirm" + cancelText="Discard" + confirmLoading={cancelMutation.isLoading} + onConfirm={async () => { + try { + await cancelMutation.mutateAsync(job); + displaySuccess("Provisioner job canceled successfully"); + dialogProps.onClose(); + } catch { + displayError("Failed to cancel provisioner job"); + } + }} + /> + ); +}; diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/DataGrid.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/DataGrid.tsx new file mode 100644 index 0000000000000..7c9d11a238581 --- /dev/null +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/DataGrid.tsx @@ -0,0 +1,25 @@ +import type { FC, HTMLProps } from "react"; +import { cn } from "utils/cn"; + +export const DataGrid: FC<HTMLProps<HTMLDListElement>> = ({ + className, + ...props +}) => { + return ( + <dl + {...props} + className={cn([ + "m-0 grid grid-cols-[auto_1fr] gap-x-4 items-center", + "[&_dt]:text-content-primary [&_dt]:font-mono [&_dt]:leading-[22px]", + className, + ])} + /> + ); +}; + +export const DataGridSpace: FC<HTMLProps<HTMLDivElement>> = ({ + className, + ...props +}) => { + return <div {...props} className={cn(["h-6 col-span-2", className])} />; +}; diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/JobStatusIndicator.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/JobStatusIndicator.tsx new file mode 100644 index 0000000000000..0671a6b932d10 --- /dev/null +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/JobStatusIndicator.tsx @@ -0,0 +1,60 @@ +import type { + ProvisionerDaemonJob, + ProvisionerJob, + ProvisionerJobStatus, +} from "api/typesGenerated"; +import { + StatusIndicator, + StatusIndicatorDot, + type StatusIndicatorProps, +} from "components/StatusIndicator/StatusIndicator"; +import { TriangleAlertIcon } from "lucide-react"; +import type { FC } from "react"; + +const variantByStatus: Record< + ProvisionerJobStatus, + StatusIndicatorProps["variant"] +> = { + succeeded: "success", + failed: "failed", + pending: "pending", + running: "pending", + canceling: "pending", + canceled: "inactive", + unknown: "inactive", +}; + +type JobStatusIndicatorProps = { + job: ProvisionerJob; +}; + +export const JobStatusIndicator: FC<JobStatusIndicatorProps> = ({ job }) => { + return ( + <StatusIndicator size="sm" variant={variantByStatus[job.status]}> + <StatusIndicatorDot /> + <span className="[&:first-letter]:uppercase">{job.status}</span> + {job.status === "failed" && ( + <TriangleAlertIcon className="size-icon-xs p-[1px]" /> + )} + {job.status === "pending" && `(${job.queue_position}/${job.queue_size})`} + </StatusIndicator> + ); +}; + +type DaemonJobStatusIndicatorProps = { + job: ProvisionerDaemonJob; +}; + +export const DaemonJobStatusIndicator: FC<DaemonJobStatusIndicatorProps> = ({ + job, +}) => { + return ( + <StatusIndicator size="sm" variant={variantByStatus[job.status]}> + <StatusIndicatorDot /> + <span className="[&:first-letter]:uppercase">{job.status}</span> + {job.status === "failed" && ( + <TriangleAlertIcon className="size-icon-xs p-[1px]" /> + )} + </StatusIndicator> + ); +}; diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerDaemonsPage.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerDaemonsPage.tsx new file mode 100644 index 0000000000000..93d670eb9b42a --- /dev/null +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerDaemonsPage.tsx @@ -0,0 +1,274 @@ +import { provisionerDaemons } from "api/queries/organizations"; +import type { Organization, ProvisionerDaemon } from "api/typesGenerated"; +import { Avatar } from "components/Avatar/Avatar"; +import { Button } from "components/Button/Button"; +import { EmptyState } from "components/EmptyState/EmptyState"; +import { Link } from "components/Link/Link"; +import { Loader } from "components/Loader/Loader"; +import { + StatusIndicator, + StatusIndicatorDot, + type StatusIndicatorProps, +} from "components/StatusIndicator/StatusIndicator"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "components/Table/Table"; +import { ChevronDownIcon, ChevronRightIcon } from "lucide-react"; +import { type FC, useState } from "react"; +import { useQuery } from "react-query"; +import { cn } from "utils/cn"; +import { docs } from "utils/docs"; +import { relativeTime } from "utils/time"; +import { DataGrid, DataGridSpace } from "./DataGrid"; +import { DaemonJobStatusIndicator } from "./JobStatusIndicator"; +import { Tag, Tags, TruncateTags } from "./Tags"; + +type ProvisionerDaemonsPageProps = { + orgId: string; +}; + +export const ProvisionerDaemonsPage: FC<ProvisionerDaemonsPageProps> = ({ + orgId, +}) => { + const { + data: daemons, + isLoadingError, + refetch, + } = useQuery({ + ...provisionerDaemons(orgId), + select: (data) => + data.toSorted((a, b) => { + if (!a.last_seen_at && !b.last_seen_at) return 0; + if (!a.last_seen_at) return 1; + if (!b.last_seen_at) return -1; + return ( + new Date(b.last_seen_at).getTime() - + new Date(a.last_seen_at).getTime() + ); + }), + }); + + return ( + <section className="flex flex-col gap-8"> + <h2 className="sr-only">Provisioner daemons</h2> + <p className="text-sm text-content-secondary m-0 mt-2"> + Coder server runs provisioner daemons which execute terraform during + workspace and template builds.{" "} + <Link + href={docs( + "/tutorials/best-practices/security-best-practices#provisioner-daemons", + )} + > + View docs + </Link> + </p> + + <Table> + <TableHeader> + <TableRow> + <TableHead>Last seen</TableHead> + <TableHead>Name</TableHead> + <TableHead>Template</TableHead> + <TableHead>Tags</TableHead> + <TableHead>Status</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {daemons ? ( + daemons.length > 0 ? ( + daemons.map((d) => <DaemonRow key={d.id} daemon={d} />) + ) : ( + <TableRow> + <TableCell colSpan={999}> + <EmptyState message="No provisioner daemons found" /> + </TableCell> + </TableRow> + ) + ) : isLoadingError ? ( + <TableRow> + <TableCell colSpan={999}> + <EmptyState + message="Error loading the provisioner daemons" + cta={<Button onClick={() => refetch()}>Retry</Button>} + /> + </TableCell> + </TableRow> + ) : ( + <TableRow> + <TableCell colSpan={999}> + <Loader /> + </TableCell> + </TableRow> + )} + </TableBody> + </Table> + </section> + ); +}; + +type DaemonRowProps = { + daemon: ProvisionerDaemon; +}; + +const DaemonRow: FC<DaemonRowProps> = ({ daemon }) => { + const [isOpen, setIsOpen] = useState(false); + + return ( + <> + <TableRow key={daemon.id}> + <TableCell> + <button + className={cn([ + "flex items-center gap-1 p-0 bg-transparent border-0 text-inherit text-xs cursor-pointer", + "transition-colors hover:text-content-primary font-medium whitespace-nowrap", + isOpen && "text-content-primary", + ])} + type="button" + onClick={() => { + setIsOpen((v) => !v); + }} + > + {isOpen ? ( + <ChevronDownIcon className="size-icon-sm p-0.5" /> + ) : ( + <ChevronRightIcon className="size-icon-sm p-0.5" /> + )} + <span className="sr-only">({isOpen ? "Hide" : "Show more"})</span> + <span className="[&:first-letter]:uppercase"> + {relativeTime( + new Date(daemon.last_seen_at ?? new Date().toISOString()), + )} + </span> + </button> + </TableCell> + <TableCell> + <span className="block whitespace-nowrap text-ellipsis overflow-hidden"> + {daemon.name} + </span> + </TableCell> + <TableCell> + {daemon.current_job ? ( + <div className="flex items-center gap-1 whitespace-nowrap"> + <Avatar + variant="icon" + src={daemon.current_job.template_icon} + fallback={ + daemon.current_job.template_display_name || + daemon.current_job.template_name + } + /> + {daemon.current_job.template_display_name ?? + daemon.current_job.template_name} + </div> + ) : ( + <span className="whitespace-nowrap">Not linked</span> + )} + </TableCell> + <TableCell> + <TruncateTags tags={daemon.tags} /> + </TableCell> + <TableCell> + <StatusIndicator size="sm" variant={statusIndicatorVariant(daemon)}> + <StatusIndicatorDot /> + <span className="[&:first-letter]:uppercase"> + {statusLabel(daemon)} + </span> + </StatusIndicator> + </TableCell> + </TableRow> + + {isOpen && ( + <TableRow> + <TableCell colSpan={999} className="p-4 border-t-0"> + <DataGrid> + <dt>Last seen:</dt> + <dd>{daemon.last_seen_at}</dd> + + <dt>Creation time:</dt> + <dd>{daemon.created_at}</dd> + + <dt>Version:</dt> + <dd>{daemon.version}</dd> + + <dt>Tags:</dt> + <dd> + <Tags> + {Object.entries(daemon.tags).map(([key, value]) => ( + <Tag key={key} label={key} value={value} /> + ))} + </Tags> + </dd> + + {daemon.current_job && ( + <> + <DataGridSpace /> + + <dt>Last job:</dt> + <dd>{daemon.current_job.id}</dd> + + <dt>Last job state:</dt> + <dd> + <DaemonJobStatusIndicator job={daemon.current_job} /> + </dd> + </> + )} + + {daemon.previous_job && ( + <> + <DataGridSpace /> + + <dt>Previous job:</dt> + <dd>{daemon.previous_job.id}</dd> + + <dt>Previous job state:</dt> + <dd> + <DaemonJobStatusIndicator job={daemon.previous_job} /> + </dd> + </> + )} + </DataGrid> + </TableCell> + </TableRow> + )} + </> + ); +}; + +function statusIndicatorVariant( + daemon: ProvisionerDaemon, +): StatusIndicatorProps["variant"] { + if (daemon.previous_job && daemon.previous_job.status === "failed") { + return "failed"; + } + + switch (daemon.status) { + case "idle": + return "success"; + case "busy": + return "pending"; + default: + return "inactive"; + } +} + +function statusLabel(daemon: ProvisionerDaemon) { + if (daemon.previous_job && daemon.previous_job.status === "failed") { + return "Last job failed"; + } + + switch (daemon.status) { + case "idle": + return "Idle"; + case "busy": + return "Busy..."; + case "offline": + return "Disconnected"; + default: + return "Unknown"; + } +} diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx new file mode 100644 index 0000000000000..e852e90f2cf7f --- /dev/null +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx @@ -0,0 +1,215 @@ +import { provisionerJobs } from "api/queries/organizations"; +import type { Organization, ProvisionerJob } from "api/typesGenerated"; +import { Avatar } from "components/Avatar/Avatar"; +import { Badge } from "components/Badge/Badge"; +import { Button } from "components/Button/Button"; +import { EmptyState } from "components/EmptyState/EmptyState"; +import { Link } from "components/Link/Link"; +import { Loader } from "components/Loader/Loader"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "components/Table/Table"; +import { + ChevronDownIcon, + ChevronRightIcon, + TriangleAlertIcon, +} from "lucide-react"; +import { type FC, useState } from "react"; +import { useQuery } from "react-query"; +import { cn } from "utils/cn"; +import { docs } from "utils/docs"; +import { relativeTime } from "utils/time"; +import { CancelJobButton } from "./CancelJobButton"; +import { DataGrid } from "./DataGrid"; +import { JobStatusIndicator } from "./JobStatusIndicator"; +import { Tag, Tags, TruncateTags } from "./Tags"; + +type ProvisionerJobsPageProps = { + orgId: string; +}; + +export const ProvisionerJobsPage: FC<ProvisionerJobsPageProps> = ({ + orgId, +}) => { + const { + data: jobs, + isLoadingError, + refetch, + } = useQuery(provisionerJobs(orgId)); + + return ( + <section className="flex flex-col gap-8"> + <h2 className="sr-only">Provisioner jobs</h2> + <p className="text-sm text-content-secondary m-0 mt-2"> + Provisioner Jobs are the individual tasks assigned to Provisioners when + the workspaces are being built.{" "} + <Link href={docs("/admin/provisioners")}>View docs</Link> + </p> + + <Table> + <TableHeader> + <TableRow> + <TableHead>Created</TableHead> + <TableHead>Type</TableHead> + <TableHead>Template</TableHead> + <TableHead>Tags</TableHead> + <TableHead>Status</TableHead> + <TableHead /> + </TableRow> + </TableHeader> + <TableBody> + {jobs ? ( + jobs.length > 0 ? ( + jobs.map((j) => <JobRow key={j.id} job={j} />) + ) : ( + <TableRow> + <TableCell colSpan={999}> + <EmptyState message="No provisioner jobs found" /> + </TableCell> + </TableRow> + ) + ) : isLoadingError ? ( + <TableRow> + <TableCell colSpan={999}> + <EmptyState + message="Error loading the provisioner jobs" + cta={<Button onClick={() => refetch()}>Retry</Button>} + /> + </TableCell> + </TableRow> + ) : ( + <TableRow> + <TableCell colSpan={999}> + <Loader /> + </TableCell> + </TableRow> + )} + </TableBody> + </Table> + </section> + ); +}; + +type JobRowProps = { + job: ProvisionerJob; +}; + +const JobRow: FC<JobRowProps> = ({ job }) => { + const metadata = job.metadata; + const [isOpen, setIsOpen] = useState(false); + + return ( + <> + <TableRow key={job.id}> + <TableCell> + <button + className={cn([ + "flex items-center gap-1 p-0 bg-transparent border-0 text-inherit text-xs cursor-pointer", + "transition-colors hover:text-content-primary font-medium whitespace-nowrap", + isOpen && "text-content-primary", + ])} + type="button" + onClick={() => { + setIsOpen((v) => !v); + }} + > + {isOpen ? ( + <ChevronDownIcon className="size-icon-sm p-0.5" /> + ) : ( + <ChevronRightIcon className="size-icon-sm p-0.5" /> + )} + <span className="sr-only">({isOpen ? "Hide" : "Show more"})</span> + <span className="[&:first-letter]:uppercase"> + {relativeTime(new Date(job.created_at))} + </span> + </button> + </TableCell> + <TableCell> + <Badge size="sm">{job.type}</Badge> + </TableCell> + <TableCell> + {job.metadata.template_name ? ( + <div className="flex items-center gap-1 whitespace-nowrap"> + <Avatar + variant="icon" + src={metadata.template_icon} + fallback={ + metadata.template_display_name || metadata.template_name + } + /> + {metadata.template_display_name ?? metadata.template_name} + </div> + ) : ( + <span className="whitespace-nowrap">Not linked</span> + )} + </TableCell> + <TableCell> + <TruncateTags tags={job.tags} /> + </TableCell> + <TableCell> + <JobStatusIndicator job={job} /> + </TableCell> + <TableCell className="text-right"> + <CancelJobButton job={job} /> + </TableCell> + </TableRow> + + {isOpen && ( + <TableRow> + <TableCell colSpan={999} className="p-4 border-t-0"> + {job.status === "failed" && ( + <div + className={cn([ + "inline-flex items-center gap-2 rounded border border-solid border-boder p-2", + "text-content-primary bg-surface-secondary mb-4", + ])} + > + <TriangleAlertIcon className="text-content-destructive size-icon-sm p-0.5" /> + <span className="[&:first-letter]:uppercase">{job.error}</span> + </div> + )} + <DataGrid> + <dt>Job ID:</dt> + <dd>{job.id}</dd> + + <dt>Available provisioners:</dt> + <dd> + {job.available_workers + ? JSON.stringify(job.available_workers) + : "[]"} + </dd> + + <dt>Completed by provisioner:</dt> + <dd>{job.worker_id}</dd> + + <dt>Associated workspace:</dt> + <dd>{job.metadata.workspace_name ?? "null"}</dd> + + <dt>Creation time:</dt> + <dd>{job.created_at}</dd> + + <dt>Queue:</dt> + <dd> + {job.queue_position}/{job.queue_size} + </dd> + + <dt>Tags:</dt> + <dd> + <Tags> + {Object.entries(job.tags).map(([key, value]) => ( + <Tag key={key} label={key} value={value} /> + ))} + </Tags> + </dd> + </DataGrid> + </TableCell> + </TableRow> + )} + </> + ); +}; diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionersPage.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionersPage.tsx new file mode 100644 index 0000000000000..051f916c3ad99 --- /dev/null +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionersPage.tsx @@ -0,0 +1,73 @@ +import { EmptyState } from "components/EmptyState/EmptyState"; +import { TabLink, Tabs, TabsList } from "components/Tabs/Tabs"; +import { useSearchParamsKey } from "hooks/useSearchParamsKey"; +import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; +import type { FC } from "react"; +import { Helmet } from "react-helmet-async"; +import { pageTitle } from "utils/page"; +import { ProvisionerDaemonsPage } from "./ProvisionerDaemonsPage"; +import { ProvisionerJobsPage } from "./ProvisionerJobsPage"; + +const ProvisionersPage: FC = () => { + const { organization, organizationPermissions } = useOrganizationSettings(); + const tab = useSearchParamsKey({ + key: "tab", + defaultValue: "jobs", + }); + + if (!organization || !organizationPermissions?.viewProvisionerJobs) { + return ( + <> + <Helmet> + <title>{pageTitle("Provisioners")} + + + + ); + } + + return ( + <> + + + {pageTitle( + "Provisioners", + organization.display_name || organization.name, + )} + + + +
+
+
+

Provisioners

+
+
+ +
+ + + + Jobs + + + Daemons + + + + +
+ {tab.value === "jobs" && ( + + )} + {tab.value === "daemons" && ( + + )} +
+
+
+ + ); +}; + +export default ProvisionersPage; diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/Tags.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/Tags.tsx new file mode 100644 index 0000000000000..449aa25593f1c --- /dev/null +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/Tags.tsx @@ -0,0 +1,52 @@ +import { Badge } from "components/Badge/Badge"; +import type { FC, HTMLProps } from "react"; +import { cn } from "utils/cn"; + +export const Tags: FC> = ({ + className, + ...props +}) => { + return ( +
+ ); +}; + +type TagProps = { + label: string; + value?: string; +}; + +export const Tag: FC = ({ label, value }) => { + return ( + + [{label} + {value && `=${value}`}] + + ); +}; + +type TagsProps = { + tags: Record; +}; + +export const TruncateTags: FC = ({ tags }) => { + const keys = Object.keys(tags); + + if (keys.length === 0) { + return null; + } + + const firstKey = keys[0]; + const firstValue = tags[firstKey]; + const remainderCount = keys.length - 1; + + return ( + + + {remainderCount > 0 && +{remainderCount}} + + ); +}; diff --git a/site/src/pages/ManagementSettingsPage/UserTable/EditRolesButton.stories.tsx b/site/src/pages/OrganizationSettingsPage/UserTable/EditRolesButton.stories.tsx similarity index 78% rename from site/src/pages/ManagementSettingsPage/UserTable/EditRolesButton.stories.tsx rename to site/src/pages/OrganizationSettingsPage/UserTable/EditRolesButton.stories.tsx index 0511a9d877ea1..f3244898483ce 100644 --- a/site/src/pages/ManagementSettingsPage/UserTable/EditRolesButton.stories.tsx +++ b/site/src/pages/OrganizationSettingsPage/UserTable/EditRolesButton.stories.tsx @@ -4,6 +4,7 @@ import { MockOwnerRole, MockSiteRoles, MockUserAdminRole, + MockWorkspaceCreationBanRole, } from "testHelpers/entities"; import { withDesktopViewport } from "testHelpers/storybook"; import { EditRolesButton } from "./EditRolesButton"; @@ -41,3 +42,14 @@ export const Loading: Story = { await userEvent.click(canvas.getByRole("button")); }, }; + +export const AdvancedOpen: Story = { + args: { + selectedRoleNames: new Set([MockWorkspaceCreationBanRole.name]), + roles: MockSiteRoles, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click(canvas.getByRole("button")); + }, +}; diff --git a/site/src/pages/ManagementSettingsPage/UserTable/EditRolesButton.tsx b/site/src/pages/OrganizationSettingsPage/UserTable/EditRolesButton.tsx similarity index 57% rename from site/src/pages/ManagementSettingsPage/UserTable/EditRolesButton.tsx rename to site/src/pages/OrganizationSettingsPage/UserTable/EditRolesButton.tsx index d7d3c100acd73..9efd99bccf106 100644 --- a/site/src/pages/ManagementSettingsPage/UserTable/EditRolesButton.tsx +++ b/site/src/pages/OrganizationSettingsPage/UserTable/EditRolesButton.tsx @@ -1,9 +1,9 @@ -import type { Interpolation, Theme } from "@emotion/react"; import UserIcon from "@mui/icons-material/PersonOutline"; import Checkbox from "@mui/material/Checkbox"; -import IconButton from "@mui/material/IconButton"; import Tooltip from "@mui/material/Tooltip"; import type { SlimRole } from "api/typesGenerated"; +import { Button } from "components/Button/Button"; +import { CollapsibleSummary } from "components/CollapsibleSummary/CollapsibleSummary"; import { HelpTooltip, HelpTooltipContent, @@ -12,14 +12,14 @@ import { HelpTooltipTrigger, } from "components/HelpTooltip/HelpTooltip"; import { EditSquare } from "components/Icons/EditSquare"; -import { Stack } from "components/Stack/Stack"; import { Popover, PopoverContent, PopoverTrigger, } from "components/deprecated/Popover/Popover"; -import { type ClassName, useClassName } from "hooks/useClassName"; -import type { FC } from "react"; +import { ChevronDownIcon, ChevronRightIcon } from "lucide-react"; +import { type FC, useEffect, useState } from "react"; +import { cn } from "utils/cn"; const roleDescriptions: Record = { owner: @@ -47,23 +47,23 @@ const Option: FC = ({ onChange, }) => { return ( -