From 7af188bfc102d97df98db011aeaace283b8e4949 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Mon, 12 May 2025 14:03:40 +0300 Subject: [PATCH 01/88] fix(agent): fix unexpanded devcontainer paths for agentcontainers (#17736) Devcontainers were duplicated in the API because paths weren't absolute, we now normalize them early on to keep it simple. Updates #16424 --- agent/agent.go | 4 +++- agent/agent_test.go | 17 +++++++++++------ agent/agentcontainers/devcontainer.go | 14 +++++++++++--- agent/agentcontainers/devcontainer_test.go | 4 +--- 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/agent/agent.go b/agent/agent.go index 7525ecf051f69..d0e668af34d74 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -1085,6 +1085,8 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context, if err != nil { return xerrors.Errorf("expand directory: %w", err) } + // Normalize all devcontainer paths by making them absolute. + manifest.Devcontainers = agentcontainers.ExpandAllDevcontainerPaths(a.logger, expandPathToAbs, manifest.Devcontainers) subsys, err := agentsdk.ProtoFromSubsystems(a.subsystems) if err != nil { a.logger.Critical(ctx, "failed to convert subsystems", slog.Error(err)) @@ -1127,7 +1129,7 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context, ) if a.experimentalDevcontainersEnabled { var dcScripts []codersdk.WorkspaceAgentScript - scripts, dcScripts = agentcontainers.ExtractAndInitializeDevcontainerScripts(a.logger, expandPathToAbs, manifest.Devcontainers, scripts) + scripts, dcScripts = agentcontainers.ExtractAndInitializeDevcontainerScripts(manifest.Devcontainers, scripts) // See ExtractAndInitializeDevcontainerScripts for motivation // behind running dcScripts as post start scripts. scriptRunnerOpts = append(scriptRunnerOpts, agentscripts.WithPostStartScripts(dcScripts...)) diff --git a/agent/agent_test.go b/agent/agent_test.go index 67fa203252ba7..fe2c99059e9d8 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -1998,8 +1998,9 @@ func TestAgent_ReconnectingPTYContainer(t *testing.T) { // You can run it manually as follows: // // CODER_TEST_USE_DOCKER=1 go test -count=1 ./agent -run TestAgent_DevcontainerAutostart +// +//nolint:paralleltest // This test sets an environment variable. func TestAgent_DevcontainerAutostart(t *testing.T) { - t.Parallel() if os.Getenv("CODER_TEST_USE_DOCKER") != "1" { t.Skip("Set CODER_TEST_USE_DOCKER=1 to run this test") } @@ -2012,9 +2013,12 @@ func TestAgent_DevcontainerAutostart(t *testing.T) { // Prepare temporary devcontainer for test (mywork). devcontainerID := uuid.New() - tempWorkspaceFolder := t.TempDir() - tempWorkspaceFolder = filepath.Join(tempWorkspaceFolder, "mywork") + tmpdir := t.TempDir() + t.Setenv("HOME", tmpdir) + tempWorkspaceFolder := filepath.Join(tmpdir, "mywork") + unexpandedWorkspaceFolder := filepath.Join("~", "mywork") t.Logf("Workspace folder: %s", tempWorkspaceFolder) + t.Logf("Unexpanded workspace folder: %s", unexpandedWorkspaceFolder) devcontainerPath := filepath.Join(tempWorkspaceFolder, ".devcontainer") err = os.MkdirAll(devcontainerPath, 0o755) require.NoError(t, err, "create devcontainer directory") @@ -2031,9 +2035,10 @@ func TestAgent_DevcontainerAutostart(t *testing.T) { // is expected to be prepared by the provisioner normally. Devcontainers: []codersdk.WorkspaceAgentDevcontainer{ { - ID: devcontainerID, - Name: "test", - WorkspaceFolder: tempWorkspaceFolder, + ID: devcontainerID, + Name: "test", + // Use an unexpanded path to test the expansion. + WorkspaceFolder: unexpandedWorkspaceFolder, }, }, Scripts: []codersdk.WorkspaceAgentScript{ diff --git a/agent/agentcontainers/devcontainer.go b/agent/agentcontainers/devcontainer.go index cbf42e150d240..e04c308934a2c 100644 --- a/agent/agentcontainers/devcontainer.go +++ b/agent/agentcontainers/devcontainer.go @@ -36,8 +36,6 @@ devcontainer up %s // initialize the workspace (e.g. git clone, npm install, etc). This is // important if e.g. a Coder module to install @devcontainer/cli is used. func ExtractAndInitializeDevcontainerScripts( - logger slog.Logger, - expandPath func(string) (string, error), devcontainers []codersdk.WorkspaceAgentDevcontainer, scripts []codersdk.WorkspaceAgentScript, ) (filteredScripts []codersdk.WorkspaceAgentScript, devcontainerScripts []codersdk.WorkspaceAgentScript) { @@ -47,7 +45,6 @@ ScriptLoop: // The devcontainer scripts match the devcontainer ID for // identification. if script.ID == dc.ID { - dc = expandDevcontainerPaths(logger, expandPath, dc) devcontainerScripts = append(devcontainerScripts, devcontainerStartupScript(dc, script)) continue ScriptLoop } @@ -75,6 +72,17 @@ func devcontainerStartupScript(dc codersdk.WorkspaceAgentDevcontainer, script co return script } +// ExpandAllDevcontainerPaths expands all devcontainer paths in the given +// devcontainers. This is required by the devcontainer CLI, which requires +// absolute paths for the workspace folder and config path. +func ExpandAllDevcontainerPaths(logger slog.Logger, expandPath func(string) (string, error), devcontainers []codersdk.WorkspaceAgentDevcontainer) []codersdk.WorkspaceAgentDevcontainer { + expanded := make([]codersdk.WorkspaceAgentDevcontainer, 0, len(devcontainers)) + for _, dc := range devcontainers { + expanded = append(expanded, expandDevcontainerPaths(logger, expandPath, dc)) + } + return expanded +} + func expandDevcontainerPaths(logger slog.Logger, expandPath func(string) (string, error), dc codersdk.WorkspaceAgentDevcontainer) codersdk.WorkspaceAgentDevcontainer { logger = logger.With(slog.F("devcontainer", dc.Name), slog.F("workspace_folder", dc.WorkspaceFolder), slog.F("config_path", dc.ConfigPath)) diff --git a/agent/agentcontainers/devcontainer_test.go b/agent/agentcontainers/devcontainer_test.go index 5e0f5d8dae7bc..b20c943175821 100644 --- a/agent/agentcontainers/devcontainer_test.go +++ b/agent/agentcontainers/devcontainer_test.go @@ -242,9 +242,7 @@ func TestExtractAndInitializeDevcontainerScripts(t *testing.T) { } } gotFilteredScripts, gotDevcontainerScripts := agentcontainers.ExtractAndInitializeDevcontainerScripts( - logger, - tt.args.expandPath, - tt.args.devcontainers, + agentcontainers.ExpandAllDevcontainerPaths(logger, tt.args.expandPath, tt.args.devcontainers), tt.args.scripts, ) From 87152db05b68185bf9aee2ba2e333042463cf3ce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 11:58:20 +0000 Subject: [PATCH 02/88] ci: bump the github-actions group across 1 directory with 4 updates (#17760) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the github-actions group with 4 updates in the / directory: [crate-ci/typos](https://github.com/crate-ci/typos), [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata), [tj-actions/changed-files](https://github.com/tj-actions/changed-files) and [github/codeql-action](https://github.com/github/codeql-action). Updates `crate-ci/typos` from 1.31.1 to 1.32.0
Release notes

Sourced from crate-ci/typos's releases.

v1.32.0

[1.32.0] - 2025-05-02

Features

  • Updated the dictionary with the April 2025 changes

v1.31.2

[1.31.2] - 2025-04-28

Fixes

  • (exclusion) Don't confused emails as base64
  • (dict) Correct contamint to contaminant, not contaminat
  • (dict) Correct contamints to contaminants, not contaminats

Performance

  • Improve tokenization performance
Changelog

Sourced from crate-ci/typos's changelog.

Change Log

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog and this project adheres to Semantic Versioning.

[Unreleased] - ReleaseDate

[1.32.0] - 2025-05-02

Features

  • Updated the dictionary with the April 2025 changes

[1.31.2] - 2025-04-28

Fixes

  • (exclusion) Don't confused emails as base64
  • (dict) Correct contamint to contaminant, not contaminat
  • (dict) Correct contamints to contaminants, not contaminats

Performance

  • Improve tokenization performance

[1.31.1] - 2025-03-31

Fixes

  • (dict) Also correct typ to type

[1.31.0] - 2025-03-28

Features

  • Updated the dictionary with the March 2025 changes

[1.30.3] - 2025-03-24

Features

  • Support detecting go.work and go.work.sum files

[1.30.2] - 2025-03-10

Features

  • Add --highlight-words and --highlight-identifiers for easier debugging of config

... (truncated)

Commits

Updates `dependabot/fetch-metadata` from 2.3.0 to 2.4.0
Release notes

Sourced from dependabot/fetch-metadata's releases.

v2.4.0

What's Changed

Full Changelog: https://github.com/dependabot/fetch-metadata/compare/v2...v2.4.0

Commits

Updates `tj-actions/changed-files` from 5426ecc3f5c2b10effaefbd374f0abdc6a571b2f to 480f49412651059a414a6a5c96887abb1877de8a
Changelog

Sourced from tj-actions/changed-files's changelog.

Changelog

46.0.5 - (2025-04-09)

⚙️ Miscellaneous Tasks

  • deps: Bump yaml from 2.7.0 to 2.7.1 (#2520) (ed68ef8) - (dependabot[bot])
  • deps-dev: Bump typescript from 5.8.2 to 5.8.3 (#2516) (a7bc14b) - (dependabot[bot])
  • deps-dev: Bump @​types/node from 22.13.11 to 22.14.0 (#2517) (3d751f6) - (dependabot[bot])
  • deps-dev: Bump eslint-plugin-prettier from 5.2.3 to 5.2.6 (#2519) (e2fda4e) - (dependabot[bot])
  • deps-dev: Bump ts-jest from 29.2.6 to 29.3.1 (#2518) (0bed1b1) - (dependabot[bot])
  • deps: Bump github/codeql-action from 3.28.12 to 3.28.15 (#2530) (6802458) - (dependabot[bot])
  • deps: Bump tj-actions/branch-names from 8.0.1 to 8.1.0 (#2521) (cf2e39e) - (dependabot[bot])
  • deps: Bump tj-actions/verify-changed-files from 20.0.1 to 20.0.4 (#2523) (6abeaa5) - (dependabot[bot])

⬆️ Upgrades

  • Upgraded to v46.0.4 (#2511)

Co-authored-by: github-actions[bot] (6f67ee9) - (github-actions[bot])

46.0.4 - (2025-04-03)

🐛 Bug Fixes

  • Bug modified_keys and changed_key outputs not set when no changes detected (#2509) (6cb76d0) - (Tonye Jack)

📚 Documentation

⬆️ Upgrades

  • Upgraded to v46.0.3 (#2506)

Co-authored-by: github-actions[bot] Co-authored-by: Tonye Jack jtonye@ymail.com (27ae6b3) - (github-actions[bot])

46.0.3 - (2025-03-23)

🔄 Update

  • Updated README.md (#2501)

Co-authored-by: github-actions[bot] (41e0de5) - (github-actions[bot])

  • Updated README.md (#2499)

Co-authored-by: github-actions[bot] (9457878) - (github-actions[bot])

📚 Documentation

... (truncated)

Commits

Updates `github/codeql-action` from 3.28.16 to 3.28.17
Release notes

Sourced from github/codeql-action's releases.

v3.28.17

CodeQL Action Changelog

See the releases page for the relevant changes to the CodeQL CLI and language packs.

3.28.17 - 02 May 2025

  • Update default CodeQL bundle version to 2.21.2. #2872

See the full CHANGELOG.md for more information.

Changelog

Sourced from github/codeql-action's changelog.

CodeQL Action Changelog

See the releases page for the relevant changes to the CodeQL CLI and language packs.

[UNRELEASED]

No user facing changes.

3.28.17 - 02 May 2025

  • Update default CodeQL bundle version to 2.21.2. #2872

3.28.16 - 23 Apr 2025

  • Update default CodeQL bundle version to 2.21.1. #2863

3.28.15 - 07 Apr 2025

  • Fix bug where the action would fail if it tried to produce a debug artifact with more than 65535 files. #2842

3.28.14 - 07 Apr 2025

  • Update default CodeQL bundle version to 2.21.0. #2838

3.28.13 - 24 Mar 2025

No user facing changes.

3.28.12 - 19 Mar 2025

  • Dependency caching should now cache more dependencies for Java build-mode: none extractions. This should speed up workflows and avoid inconsistent alerts in some cases.
  • Update default CodeQL bundle version to 2.20.7. #2810

3.28.11 - 07 Mar 2025

  • Update default CodeQL bundle version to 2.20.6. #2793

3.28.10 - 21 Feb 2025

  • Update default CodeQL bundle version to 2.20.5. #2772
  • Address an issue where the CodeQL Bundle would occasionally fail to decompress on macOS. #2768

3.28.9 - 07 Feb 2025

  • Update default CodeQL bundle version to 2.20.4. #2753

3.28.8 - 29 Jan 2025

  • Enable support for Kotlin 2.1.10 when running with CodeQL CLI v2.20.3. #2744

... (truncated)

Commits

Most Recent Ignore Conditions Applied to This Pull Request | Dependency Name | Ignore Conditions | | --- | --- | | crate-ci/typos | [>= 1.30.a, < 1.31] |
Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- .github/workflows/dependabot.yaml | 2 +- .github/workflows/docs-ci.yaml | 2 +- .github/workflows/scorecard.yml | 2 +- .github/workflows/security.yaml | 6 +++--- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e46aa7eb7383a..fea76c03c1a4f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -188,7 +188,7 @@ jobs: # Check for any typos - name: Check for typos - uses: crate-ci/typos@b1a1ef3893ff35ade0cfa71523852a49bfd05d19 # v1.31.1 + uses: crate-ci/typos@0f0ccba9ed1df83948f0c15026e4f5ccfce46109 # v1.32.0 with: config: .github/workflows/typos.toml diff --git a/.github/workflows/dependabot.yaml b/.github/workflows/dependabot.yaml index 16401475b48fc..f86601096ae96 100644 --- a/.github/workflows/dependabot.yaml +++ b/.github/workflows/dependabot.yaml @@ -23,7 +23,7 @@ jobs: steps: - name: Dependabot metadata id: metadata - uses: dependabot/fetch-metadata@d7267f607e9d3fb96fc2fbe83e0af444713e90b7 # v2.3.0 + uses: dependabot/fetch-metadata@08eff52bf64351f401fb50d4972fa95b9f2c2d1b # v2.4.0 with: github-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/docs-ci.yaml b/.github/workflows/docs-ci.yaml index 07fcdc61ab9e5..587977c1d2a04 100644 --- a/.github/workflows/docs-ci.yaml +++ b/.github/workflows/docs-ci.yaml @@ -28,7 +28,7 @@ jobs: - name: Setup Node uses: ./.github/actions/setup-node - - uses: tj-actions/changed-files@5426ecc3f5c2b10effaefbd374f0abdc6a571b2f # v45.0.7 + - uses: tj-actions/changed-files@480f49412651059a414a6a5c96887abb1877de8a # v45.0.7 id: changed-files with: files: | diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 38e2413f76fc9..5b68e4b26c20d 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -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@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 + uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 with: sarif_file: results.sarif diff --git a/.github/workflows/security.yaml b/.github/workflows/security.yaml index d9f178ec85e9f..f9f461cfe9966 100644 --- a/.github/workflows/security.yaml +++ b/.github/workflows/security.yaml @@ -38,7 +38,7 @@ jobs: uses: ./.github/actions/setup-go - name: Initialize CodeQL - uses: github/codeql-action/init@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 + uses: github/codeql-action/init@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 with: languages: go, javascript @@ -48,7 +48,7 @@ jobs: rm Makefile - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 + uses: github/codeql-action/analyze@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 - name: Send Slack notification on failure if: ${{ failure() }} @@ -150,7 +150,7 @@ jobs: severity: "CRITICAL,HIGH" - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 + uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 with: sarif_file: trivy-results.sarif category: "Trivy" From 4f1df34981fcc603ea04702b57ba5761fbfb8689 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 12:09:34 +0000 Subject: [PATCH 03/88] chore: bump github.com/mark3labs/mcp-go from 0.25.0 to 0.27.0 (#17762) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [github.com/mark3labs/mcp-go](https://github.com/mark3labs/mcp-go) from 0.25.0 to 0.27.0.
Release notes

Sourced from github.com/mark3labs/mcp-go's releases.

Release v0.27.0

What's Changed

New Contributors

Full Changelog: https://github.com/mark3labs/mcp-go/compare/v0.26.0...v0.27.0

Release v0.26.0

What's Changed

New Contributors

Full Changelog: https://github.com/mark3labs/mcp-go/compare/v0.25.0...v0.26.0

Commits
  • e5121b3 Release v0.27.0
  • eeb7070 fix: properly marshal ToolAnnotations with false values (#260)
  • e1f1b47 optimize listByPagination (#246)
  • 46bfb6f fix: fix some obvious simplifications (#267)
  • 716eabe docs: Remove reference to mcp.RoleSystem (#269)
  • 3dfa331 fix(server/stdio): risk of concurrent reads and data loss in readNextLine() (...
  • f8badd6 chore: replace interface{} with any (#261)
  • 3442d32 feat(MCPServer): avoid unnecessary notifications when Resource/Tool not exist...
  • 61b9784 docs: make code examples in the README correct as per spec (#268)
  • 1c99eaf test(server): reliably detect Start/Shutdown deadlock in SSEServer (#264)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/mark3labs/mcp-go&package-manager=go_modules&previous-version=0.25.0&new-version=0.27.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index cf42a07dab9bf..05f41a8ae3c5a 100644 --- a/go.mod +++ b/go.mod @@ -491,7 +491,7 @@ require ( github.com/coder/preview v0.0.2-0.20250509141204-fc9484dbe506 github.com/fsnotify/fsnotify v1.9.0 github.com/kylecarbs/aisdk-go v0.0.8 - github.com/mark3labs/mcp-go v0.25.0 + github.com/mark3labs/mcp-go v0.27.0 github.com/openai/openai-go v0.1.0-beta.10 google.golang.org/genai v0.7.0 ) diff --git a/go.sum b/go.sum index cffe83a883125..a461ce153a772 100644 --- a/go.sum +++ b/go.sum @@ -1501,8 +1501,8 @@ github.com/makeworld-the-better-one/dither/v2 v2.4.0 h1:Az/dYXiTcwcRSe59Hzw4RI1r github.com/makeworld-the-better-one/dither/v2 v2.4.0/go.mod h1:VBtN8DXO7SNtyGmLiGA7IsFeKrBkQPze1/iAeM95arc= github.com/marekm4/color-extractor v1.2.1 h1:3Zb2tQsn6bITZ8MBVhc33Qn1k5/SEuZ18mrXGUqIwn0= github.com/marekm4/color-extractor v1.2.1/go.mod h1:90VjmiHI6M8ez9eYUaXLdcKnS+BAOp7w+NpwBdkJmpA= -github.com/mark3labs/mcp-go v0.25.0 h1:UUpcMT3L5hIhuDy7aifj4Bphw4Pfx1Rf8mzMXDe8RQw= -github.com/mark3labs/mcp-go v0.25.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4= +github.com/mark3labs/mcp-go v0.27.0 h1:iok9kU4DUIU2/XVLgFS2Q9biIDqstC0jY4EQTK2Erzc= +github.com/mark3labs/mcp-go v0.27.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= From 0832afbaf4bedb58786c3daa7db9af78f36aa359 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 12:11:02 +0000 Subject: [PATCH 04/88] chore: bump gopkg.in/DataDog/dd-trace-go.v1 from 1.72.1 to 1.73.0 (#17763) Bumps gopkg.in/DataDog/dd-trace-go.v1 from 1.72.1 to 1.73.0.
Most Recent Ignore Conditions Applied to This Pull Request | Dependency Name | Ignore Conditions | | --- | --- | | gopkg.in/DataDog/dd-trace-go.v1 | [>= 1.58.a, < 1.59] |
[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=gopkg.in/DataDog/dd-trace-go.v1&package-manager=go_modules&previous-version=1.72.1&new-version=1.73.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 50 ++++++++++---------- go.sum | 141 ++++++++++++++++++++++++++++++++------------------------- 2 files changed, 105 insertions(+), 86 deletions(-) diff --git a/go.mod b/go.mod index 05f41a8ae3c5a..25d6f70ec2f16 100644 --- a/go.mod +++ b/go.mod @@ -196,7 +196,7 @@ require ( go.uber.org/mock v0.5.0 go4.org/netipx v0.0.0-20230728180743-ad4cb58a6516 golang.org/x/crypto v0.37.0 - golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 + golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac golang.org/x/mod v0.24.0 golang.org/x/net v0.39.0 golang.org/x/oauth2 v0.29.0 @@ -209,7 +209,7 @@ require ( google.golang.org/api v0.231.0 google.golang.org/grpc v1.72.0 google.golang.org/protobuf v1.36.6 - gopkg.in/DataDog/dd-trace-go.v1 v1.72.1 + gopkg.in/DataDog/dd-trace-go.v1 v1.73.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v3 v3.0.1 gvisor.dev/gvisor v0.0.0-20240509041132-65b30f7869dc @@ -227,20 +227,20 @@ require ( filippo.io/edwards25519 v1.1.0 // indirect 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/datadog-agent/pkg/obfuscate v0.64.0-rc.1 // indirect + github.com/DataDog/datadog-agent/pkg/proto v0.64.0-rc.1 // indirect + github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.64.0-rc.1 // indirect + github.com/DataDog/datadog-agent/pkg/trace v0.64.0-rc.1 // indirect + github.com/DataDog/datadog-agent/pkg/util/log v0.64.0-rc.1 // indirect + github.com/DataDog/datadog-agent/pkg/util/scrubber v0.64.0-rc.1 // indirect + github.com/DataDog/datadog-go/v5 v5.6.0 // indirect + github.com/DataDog/go-libddwaf/v3 v3.5.3 // 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-sqllexer v0.1.0 // 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/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes v0.26.0 // indirect + github.com/DataDog/sketches-go v1.4.7 // 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 @@ -323,7 +323,7 @@ require ( github.com/google/btree v1.1.2 // 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-20240227163752-401108e1b7e7 // indirect + github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect @@ -392,7 +392,7 @@ require ( github.com/opencontainers/runc v1.2.3 // indirect github.com/outcaste-io/ristretto v0.2.3 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986 // indirect + github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect github.com/pierrec/lz4/v4 v4.1.18 // indirect github.com/pion/transport/v2 v2.2.10 // indirect github.com/pion/transport/v3 v3.0.7 // indirect @@ -407,8 +407,6 @@ 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.9.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 @@ -426,9 +424,9 @@ require ( 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.13 // indirect - github.com/tklauser/numcpus v0.7.0 // indirect + github.com/tinylib/msgp v1.2.5 // indirect + github.com/tklauser/go-sysconf v0.3.14 // indirect + github.com/tklauser/numcpus v0.8.0 // 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 @@ -447,11 +445,10 @@ require ( github.com/zclconf/go-cty v1.16.2 github.com/zeebo/errs v1.4.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/collector/component v0.120.0 // indirect + go.opentelemetry.io/collector/pdata v1.26.0 // indirect + go.opentelemetry.io/collector/pdata/pprofile v0.120.0 // indirect + go.opentelemetry.io/collector/semconv v0.120.0 // indirect go.opentelemetry.io/contrib v1.19.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect go.opentelemetry.io/otel/metric v1.35.0 // indirect @@ -502,6 +499,8 @@ require ( cloud.google.com/go/iam v1.4.0 // indirect cloud.google.com/go/monitoring v1.24.0 // indirect cloud.google.com/go/storage v1.50.0 // indirect + github.com/DataDog/datadog-agent/comp/core/tagger/origindetection v0.64.0-rc.1 // indirect + github.com/DataDog/datadog-agent/pkg/version v0.64.0-rc.1 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0 // indirect @@ -519,6 +518,7 @@ require ( github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/moby/sys/user v0.3.0 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect github.com/samber/lo v1.49.1 // indirect github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect github.com/tidwall/sjson v1.2.5 // indirect diff --git a/go.sum b/go.sum index a461ce153a772..a42428c7c8e7e 100644 --- a/go.sum +++ b/go.sum @@ -632,34 +632,38 @@ github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7Oputl github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/DataDog/appsec-internal-go v1.9.0 h1:cGOneFsg0JTRzWl5U2+og5dbtyW3N8XaYwc5nXe39Vw= github.com/DataDog/appsec-internal-go v1.9.0/go.mod h1:wW0cRfWBo4C044jHGwYiyh5moQV2x0AhnwqMuiX7O/g= -github.com/DataDog/datadog-agent/pkg/obfuscate v0.58.0 h1:nOrRNCHyriM/EjptMrttFOQhRSmvfagESdpyknb5VPg= -github.com/DataDog/datadog-agent/pkg/obfuscate v0.58.0/go.mod h1:MfDvphBMmEMwE3a30h27AtPO7OzmvdoVTiGY1alEmo4= -github.com/DataDog/datadog-agent/pkg/proto v0.58.0 h1:JX2Q0C5QnKcYqnYHWUcP0z7R0WB8iiQz3aWn+kT5DEc= -github.com/DataDog/datadog-agent/pkg/proto v0.58.0/go.mod h1:0wLYojGxRZZFQ+SBbFjay9Igg0zbP88l03TfZaVZ6Dc= -github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.58.0 h1:5hGO0Z8ih0bRojuq+1ZwLFtdgsfO3TqIjbwJAH12sOQ= -github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.58.0/go.mod h1:jN5BsZI+VilHJV1Wac/efGxS4TPtXa1Lh9SiUyv93F4= -github.com/DataDog/datadog-agent/pkg/trace v0.58.0 h1:4AjohoBWWN0nNaeD/0SDZ8lRTYmnJ48CqREevUfSets= -github.com/DataDog/datadog-agent/pkg/trace v0.58.0/go.mod h1:MFnhDW22V5M78MxR7nv7abWaGc/B4L42uHH1KcIKxZs= -github.com/DataDog/datadog-agent/pkg/util/log v0.58.0 h1:2MENBnHNw2Vx/ebKRyOPMqvzWOUps2Ol2o/j8uMvN4U= -github.com/DataDog/datadog-agent/pkg/util/log v0.58.0/go.mod h1:1KdlfcwhqtYHS1szAunsgSfvgoiVsf3mAJc+WvNTnIE= -github.com/DataDog/datadog-agent/pkg/util/scrubber v0.58.0 h1:Jkf91q3tuIer4Hv9CLJIYjlmcelAsoJRMmkHyz+p1Dc= -github.com/DataDog/datadog-agent/pkg/util/scrubber v0.58.0/go.mod h1:krOxbYZc4KKE7bdEDu10lLSQBjdeSFS/XDSclsaSf1Y= -github.com/DataDog/datadog-go/v5 v5.5.0 h1:G5KHeB8pWBNXT4Jtw0zAkhdxEAWSpWH00geHI6LDrKU= -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/datadog-agent/comp/core/tagger/origindetection v0.64.0-rc.1 h1:XHITEDEb6NVc9n+myS8KJhdK0vKOvY0BTWSFrFynm4s= +github.com/DataDog/datadog-agent/comp/core/tagger/origindetection v0.64.0-rc.1/go.mod h1:lzCtnMSGZm/3RMk5RBRW/6IuK1TNbDXx1ttHTxN5Ykc= +github.com/DataDog/datadog-agent/pkg/obfuscate v0.64.0-rc.1 h1:63L66uiNazsZs1DCmb5aDv/YAkCqn6xKqc0aYeATkQ8= +github.com/DataDog/datadog-agent/pkg/obfuscate v0.64.0-rc.1/go.mod h1:3BS4G7V1y7jhSgrbqPx2lGxBb/YomYwUP0wjwr+cBHc= +github.com/DataDog/datadog-agent/pkg/proto v0.64.0-rc.1 h1:8+4sv0i+na4QMjggZrQNFspbVHu7iaZU6VWeupPMdbA= +github.com/DataDog/datadog-agent/pkg/proto v0.64.0-rc.1/go.mod h1:q324yHcBN5hIeCU8eoinM7lP9c7MOA2FTj7oeWAl3Pc= +github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.64.0-rc.1 h1:MpUmwDTz+UQN/Pyng5GwvomH7LYjdcFhVVNMnxT4Rvc= +github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.64.0-rc.1/go.mod h1:QHiOw0sFriX2whwein+Puv69CqJcbOQnocUBo2IahNk= +github.com/DataDog/datadog-agent/pkg/trace v0.64.0-rc.1 h1:5PbiZw511B+qESc7PxxWY5ubiBtVnLFqC+UZKZAB3xo= +github.com/DataDog/datadog-agent/pkg/trace v0.64.0-rc.1/go.mod h1:AkapH6q9UZLoRQuhlOPiibRFqZtaKPMwtzZwYjjzgK0= +github.com/DataDog/datadog-agent/pkg/util/log v0.64.0-rc.1 h1:5UHDao4MdRwRsf4ZEvMSbgoujHY/2Aj+TQ768ZrPXq8= +github.com/DataDog/datadog-agent/pkg/util/log v0.64.0-rc.1/go.mod h1:ZEm+kWbgm3alAsoVbYFM10a+PIxEW5KoVhV3kwiCuxE= +github.com/DataDog/datadog-agent/pkg/util/scrubber v0.64.0-rc.1 h1:yqzXiCXrBXsQrbsFCTele7SgM6nK0bElDmBM0lsueIE= +github.com/DataDog/datadog-agent/pkg/util/scrubber v0.64.0-rc.1/go.mod h1:9ZfE6J8Ty8xkgRuoH1ip9kvtlq6UaHwPOqxe9NJbVUE= +github.com/DataDog/datadog-agent/pkg/version v0.64.0-rc.1 h1:eg+XW2CzOwFa//bjoXiw4xhNWWSdEJbMSC4TFcx6lVk= +github.com/DataDog/datadog-agent/pkg/version v0.64.0-rc.1/go.mod h1:DgOVsfSRaNV4GZNl/qgoZjG3hJjoYUNWPPhbfTfTqtY= +github.com/DataDog/datadog-go/v5 v5.6.0 h1:2oCLxjF/4htd55piM75baflj/KoE6VYS7alEUqFvRDw= +github.com/DataDog/datadog-go/v5 v5.6.0/go.mod h1:K9kcYBlxkcPP8tvvjZZKs/m1edNAUFzBbdpTUKfCsuw= +github.com/DataDog/go-libddwaf/v3 v3.5.3 h1:UzIUhr/9SnRpDkxE18VeU6Fu4HiDv9yIR5R36N/LwVI= +github.com/DataDog/go-libddwaf/v3 v3.5.3/go.mod h1:HoLUHdj0NybsPBth/UppTcg8/DKA4g+AXuk8cZ6nuoo= 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-sqllexer v0.1.0 h1:QGBH68R4PFYGUbZjNjsT4ESHCIhO9Mmiz+SMKI7DzaY= +github.com/DataDog/go-sqllexer v0.1.0/go.mod h1:KwkYhpFEVIq+BfobkTC1vfqm4gTi65skV/DpDBXtexc= github.com/DataDog/go-tuf v1.1.0-0.5.2 h1:4CagiIekonLSfL8GMHRHcHudo1fQnxELS9g4tiAupQ4= github.com/DataDog/go-tuf v1.1.0-0.5.2/go.mod h1:zBcq6f654iVqmkk8n2Cx81E1JnNTMOAx1UEO/wZR+P0= github.com/DataDog/gostackparse v0.7.0 h1:i7dLkXHvYzHV308hnkvVGDL3BR4FWl7IsXNPz/IGQh4= github.com/DataDog/gostackparse v0.7.0/go.mod h1:lTfqcJKqS9KnXQGnyQMCugq3u1FP6UZMfWR0aitKFMM= -github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes v0.20.0 h1:fKv05WFWHCXQmUTehW1eEZvXJP65Qv00W4V01B1EqSA= -github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes v0.20.0/go.mod h1:dvIWN9pA2zWNTw5rhDWZgzZnhcfpH++d+8d1SWW6xkY= -github.com/DataDog/sketches-go v1.4.5 h1:ki7VfeNz7IcNafq7yI/j5U/YCkO3LJiMDtXz9OMQbyE= -github.com/DataDog/sketches-go v1.4.5/go.mod h1:7Y8GN8Jf66DLyDhc94zuWA3uHEt/7ttt8jHOBWWrSOg= +github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes v0.26.0 h1:GlvoS6hJN0uANUC3fjx72rOgM4StAKYo2HtQGaasC7s= +github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes v0.26.0/go.mod h1:mYQmU7mbHH6DrCaS8N6GZcxwPoeNfyuopUoLQltwSzs= +github.com/DataDog/sketches-go v1.4.7 h1:eHs5/0i2Sdf20Zkj0udVFWuCrXGRFig2Dcfm5rtcTxc= +github.com/DataDog/sketches-go v1.4.7/go.mod h1:eAmQ/EBmtSO+nQp7IZMZVRPT4BQTmIc5RZQ+deGlTPM= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0 h1:f2Qw/Ehhimh5uO1fayV0QIW7DShEQqhtUfhYc+cBPlw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0/go.mod h1:2bIszWvQRlJVmJLiuLhukLImRjKPcYdzzsx6darK02A= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0 h1:5IT7xOdq17MtcdtL/vtl6mGfzhaq4m4vpollPRmlsBQ= @@ -1198,6 +1202,8 @@ github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= +github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -1282,8 +1288,8 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 h1:y3N7Bm7Y9/CtpiVkw/ZWj6lSlDF3F74SfKwfTCer72Q= -github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= @@ -1487,7 +1493,6 @@ github.com/liamg/memoryfs v1.6.0 h1:jAFec2HI1PgMTem5gR7UT8zi9u4BfG5jorCRlLH06W8= github.com/liamg/memoryfs v1.6.0/go.mod h1:z7mfqXFQS8eSeBBsFjYLlxYRMRyiPktytvYCYTb3BSk= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a h1:3Bm7EwfUQUvhNeKIkUct/gl9eod1TcXuj8stxvi/GoI= github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= @@ -1613,6 +1618,10 @@ github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/open-policy-agent/opa v1.3.0 h1:zVvQvQg+9+FuSRBt4LgKNzJwsWl/c85kD5jPozJTydY= github.com/open-policy-agent/opa v1.3.0/go.mod h1:t9iPNhaplD2qpiBqeudzJtEX3fKHK8zdA29oFvofAHo= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.120.1 h1:lK/3zr73guK9apbXTcnDnYrC0YCQ25V3CIULYz3k2xU= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.120.1/go.mod h1:01TvyaK8x640crO2iFwW/6CFCZgNsOvOGH3B5J239m0= +github.com/open-telemetry/opentelemetry-collector-contrib/processor/probabilisticsamplerprocessor v0.120.1 h1:TCyOus9tym82PD1VYtthLKMVMlVyRwtDI4ck4SR2+Ok= +github.com/open-telemetry/opentelemetry-collector-contrib/processor/probabilisticsamplerprocessor v0.120.1/go.mod h1:Z/S1brD5gU2Ntht/bHxBVnGxXKTvZDr0dNv/riUzPmY= github.com/openai/openai-go v0.1.0-beta.10 h1:CknhGXe8aXQMRuqg255PFnWzgRY9nEryMxoNIBBM9tU= github.com/openai/openai-go v0.1.0-beta.10/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -1635,8 +1644,8 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0 github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= -github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986 h1:jYi87L8j62qkXzaYHAQAhEapgukhenIMZRBKTNRLHJ4= -github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= @@ -1668,7 +1677,6 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus-community/pro-bing v0.7.0 h1:KFYFbxC2f2Fp6c+TyxbCOEarf7rbnzr9Gw8eIb0RfZA= @@ -1684,6 +1692,8 @@ github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= +github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe60+5DqOpCjPE= github.com/quasilyte/go-ruleguard/dsl v0.3.22/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= @@ -1720,14 +1730,8 @@ github.com/secure-systems-lab/go-securesystemslib v0.9.0 h1:rf1HIbL64nUpEIZnjLZ3 github.com/secure-systems-lab/go-securesystemslib v0.9.0/go.mod h1:DVHKMcZ+V4/woA/peqr+L0joiRXbPpQ042GgJckkFgw= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= -github.com/shirou/gopsutil/v3 v3.24.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU= -github.com/shirou/gopsutil/v3 v3.24.4/go.mod h1:lTd2mdiOspcqLgAnr9/nGi71NkeMpWKdmhuxm9GusH8= github.com/shirou/gopsutil/v4 v4.25.2 h1:NMscG3l2CqtWFS86kj3vP7soOczqrQYIEhO/pMvvQkk= github.com/shirou/gopsutil/v4 v4.25.2/go.mod h1:34gBYJzyqCDT11b6bMHP0XCvWeU3J61XRT7a2EmCRTA= -github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= -github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= -github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= -github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= @@ -1815,14 +1819,12 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= -github.com/tinylib/msgp v1.2.1 h1:6ypy2qcCznxpP4hpORzhtXyTqrBs7cfM9MCCWY8zsmU= -github.com/tinylib/msgp v1.2.1/go.mod h1:2vIGs3lcUo8izAATNobrCHevYZC/LMsJtw4JPiYPHro= -github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= -github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4= -github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0= -github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= -github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4= -github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY= +github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po= +github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= +github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= +github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= +github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= github.com/u-root/gobusybox/src v0.0.0-20240225013946-a274a8d5d83a h1:eg5FkNoQp76ZsswyGZ+TjYqA/rhKefxK8BW7XOlQsxo= github.com/u-root/gobusybox/src v0.0.0-20240225013946-a274a8d5d83a/go.mod h1:e/8TmrdreH0sZOw2DFKBaUV7bvDWRq6SeM9PzkuVM68= github.com/u-root/u-root v0.14.0 h1:Ka4T10EEML7dQ5XDvO9c3MBN8z4nuSnGjcd1jmU2ivg= @@ -1846,8 +1848,8 @@ github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZla github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= -github.com/vmihailenco/msgpack/v4 v4.3.12 h1:07s4sz9IReOgdikxLTKNbBdqDMLsjPKXwvCazn8G65U= -github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= +github.com/vmihailenco/msgpack/v4 v4.3.13 h1:A2wsiTbvp63ilDaWmsk2wjx6xZdxQOvpiNlKBGKKXKI= +github.com/vmihailenco/msgpack/v4 v4.3.13/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc= @@ -1913,16 +1915,34 @@ go.nhat.io/otelsql v0.15.0 h1:e2lpIaFPe62Pa1fXZoOWXTvMzcN4SwHwHdCz1wDUG6c= go.nhat.io/otelsql v0.15.0/go.mod h1:IYUaWCLf7c883mzhfVpHXTBn0jxF4TRMkQjX6fqhXJ8= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/collector/component v0.104.0 h1:jqu/X9rnv8ha0RNZ1a9+x7OU49KwSMsPbOuIEykHuQE= -go.opentelemetry.io/collector/component v0.104.0/go.mod h1:1C7C0hMVSbXyY1ycCmaMUAR9fVwpgyiNQqxXtEWhVpw= -go.opentelemetry.io/collector/config/configtelemetry v0.104.0 h1:eHv98XIhapZA8MgTiipvi+FDOXoFhCYOwyKReOt+E4E= -go.opentelemetry.io/collector/config/configtelemetry v0.104.0/go.mod h1:WxWKNVAQJg/Io1nA3xLgn/DWLE/W1QOB2+/Js3ACi40= -go.opentelemetry.io/collector/pdata v1.11.0 h1:rzYyV1zfTQQz1DI9hCiaKyyaczqawN75XO9mdXmR/hE= -go.opentelemetry.io/collector/pdata v1.11.0/go.mod h1:IHxHsp+Jq/xfjORQMDJjSH6jvedOSTOyu3nbxqhWSYE= -go.opentelemetry.io/collector/pdata/pprofile v0.104.0 h1:MYOIHvPlKEJbWLiBKFQWGD0xd2u22xGVLt4jPbdxP4Y= -go.opentelemetry.io/collector/pdata/pprofile v0.104.0/go.mod h1:7WpyHk2wJZRx70CGkBio8klrYTTXASbyIhf+rH4FKnA= -go.opentelemetry.io/collector/semconv v0.104.0 h1:dUvajnh+AYJLEW/XOPk0T0BlwltSdi3vrjO7nSOos3k= -go.opentelemetry.io/collector/semconv v0.104.0/go.mod h1:yMVUCNoQPZVq/IPfrHrnntZTWsLf5YGZ7qwKulIl5hw= +go.opentelemetry.io/collector/component v0.120.0 h1:YHEQ6NuBI6FQHKW24OwrNg2IJ0EUIg4RIuwV5YQ6PSI= +go.opentelemetry.io/collector/component v0.120.0/go.mod h1:Ya5O+5NWG9XdhJPnOVhKtBrNXHN3hweQbB98HH4KPNU= +go.opentelemetry.io/collector/component/componentstatus v0.120.0 h1:hzKjI9+AIl8A/saAARb47JqabWsge0kMp8NSPNiCNOQ= +go.opentelemetry.io/collector/component/componentstatus v0.120.0/go.mod h1:kbuAEddxvcyjGLXGmys3nckAj4jTGC0IqDIEXAOr3Ag= +go.opentelemetry.io/collector/component/componenttest v0.120.0 h1:vKX85d3lpxj/RoiFQNvmIpX9lOS80FY5svzOYUyeYX0= +go.opentelemetry.io/collector/component/componenttest v0.120.0/go.mod h1:QDLboWF2akEqAGyvje8Hc7GfXcrZvQ5FhmlWvD5SkzY= +go.opentelemetry.io/collector/consumer v1.26.0 h1:0MwuzkWFLOm13qJvwW85QkoavnGpR4ZObqCs9g1XAvk= +go.opentelemetry.io/collector/consumer v1.26.0/go.mod h1:I/ZwlWM0sbFLhbStpDOeimjtMbWpMFSoGdVmzYxLGDg= +go.opentelemetry.io/collector/consumer/consumertest v0.120.0 h1:iPFmXygDsDOjqwdQ6YZcTmpiJeQDJX+nHvrjTPsUuv4= +go.opentelemetry.io/collector/consumer/consumertest v0.120.0/go.mod h1:HeSnmPfAEBnjsRR5UY1fDTLlSrYsMsUjufg1ihgnFJ0= +go.opentelemetry.io/collector/consumer/xconsumer v0.120.0 h1:dzM/3KkFfMBIvad+NVXDV+mA+qUpHyu5c70TFOjDg68= +go.opentelemetry.io/collector/consumer/xconsumer v0.120.0/go.mod h1:eOf7RX9CYC7bTZQFg0z2GHdATpQDxI0DP36F9gsvXOQ= +go.opentelemetry.io/collector/pdata v1.26.0 h1:o7nP0RTQOG0LXk55ZZjLrxwjX8x3wHF7Z7xPeOaskEA= +go.opentelemetry.io/collector/pdata v1.26.0/go.mod h1:18e8/xDZsqyj00h/5HM5GLdJgBzzG9Ei8g9SpNoiMtI= +go.opentelemetry.io/collector/pdata/pprofile v0.120.0 h1:lQl74z41MN9a0M+JFMZbJVesjndbwHXwUleVrVcTgc8= +go.opentelemetry.io/collector/pdata/pprofile v0.120.0/go.mod h1:4zwhklS0qhjptF5GUJTWoCZSTYE+2KkxYrQMuN4doVI= +go.opentelemetry.io/collector/pdata/testdata v0.120.0 h1:Zp0LBOv3yzv/lbWHK1oht41OZ4WNbaXb70ENqRY7HnE= +go.opentelemetry.io/collector/pdata/testdata v0.120.0/go.mod h1:PfezW5Rzd13CWwrElTZRrjRTSgMGUOOGLfHeBjj+LwY= +go.opentelemetry.io/collector/pipeline v0.120.0 h1:QQQbnLCYiuOqmxIRQ11cvFGt+SXq0rypK3fW8qMkzqQ= +go.opentelemetry.io/collector/pipeline v0.120.0/go.mod h1:TO02zju/K6E+oFIOdi372Wk0MXd+Szy72zcTsFQwXl4= +go.opentelemetry.io/collector/processor v0.120.0 h1:No+I65ybBLVy4jc7CxcsfduiBrm7Z6kGfTnekW3hx1A= +go.opentelemetry.io/collector/processor v0.120.0/go.mod h1:4zaJGLZCK8XKChkwlGC/gn0Dj4Yke04gQCu4LGbJGro= +go.opentelemetry.io/collector/processor/processortest v0.120.0 h1:R+VSVSU59W0/mPAcyt8/h1d0PfWN6JI2KY5KeMICXvo= +go.opentelemetry.io/collector/processor/processortest v0.120.0/go.mod h1:me+IVxPsj4IgK99I0pgKLX34XnJtcLwqtgTuVLhhYDI= +go.opentelemetry.io/collector/processor/xprocessor v0.120.0 h1:mBznj/1MtNqmu6UpcoXz6a63tU0931oWH2pVAt2+hzo= +go.opentelemetry.io/collector/processor/xprocessor v0.120.0/go.mod h1:Nsp0sDR3gE+GAhi9d0KbN0RhOP+BK8CGjBRn8+9d/SY= +go.opentelemetry.io/collector/semconv v0.120.0 h1:iG9N78c2IZN4XOH7ZSdAQJBbaHDTuPnTlbQjKV9uIPY= +go.opentelemetry.io/collector/semconv v0.120.0/go.mod h1:te6VQ4zZJO5Lp8dM2XIhDxDiL45mwX0YAQQWRQ0Qr9U= 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= @@ -1941,8 +1961,6 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk= -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= @@ -2009,8 +2027,8 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= -golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= +golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs= +golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -2273,7 +2291,6 @@ golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.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.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -2690,8 +2707,8 @@ google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= -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/DataDog/dd-trace-go.v1 v1.73.0 h1:9s6iGFpUBbotQJtv4wHhgHoLrFFji3m/PPcuvZCFieE= +gopkg.in/DataDog/dd-trace-go.v1 v1.73.0/go.mod h1:MVHzDPBdS141gBKBwXvaa8VOLyfoO/vFTLW71OkGxug= 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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= @@ -2729,6 +2746,8 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= +k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= +k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= kernel.org/pub/linux/libs/security/libcap/cap v1.2.73 h1:Th2b8jljYqkyZKS3aD3N9VpYsQpHuXLgea+SZUIfODA= From 345a239838d5fac5edf4fb520ddd66fe45c48e7d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 12:23:29 +0000 Subject: [PATCH 05/88] chore: bump github.com/open-policy-agent/opa from 1.3.0 to 1.4.2 (#17674) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [github.com/open-policy-agent/opa](https://github.com/open-policy-agent/opa) from 1.3.0 to 1.4.2.
Release notes

Sourced from github.com/open-policy-agent/opa's releases.

v1.4.2

This is a bug fix release addressing the missing capabilities/v1.4.1.json in the v1.4.1 release.

v1.4.1

⚠️ Please skip this release and go straight to v1.4.2 ⚠️ This release is broken due to a mistake during the release process and the artifacts are missing a crucial capabilities file. Sorry for any inconvenience.


This is a security fix release for the fixes published in Go 1.24.1 and 1.24.2

  • build: bump go to 1.24.2 (#7544) (authored by @​sspaink) Addressing CVE-2025-22870 and CVE-2025-22871 vulnerabilities in the Go runtime.

v1.4.0

This release contains a security fix addressing CVE-2025-46569. It also includes a mix of new features, bugfixes, and dependency updates.

Security Fix: CVE-2025-46569 - OPA server Data API HTTP path injection of Rego (GHSA-6m8w-jc87-6cr7)

A vulnerability in the OPA server's Data API allows an attacker to craft the HTTP path in a way that injects Rego code into the query that is evaluated.
The evaluation result cannot be made to return any other data than what is generated by the requested path, but this path can be misdirected, and the injected Rego code can be crafted to make the query succeed or fail; opening up for oracle attacks or, given the right circumstances, erroneous policy decision results. Furthermore, the injected code can be crafted to be computationally expensive, resulting in a Denial Of Service (DoS) attack.

Users are only impacted if all of the following apply:

  • OPA is deployed as a standalone server (rather than being used as a Go library)
  • The OPA server is exposed outside of the local host in an untrusted environment.
  • The configured authorization policy does not do exact matching of the input.path attribute when deciding if the request should be allowed.

or, if all of the following apply:

  • OPA is deployed as a standalone server.
  • The service connecting to OPA allows 3rd parties to insert unsanitised text into the path of the HTTP request to OPA’s Data API.

Note: With no Authorization Policy configured for restricting API access (the default configuration), the RESTful Data API provides access for managing Rego policies; and the RESTful Query API facilitates advanced queries. Full access to these APIs provides both simpler, and broader access than what the security issue describes here can facilitate. As such, OPA servers exposed to a network are not considered affected by the attack described here if they are knowingly not restricting access through an Authorization Policy.

This issue affects all versions of OPA prior to 1.4.0.

See the Security Advisory for more details.

Reported by @​GamrayW, @​HyouKash, @​AdrienIT, authored by @​johanfylling

Runtime, Tooling, SDK

... (truncated)

Changelog

Sourced from github.com/open-policy-agent/opa's changelog.

1.4.2

This is a bug fix release addressing the missing capabilities/v1.4.1.json in the v1.4.1 release.

1.4.1

This is a security fix release for the fixes published in Go 1.24.1 and 1.24.2

  • build: bump go to 1.24.2 (#7544) (authored by @​sspaink) Addressing CVE-2025-22870 and CVE-2025-22871 vulnerabilities in the Go runtime.

1.4.0

This release contains a security fix addressing CVE-2025-46569. It also includes a mix of new features, bugfixes, and dependency updates.

Security Fix: CVE-2025-46569 - OPA server Data API HTTP path injection of Rego (GHSA-6m8w-jc87-6cr7)

A vulnerability in the OPA server's Data API allows an attacker to craft the HTTP path in a way that injects Rego code into the query that is evaluated.
The evaluation result cannot be made to return any other data than what is generated by the requested path, but this path can be misdirected, and the injected Rego code can be crafted to make the query succeed or fail; opening up for oracle attacks or, given the right circumstances, erroneous policy decision results. Furthermore, the injected code can be crafted to be computationally expensive, resulting in a Denial Of Service (DoS) attack.

Users are only impacted if all of the following apply:

  • OPA is deployed as a standalone server (rather than being used as a Go library)
  • The OPA server is exposed outside of the local host in an untrusted environment.
  • The configured authorization policy does not do exact matching of the input.path attribute when deciding if the request should be allowed.

or, if all of the following apply:

  • OPA is deployed as a standalone server.
  • The service connecting to OPA allows 3rd parties to insert unsanitised text into the path of the HTTP request to OPA’s Data API.

Note: With no Authorization Policy configured for restricting API access (the default configuration), the RESTful Data API provides access for managing Rego policies; and the RESTful Query API facilitates advanced queries. Full access to these APIs provides both simpler, and broader access than what the security issue describes here can facilitate. As such, OPA servers exposed to a network are not considered affected by the attack described here if they are knowingly not restricting access through an Authorization Policy.

This issue affects all versions of OPA prior to 1.4.0.

See the Security Advisory for more details.

Reported by @​GamrayW, @​HyouKash, @​AdrienIT, authored by @​johanfylling

Runtime, Tooling, SDK

... (truncated)

Commits
  • 5e4582b Prepare v1.4.2 release (#7547)
  • 3b64aff Patch release v1.4.1 (#7545)
  • 8b07202 Prepare v1.4.0 release (#7541)
  • ad20632 Merge commit from fork
  • 24ff9cf fix: return the raw strings when formatting (#7525)
  • 254f3bf fix(status plugin): make sure the latest status is read before manually trigg...
  • 9b5f601 docs: fix post merge badge (#7532)
  • e490277 docs: Point path versioned requests to new sites (#7531)
  • d65888c plugins/status: FIFO buffer channel for status events to prevent slow status ...
  • eb77d10 docs: update edge links to use /docs/edge/ path (#7529)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/open-policy-agent/opa&package-manager=go_modules&previous-version=1.3.0&new-version=1.4.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 3 ++- go.sum | 16 ++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 25d6f70ec2f16..2db08036dcb90 100644 --- a/go.mod +++ b/go.mod @@ -158,7 +158,7 @@ require ( github.com/mocktools/go-smtp-mock/v2 v2.4.0 github.com/muesli/termenv v0.16.0 github.com/natefinch/atomic v1.0.1 - github.com/open-policy-agent/opa v1.3.0 + github.com/open-policy-agent/opa v1.4.2 github.com/ory/dockertest/v3 v3.12.0 github.com/pion/udp v0.1.4 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c @@ -510,6 +510,7 @@ require ( github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // indirect + github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect diff --git a/go.sum b/go.sum index a42428c7c8e7e..36de6b4f74d46 100644 --- a/go.sum +++ b/go.sum @@ -964,13 +964,13 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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.6.0 h1:acOwfOOZ4p1dPRnYzvkVm7rUk2Y21TgPVepCy5dJdFQ= -github.com/dgraph-io/badger/v4 v4.6.0/go.mod h1:KSJ5VTuZNC3Sd+YhvVjk2nYua9UZnnTr/SkXvdtiPgI= -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/dgraph-io/badger/v4 v4.7.0 h1:Q+J8HApYAY7UMpL8d9owqiB+odzEc0zn/aqOD9jhc6Y= +github.com/dgraph-io/badger/v4 v4.7.0/go.mod h1:He7TzG3YBy3j4f5baj5B7Zl2XyfNe5bl4Udl0aPemVA= +github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM= +github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI= 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= +github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= +github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/dhui/dktest v0.4.3 h1:wquqUxAFdcUgabAVLvSCOKOlag5cIZuaOjYIBOWdsR0= @@ -1616,8 +1616,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.3.0 h1:zVvQvQg+9+FuSRBt4LgKNzJwsWl/c85kD5jPozJTydY= -github.com/open-policy-agent/opa v1.3.0/go.mod h1:t9iPNhaplD2qpiBqeudzJtEX3fKHK8zdA29oFvofAHo= +github.com/open-policy-agent/opa v1.4.2 h1:ag4upP7zMsa4WE2p1pwAFeG4Pn3mNwfAx9DLhhJfbjU= +github.com/open-policy-agent/opa v1.4.2/go.mod h1:DNzZPKqKh4U0n0ANxcCVlw8lCSv2c+h5G/3QvSYdWZ8= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.120.1 h1:lK/3zr73guK9apbXTcnDnYrC0YCQ25V3CIULYz3k2xU= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.120.1/go.mod h1:01TvyaK8x640crO2iFwW/6CFCZgNsOvOGH3B5J239m0= github.com/open-telemetry/opentelemetry-collector-contrib/processor/probabilisticsamplerprocessor v0.120.1 h1:TCyOus9tym82PD1VYtthLKMVMlVyRwtDI4ck4SR2+Ok= From 799a0ba57348056771f0c9229b2e0d67ae713dad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 12:24:00 +0000 Subject: [PATCH 06/88] chore: bump github.com/valyala/fasthttp from 1.61.0 to 1.62.0 (#17766) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [github.com/valyala/fasthttp](https://github.com/valyala/fasthttp) from 1.61.0 to 1.62.0.
Release notes

Sourced from github.com/valyala/fasthttp's releases.

v1.62.0

What's Changed

New Contributors

Full Changelog: https://github.com/valyala/fasthttp/compare/v1.61.0...v1.62.0

Commits
  • 9e457eb mod acceptConn (#2005)
  • 69a68df chore(deps): bump golang.org/x/net from 0.39.0 to 0.40.0 (#2003)
  • 83fbe80 chore(deps): bump golang.org/x/crypto from 0.37.0 to 0.38.0 (#2002)
  • 51817a4 chore(deps): bump golangci/golangci-lint-action from 7 to 8 (#2001)
  • 41a1449 feat: move user values to Request structure (#1999)
  • 1345f42 Add support for streaming identity-encoded or unknown length response bodies ...
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/valyala/fasthttp&package-manager=go_modules&previous-version=1.61.0&new-version=1.62.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 14 +++++++------- go.sum | 28 ++++++++++++++-------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/go.mod b/go.mod index 2db08036dcb90..e09c4d4fbaa82 100644 --- a/go.mod +++ b/go.mod @@ -181,7 +181,7 @@ 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.61.0 + github.com/valyala/fasthttp v1.62.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 @@ -195,15 +195,15 @@ require ( 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.37.0 + golang.org/x/crypto v0.38.0 golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac golang.org/x/mod v0.24.0 - golang.org/x/net v0.39.0 + golang.org/x/net v0.40.0 golang.org/x/oauth2 v0.29.0 - golang.org/x/sync v0.13.0 - golang.org/x/sys v0.32.0 - golang.org/x/term v0.31.0 - golang.org/x/text v0.24.0 // indirect + golang.org/x/sync v0.14.0 + golang.org/x/sys v0.33.0 + golang.org/x/term v0.32.0 + golang.org/x/text v0.25.0 // indirect golang.org/x/tools v0.32.0 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da google.golang.org/api v0.231.0 diff --git a/go.sum b/go.sum index 36de6b4f74d46..340dc21cfe16c 100644 --- a/go.sum +++ b/go.sum @@ -1838,8 +1838,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.61.0 h1:VV08V0AfoRaFurP1EWKvQQdPTZHiUzaVoulX1aBDgzU= -github.com/valyala/fasthttp v1.61.0/go.mod h1:wRIV/4cMwUPWnRcDno9hGnYZGh78QzODFfo1LTUhBog= +github.com/valyala/fasthttp v1.62.0 h1:8dKRBX/y2rCzyc6903Zu1+3qN0H/d2MsxPPmVNamiH0= +github.com/valyala/fasthttp v1.62.0/go.mod h1:FCINgr4GKdKqV8Q0xv8b+UxPV+H/O5nNFo3D+r54Htg= 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= @@ -2010,8 +2010,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -2140,8 +2140,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -2194,8 +2194,8 @@ golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -2294,8 +2294,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 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= @@ -2314,8 +2314,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= -golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= -golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -2338,8 +2338,8 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= From af2941bb92306f00c2e7576ec9a0a36a298e603f Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Mon, 12 May 2025 16:19:03 +0200 Subject: [PATCH 07/88] feat: add `is_prebuild_claim` to distinguish post-claim provisioning (#17757) Used in combination with https://github.com/coder/terraform-provider-coder/pull/396 This is required by both https://github.com/coder/coder/pull/17475 and https://github.com/coder/coder/pull/17571 Operators may need to conditionalize their templates to perform certain operations once a prebuilt workspace has been claimed. This value will **only** be set once a claim takes place and a subsequent `terraform apply` occurs. Any `terraform apply` runs thereafter will be indistinguishable from a normal run on a workspace. --------- Signed-off-by: Danny Kopping --- ...oder_provisioner_list_--output_json.golden | 2 +- .../provisionerdserver/provisionerdserver.go | 11 +- .../provisionerdserver_test.go | 13 +- coderd/workspaces.go | 2 +- coderd/wsbuilder/wsbuilder.go | 19 +- go.mod | 2 +- go.sum | 4 +- provisioner/terraform/provision.go | 5 +- provisioner/terraform/provision_test.go | 43 +- provisionerd/proto/version.go | 7 +- provisionerd/provisionerd.go | 4 +- provisionersdk/proto/prebuilt_workspace.go | 9 + provisionersdk/proto/provisioner.pb.go | 668 ++++++++++-------- provisionersdk/proto/provisioner.proto | 10 +- site/e2e/provisionerGenerated.ts | 18 +- 15 files changed, 478 insertions(+), 339 deletions(-) create mode 100644 provisionersdk/proto/prebuilt_workspace.go diff --git a/cli/testdata/coder_provisioner_list_--output_json.golden b/cli/testdata/coder_provisioner_list_--output_json.golden index f619dce028cde..3daeb89febcb4 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.4", + "api_version": "1.5", "provisioners": [ "echo" ], diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 9362d2f3e5a85..68aece517bb2f 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -645,7 +645,7 @@ func (s *server) acquireProtoJob(ctx context.Context, job database.ProvisionerJo WorkspaceBuildId: workspaceBuild.ID.String(), WorkspaceOwnerLoginType: string(owner.LoginType), WorkspaceOwnerRbacRoles: ownerRbacRoles, - IsPrebuild: input.IsPrebuild, + PrebuiltWorkspaceBuildStage: input.PrebuiltWorkspaceBuildStage, }, LogLevel: input.LogLevel, }, @@ -2471,11 +2471,10 @@ type TemplateVersionImportJob struct { // WorkspaceProvisionJob is the payload for the "workspace_provision" job type. type WorkspaceProvisionJob struct { - WorkspaceBuildID uuid.UUID `json:"workspace_build_id"` - DryRun bool `json:"dry_run"` - IsPrebuild bool `json:"is_prebuild,omitempty"` - PrebuildClaimedByUser uuid.UUID `json:"prebuild_claimed_by,omitempty"` - LogLevel string `json:"log_level,omitempty"` + WorkspaceBuildID uuid.UUID `json:"workspace_build_id"` + DryRun bool `json:"dry_run"` + LogLevel string `json:"log_level,omitempty"` + PrebuiltWorkspaceBuildStage sdkproto.PrebuiltWorkspaceBuildStage `json:"prebuilt_workspace_stage,omitempty"` } // TemplateVersionDryRunJob is the payload for the "template_version_dry_run" job type. diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index caeef8a9793b7..488ef4bbdfd97 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -23,10 +23,11 @@ import ( "storj.io/drpc" "cdr.dev/slog/sloggers/slogtest" - "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/quartz" "github.com/coder/serpent" + "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/buildinfo" "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/database" @@ -299,6 +300,10 @@ func TestAcquireJob(t *testing.T) { Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator, }) + var buildState sdkproto.PrebuiltWorkspaceBuildStage + if prebuiltWorkspace { + buildState = sdkproto.PrebuiltWorkspaceBuildStage_CREATE + } _ = dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{ ID: build.ID, OrganizationID: pd.OrganizationID, @@ -308,8 +313,8 @@ func TestAcquireJob(t *testing.T) { FileID: file.ID, Type: database.ProvisionerJobTypeWorkspaceBuild, Input: must(json.Marshal(provisionerdserver.WorkspaceProvisionJob{ - WorkspaceBuildID: build.ID, - IsPrebuild: prebuiltWorkspace, + WorkspaceBuildID: build.ID, + PrebuiltWorkspaceBuildStage: buildState, })), }) @@ -380,7 +385,7 @@ func TestAcquireJob(t *testing.T) { WorkspaceOwnerRbacRoles: []*sdkproto.Role{{Name: rbac.RoleOrgMember(), OrgId: pd.OrganizationID.String()}, {Name: "member", OrgId: ""}, {Name: rbac.RoleOrgAuditor(), OrgId: pd.OrganizationID.String()}}, } if prebuiltWorkspace { - wantedMetadata.IsPrebuild = true + wantedMetadata.PrebuiltWorkspaceBuildStage = sdkproto.PrebuiltWorkspaceBuildStage_CREATE } slices.SortFunc(wantedMetadata.WorkspaceOwnerRbacRoles, func(a, b *sdkproto.Role) int { diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 6b187e241e80f..b61564c5039a2 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -713,7 +713,7 @@ func createWorkspace( builder = builder.TemplateVersionPresetID(req.TemplateVersionPresetID) } if claimedWorkspace != nil { - builder = builder.MarkPrebuildClaimedBy(owner.ID) + builder = builder.MarkPrebuiltWorkspaceClaim() } if req.EnableDynamicParameters && api.Experiments.Enabled(codersdk.ExperimentDynamicParameters) { diff --git a/coderd/wsbuilder/wsbuilder.go b/coderd/wsbuilder/wsbuilder.go index 942829004309c..b6eb621c55620 100644 --- a/coderd/wsbuilder/wsbuilder.go +++ b/coderd/wsbuilder/wsbuilder.go @@ -16,6 +16,7 @@ import ( "github.com/coder/coder/v2/coderd/rbac/policy" "github.com/coder/coder/v2/provisioner/terraform/tfparse" "github.com/coder/coder/v2/provisionersdk" + sdkproto "github.com/coder/coder/v2/provisionersdk/proto" "github.com/google/uuid" "github.com/sqlc-dev/pqtype" @@ -76,8 +77,7 @@ type Builder struct { parameterValues *[]string templateVersionPresetParameterValues []database.TemplateVersionPresetParameter - prebuild bool - prebuildClaimedBy uuid.UUID + prebuiltWorkspaceBuildStage sdkproto.PrebuiltWorkspaceBuildStage verifyNoLegacyParametersOnce bool } @@ -174,15 +174,17 @@ func (b Builder) RichParameterValues(p []codersdk.WorkspaceBuildParameter) Build return b } +// MarkPrebuild indicates that a prebuilt workspace is being built. func (b Builder) MarkPrebuild() Builder { // nolint: revive - b.prebuild = true + b.prebuiltWorkspaceBuildStage = sdkproto.PrebuiltWorkspaceBuildStage_CREATE return b } -func (b Builder) MarkPrebuildClaimedBy(userID uuid.UUID) Builder { +// MarkPrebuiltWorkspaceClaim indicates that a prebuilt workspace is being claimed. +func (b Builder) MarkPrebuiltWorkspaceClaim() Builder { // nolint: revive - b.prebuildClaimedBy = userID + b.prebuiltWorkspaceBuildStage = sdkproto.PrebuiltWorkspaceBuildStage_CLAIM return b } @@ -322,10 +324,9 @@ func (b *Builder) buildTx(authFunc func(action policy.Action, object rbac.Object workspaceBuildID := uuid.New() input, err := json.Marshal(provisionerdserver.WorkspaceProvisionJob{ - WorkspaceBuildID: workspaceBuildID, - LogLevel: b.logLevel, - IsPrebuild: b.prebuild, - PrebuildClaimedByUser: b.prebuildClaimedBy, + WorkspaceBuildID: workspaceBuildID, + LogLevel: b.logLevel, + PrebuiltWorkspaceBuildStage: b.prebuiltWorkspaceBuildStage, }) if err != nil { return nil, nil, nil, BuildError{ diff --git a/go.mod b/go.mod index e09c4d4fbaa82..8fe4da248d522 100644 --- a/go.mod +++ b/go.mod @@ -101,7 +101,7 @@ require ( github.com/coder/quartz v0.1.3 github.com/coder/retry v1.5.1 github.com/coder/serpent v0.10.0 - github.com/coder/terraform-provider-coder/v2 v2.4.0 + github.com/coder/terraform-provider-coder/v2 v2.4.1 github.com/coder/websocket v1.8.13 github.com/coder/wgtunnel v0.1.13-0.20240522110300-ade90dfb2da0 github.com/coreos/go-oidc/v3 v3.14.1 diff --git a/go.sum b/go.sum index 340dc21cfe16c..b03afb434ce38 100644 --- a/go.sum +++ b/go.sum @@ -925,8 +925,8 @@ github.com/coder/tailscale v1.1.1-0.20250422090654-5090e715905e h1:nope/SZfoLB9M github.com/coder/tailscale v1.1.1-0.20250422090654-5090e715905e/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/v2 v2.4.0 h1:uuFmF03IyahAZLXEukOdmvV9hGfUMJSESD8+G5wkTcM= -github.com/coder/terraform-provider-coder/v2 v2.4.0/go.mod h1:2kaBpn5k9ZWtgKq5k4JbkVZG9DzEqR4mJSmpdshcO+s= +github.com/coder/terraform-provider-coder/v2 v2.4.1 h1:+HxLJVENJ+kvGhibQ0jbr8Evi6M857d9691ytxNbv90= +github.com/coder/terraform-provider-coder/v2 v2.4.1/go.mod h1:2kaBpn5k9ZWtgKq5k4JbkVZG9DzEqR4mJSmpdshcO+s= github.com/coder/trivy v0.0.0-20250409153844-e6b004bc465a h1:yryP7e+IQUAArlycH4hQrjXQ64eRNbxsV5/wuVXHgME= github.com/coder/trivy v0.0.0-20250409153844-e6b004bc465a/go.mod h1:dDvq9axp3kZsT63gY2Znd1iwzfqDq3kXbQnccIrjRYY= github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= diff --git a/provisioner/terraform/provision.go b/provisioner/terraform/provision.go index f8f82bbad7b9a..aa954ef734a02 100644 --- a/provisioner/terraform/provision.go +++ b/provisioner/terraform/provision.go @@ -270,9 +270,12 @@ func provisionEnv( "CODER_WORKSPACE_TEMPLATE_VERSION="+metadata.GetTemplateVersion(), "CODER_WORKSPACE_BUILD_ID="+metadata.GetWorkspaceBuildId(), ) - if metadata.GetIsPrebuild() { + if metadata.GetPrebuiltWorkspaceBuildStage().IsPrebuild() { env = append(env, provider.IsPrebuildEnvironmentVariable()+"=true") } + if metadata.GetPrebuiltWorkspaceBuildStage().IsPrebuiltWorkspaceClaim() { + env = append(env, provider.IsPrebuildClaimEnvironmentVariable()+"=true") + } for key, value := range provisionersdk.AgentScriptEnv() { env = append(env, key+"="+value) diff --git a/provisioner/terraform/provision_test.go b/provisioner/terraform/provision_test.go index 96514cc4b59ad..ecff965b72984 100644 --- a/provisioner/terraform/provision_test.go +++ b/provisioner/terraform/provision_test.go @@ -25,6 +25,7 @@ import ( "cdr.dev/slog" "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/codersdk/drpc" "github.com/coder/coder/v2/provisioner/terraform" "github.com/coder/coder/v2/provisionersdk" @@ -977,7 +978,7 @@ func TestProvision(t *testing.T) { required_providers { coder = { source = "coder/coder" - version = "2.3.0-pre2" + version = ">= 2.4.1" } } } @@ -994,7 +995,7 @@ func TestProvision(t *testing.T) { }, Request: &proto.PlanRequest{ Metadata: &proto.Metadata{ - IsPrebuild: true, + PrebuiltWorkspaceBuildStage: proto.PrebuiltWorkspaceBuildStage_CREATE, }, }, Response: &proto.PlanComplete{ @@ -1008,6 +1009,44 @@ func TestProvision(t *testing.T) { }}, }, }, + { + Name: "is-prebuild-claim", + Files: map[string]string{ + "main.tf": `terraform { + required_providers { + coder = { + source = "coder/coder" + version = ">= 2.4.1" + } + } + } + data "coder_workspace" "me" {} + resource "null_resource" "example" {} + resource "coder_metadata" "example" { + resource_id = null_resource.example.id + item { + key = "is_prebuild_claim" + value = data.coder_workspace.me.is_prebuild_claim + } + } + `, + }, + Request: &proto.PlanRequest{ + Metadata: &proto.Metadata{ + PrebuiltWorkspaceBuildStage: proto.PrebuiltWorkspaceBuildStage_CLAIM, + }, + }, + Response: &proto.PlanComplete{ + Resources: []*proto.Resource{{ + Name: "example", + Type: "null_resource", + Metadata: []*proto.Resource_Metadata{{ + Key: "is_prebuild_claim", + Value: "true", + }}, + }}, + }, + }, } // Remove unused cache dirs before running tests. diff --git a/provisionerd/proto/version.go b/provisionerd/proto/version.go index d502a1f544fe3..1a82d240b9e7a 100644 --- a/provisionerd/proto/version.go +++ b/provisionerd/proto/version.go @@ -12,12 +12,17 @@ import "github.com/coder/coder/v2/apiversion" // // API v1.4: // - Add new field named `devcontainers` in the Agent. +// +// API v1.5: +// - Add new field named `prebuilt_workspace_build_stage` enum in the Metadata message. const ( CurrentMajor = 1 - CurrentMinor = 4 + CurrentMinor = 5 ) // CurrentVersion is the current provisionerd API version. // Breaking changes to the provisionerd API **MUST** increment // CurrentMajor above. +// Non-breaking changes to the provisionerd API **MUST** increment +// CurrentMinor above. var CurrentVersion = apiversion.New(CurrentMajor, CurrentMinor) diff --git a/provisionerd/provisionerd.go b/provisionerd/provisionerd.go index 6635495a2553a..76a06d7fa68b1 100644 --- a/provisionerd/provisionerd.go +++ b/provisionerd/provisionerd.go @@ -378,7 +378,7 @@ func (p *Server) acquireAndRunOne(client proto.DRPCProvisionerDaemonClient) erro slog.F("workspace_build_id", build.WorkspaceBuildId), slog.F("workspace_id", build.Metadata.WorkspaceId), slog.F("workspace_name", build.WorkspaceName), - slog.F("is_prebuild", build.Metadata.IsPrebuild), + slog.F("prebuilt_workspace_build_stage", build.Metadata.GetPrebuiltWorkspaceBuildStage().String()), ) span.SetAttributes( @@ -388,7 +388,7 @@ func (p *Server) acquireAndRunOne(client proto.DRPCProvisionerDaemonClient) erro attribute.String("workspace_owner_id", build.Metadata.WorkspaceOwnerId), attribute.String("workspace_owner", build.Metadata.WorkspaceOwner), attribute.String("workspace_transition", build.Metadata.WorkspaceTransition.String()), - attribute.Bool("is_prebuild", build.Metadata.IsPrebuild), + attribute.String("prebuilt_workspace_build_stage", build.Metadata.GetPrebuiltWorkspaceBuildStage().String()), ) } diff --git a/provisionersdk/proto/prebuilt_workspace.go b/provisionersdk/proto/prebuilt_workspace.go new file mode 100644 index 0000000000000..3aa80512344b6 --- /dev/null +++ b/provisionersdk/proto/prebuilt_workspace.go @@ -0,0 +1,9 @@ +package proto + +func (p PrebuiltWorkspaceBuildStage) IsPrebuild() bool { + return p == PrebuiltWorkspaceBuildStage_CREATE +} + +func (p PrebuiltWorkspaceBuildStage) IsPrebuiltWorkspaceClaim() bool { + return p == PrebuiltWorkspaceBuildStage_CLAIM +} diff --git a/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go index f258f79e36f94..cbe7ebb3007e6 100644 --- a/provisionersdk/proto/provisioner.pb.go +++ b/provisionersdk/proto/provisioner.pb.go @@ -226,6 +226,55 @@ func (WorkspaceTransition) EnumDescriptor() ([]byte, []int) { return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{3} } +type PrebuiltWorkspaceBuildStage int32 + +const ( + PrebuiltWorkspaceBuildStage_NONE PrebuiltWorkspaceBuildStage = 0 // Default value for builds unrelated to prebuilds. + PrebuiltWorkspaceBuildStage_CREATE PrebuiltWorkspaceBuildStage = 1 // A prebuilt workspace is being provisioned. + PrebuiltWorkspaceBuildStage_CLAIM PrebuiltWorkspaceBuildStage = 2 // A prebuilt workspace is being claimed. +) + +// Enum value maps for PrebuiltWorkspaceBuildStage. +var ( + PrebuiltWorkspaceBuildStage_name = map[int32]string{ + 0: "NONE", + 1: "CREATE", + 2: "CLAIM", + } + PrebuiltWorkspaceBuildStage_value = map[string]int32{ + "NONE": 0, + "CREATE": 1, + "CLAIM": 2, + } +) + +func (x PrebuiltWorkspaceBuildStage) Enum() *PrebuiltWorkspaceBuildStage { + p := new(PrebuiltWorkspaceBuildStage) + *p = x + return p +} + +func (x PrebuiltWorkspaceBuildStage) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (PrebuiltWorkspaceBuildStage) Descriptor() protoreflect.EnumDescriptor { + return file_provisionersdk_proto_provisioner_proto_enumTypes[4].Descriptor() +} + +func (PrebuiltWorkspaceBuildStage) Type() protoreflect.EnumType { + return &file_provisionersdk_proto_provisioner_proto_enumTypes[4] +} + +func (x PrebuiltWorkspaceBuildStage) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use PrebuiltWorkspaceBuildStage.Descriptor instead. +func (PrebuiltWorkspaceBuildStage) EnumDescriptor() ([]byte, []int) { + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{4} +} + type TimingState int32 const ( @@ -259,11 +308,11 @@ func (x TimingState) String() string { } func (TimingState) Descriptor() protoreflect.EnumDescriptor { - return file_provisionersdk_proto_provisioner_proto_enumTypes[4].Descriptor() + return file_provisionersdk_proto_provisioner_proto_enumTypes[5].Descriptor() } func (TimingState) Type() protoreflect.EnumType { - return &file_provisionersdk_proto_provisioner_proto_enumTypes[4] + return &file_provisionersdk_proto_provisioner_proto_enumTypes[5] } func (x TimingState) Number() protoreflect.EnumNumber { @@ -272,7 +321,7 @@ func (x TimingState) Number() protoreflect.EnumNumber { // Deprecated: Use TimingState.Descriptor instead. func (TimingState) EnumDescriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{4} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{5} } // Empty indicates a successful request/response. @@ -2284,27 +2333,27 @@ type Metadata struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - CoderUrl string `protobuf:"bytes,1,opt,name=coder_url,json=coderUrl,proto3" json:"coder_url,omitempty"` - WorkspaceTransition WorkspaceTransition `protobuf:"varint,2,opt,name=workspace_transition,json=workspaceTransition,proto3,enum=provisioner.WorkspaceTransition" json:"workspace_transition,omitempty"` - WorkspaceName string `protobuf:"bytes,3,opt,name=workspace_name,json=workspaceName,proto3" json:"workspace_name,omitempty"` - WorkspaceOwner string `protobuf:"bytes,4,opt,name=workspace_owner,json=workspaceOwner,proto3" json:"workspace_owner,omitempty"` - WorkspaceId string `protobuf:"bytes,5,opt,name=workspace_id,json=workspaceId,proto3" json:"workspace_id,omitempty"` - WorkspaceOwnerId string `protobuf:"bytes,6,opt,name=workspace_owner_id,json=workspaceOwnerId,proto3" json:"workspace_owner_id,omitempty"` - WorkspaceOwnerEmail string `protobuf:"bytes,7,opt,name=workspace_owner_email,json=workspaceOwnerEmail,proto3" json:"workspace_owner_email,omitempty"` - TemplateName string `protobuf:"bytes,8,opt,name=template_name,json=templateName,proto3" json:"template_name,omitempty"` - TemplateVersion string `protobuf:"bytes,9,opt,name=template_version,json=templateVersion,proto3" json:"template_version,omitempty"` - WorkspaceOwnerOidcAccessToken string `protobuf:"bytes,10,opt,name=workspace_owner_oidc_access_token,json=workspaceOwnerOidcAccessToken,proto3" json:"workspace_owner_oidc_access_token,omitempty"` - WorkspaceOwnerSessionToken string `protobuf:"bytes,11,opt,name=workspace_owner_session_token,json=workspaceOwnerSessionToken,proto3" json:"workspace_owner_session_token,omitempty"` - TemplateId string `protobuf:"bytes,12,opt,name=template_id,json=templateId,proto3" json:"template_id,omitempty"` - WorkspaceOwnerName string `protobuf:"bytes,13,opt,name=workspace_owner_name,json=workspaceOwnerName,proto3" json:"workspace_owner_name,omitempty"` - WorkspaceOwnerGroups []string `protobuf:"bytes,14,rep,name=workspace_owner_groups,json=workspaceOwnerGroups,proto3" json:"workspace_owner_groups,omitempty"` - WorkspaceOwnerSshPublicKey string `protobuf:"bytes,15,opt,name=workspace_owner_ssh_public_key,json=workspaceOwnerSshPublicKey,proto3" json:"workspace_owner_ssh_public_key,omitempty"` - WorkspaceOwnerSshPrivateKey string `protobuf:"bytes,16,opt,name=workspace_owner_ssh_private_key,json=workspaceOwnerSshPrivateKey,proto3" json:"workspace_owner_ssh_private_key,omitempty"` - WorkspaceBuildId string `protobuf:"bytes,17,opt,name=workspace_build_id,json=workspaceBuildId,proto3" json:"workspace_build_id,omitempty"` - WorkspaceOwnerLoginType string `protobuf:"bytes,18,opt,name=workspace_owner_login_type,json=workspaceOwnerLoginType,proto3" json:"workspace_owner_login_type,omitempty"` - WorkspaceOwnerRbacRoles []*Role `protobuf:"bytes,19,rep,name=workspace_owner_rbac_roles,json=workspaceOwnerRbacRoles,proto3" json:"workspace_owner_rbac_roles,omitempty"` - IsPrebuild bool `protobuf:"varint,20,opt,name=is_prebuild,json=isPrebuild,proto3" json:"is_prebuild,omitempty"` - RunningWorkspaceAgentToken string `protobuf:"bytes,21,opt,name=running_workspace_agent_token,json=runningWorkspaceAgentToken,proto3" json:"running_workspace_agent_token,omitempty"` + CoderUrl string `protobuf:"bytes,1,opt,name=coder_url,json=coderUrl,proto3" json:"coder_url,omitempty"` + WorkspaceTransition WorkspaceTransition `protobuf:"varint,2,opt,name=workspace_transition,json=workspaceTransition,proto3,enum=provisioner.WorkspaceTransition" json:"workspace_transition,omitempty"` + WorkspaceName string `protobuf:"bytes,3,opt,name=workspace_name,json=workspaceName,proto3" json:"workspace_name,omitempty"` + WorkspaceOwner string `protobuf:"bytes,4,opt,name=workspace_owner,json=workspaceOwner,proto3" json:"workspace_owner,omitempty"` + WorkspaceId string `protobuf:"bytes,5,opt,name=workspace_id,json=workspaceId,proto3" json:"workspace_id,omitempty"` + WorkspaceOwnerId string `protobuf:"bytes,6,opt,name=workspace_owner_id,json=workspaceOwnerId,proto3" json:"workspace_owner_id,omitempty"` + WorkspaceOwnerEmail string `protobuf:"bytes,7,opt,name=workspace_owner_email,json=workspaceOwnerEmail,proto3" json:"workspace_owner_email,omitempty"` + TemplateName string `protobuf:"bytes,8,opt,name=template_name,json=templateName,proto3" json:"template_name,omitempty"` + TemplateVersion string `protobuf:"bytes,9,opt,name=template_version,json=templateVersion,proto3" json:"template_version,omitempty"` + WorkspaceOwnerOidcAccessToken string `protobuf:"bytes,10,opt,name=workspace_owner_oidc_access_token,json=workspaceOwnerOidcAccessToken,proto3" json:"workspace_owner_oidc_access_token,omitempty"` + WorkspaceOwnerSessionToken string `protobuf:"bytes,11,opt,name=workspace_owner_session_token,json=workspaceOwnerSessionToken,proto3" json:"workspace_owner_session_token,omitempty"` + TemplateId string `protobuf:"bytes,12,opt,name=template_id,json=templateId,proto3" json:"template_id,omitempty"` + WorkspaceOwnerName string `protobuf:"bytes,13,opt,name=workspace_owner_name,json=workspaceOwnerName,proto3" json:"workspace_owner_name,omitempty"` + WorkspaceOwnerGroups []string `protobuf:"bytes,14,rep,name=workspace_owner_groups,json=workspaceOwnerGroups,proto3" json:"workspace_owner_groups,omitempty"` + WorkspaceOwnerSshPublicKey string `protobuf:"bytes,15,opt,name=workspace_owner_ssh_public_key,json=workspaceOwnerSshPublicKey,proto3" json:"workspace_owner_ssh_public_key,omitempty"` + WorkspaceOwnerSshPrivateKey string `protobuf:"bytes,16,opt,name=workspace_owner_ssh_private_key,json=workspaceOwnerSshPrivateKey,proto3" json:"workspace_owner_ssh_private_key,omitempty"` + WorkspaceBuildId string `protobuf:"bytes,17,opt,name=workspace_build_id,json=workspaceBuildId,proto3" json:"workspace_build_id,omitempty"` + WorkspaceOwnerLoginType string `protobuf:"bytes,18,opt,name=workspace_owner_login_type,json=workspaceOwnerLoginType,proto3" json:"workspace_owner_login_type,omitempty"` + WorkspaceOwnerRbacRoles []*Role `protobuf:"bytes,19,rep,name=workspace_owner_rbac_roles,json=workspaceOwnerRbacRoles,proto3" json:"workspace_owner_rbac_roles,omitempty"` + PrebuiltWorkspaceBuildStage PrebuiltWorkspaceBuildStage `protobuf:"varint,20,opt,name=prebuilt_workspace_build_stage,json=prebuiltWorkspaceBuildStage,proto3,enum=provisioner.PrebuiltWorkspaceBuildStage" json:"prebuilt_workspace_build_stage,omitempty"` // Indicates that a prebuilt workspace is being built. + RunningWorkspaceAgentToken string `protobuf:"bytes,21,opt,name=running_workspace_agent_token,json=runningWorkspaceAgentToken,proto3" json:"running_workspace_agent_token,omitempty"` // Preserves the running agent token of a prebuilt workspace so it can reinitialize. } func (x *Metadata) Reset() { @@ -2472,11 +2521,11 @@ func (x *Metadata) GetWorkspaceOwnerRbacRoles() []*Role { return nil } -func (x *Metadata) GetIsPrebuild() bool { +func (x *Metadata) GetPrebuiltWorkspaceBuildStage() PrebuiltWorkspaceBuildStage { if x != nil { - return x.IsPrebuild + return x.PrebuiltWorkspaceBuildStage } - return false + return PrebuiltWorkspaceBuildStage_NONE } func (x *Metadata) GetRunningWorkspaceAgentToken() string { @@ -3804,7 +3853,7 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 0x6b, 0x65, 0x79, 0x22, 0x31, 0x0a, 0x04, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x15, 0x0a, 0x06, 0x6f, 0x72, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x6f, 0x72, 0x67, 0x49, 0x64, 0x22, 0xe0, 0x08, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x05, 0x6f, 0x72, 0x67, 0x49, 0x64, 0x22, 0xae, 0x09, 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, @@ -3868,184 +3917,193 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 0x6c, 0x65, 0x73, 0x18, 0x13, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x6f, 0x6c, 0x65, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x52, 0x62, 0x61, 0x63, - 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x73, 0x5f, 0x70, 0x72, 0x65, 0x62, - 0x75, 0x69, 0x6c, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x69, 0x73, 0x50, 0x72, - 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x12, 0x41, 0x0a, 0x1d, 0x72, 0x75, 0x6e, 0x6e, 0x69, 0x6e, - 0x67, 0x5f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x15, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1a, 0x72, - 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, - 0x67, 0x65, 0x6e, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 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, 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, 0x99, 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, 0x12, 0x12, 0x0a, 0x04, - 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, - 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, + 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x12, 0x6d, 0x0a, 0x1e, 0x70, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, + 0x74, 0x5f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x62, 0x75, 0x69, 0x6c, + 0x64, 0x5f, 0x73, 0x74, 0x61, 0x67, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x28, 0x2e, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x65, 0x62, + 0x75, 0x69, 0x6c, 0x74, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, + 0x6c, 0x64, 0x53, 0x74, 0x61, 0x67, 0x65, 0x52, 0x1b, 0x70, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, + 0x74, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x53, + 0x74, 0x61, 0x67, 0x65, 0x12, 0x41, 0x0a, 0x1d, 0x72, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x5f, + 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, + 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x15, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1a, 0x72, 0x75, 0x6e, + 0x6e, 0x69, 0x6e, 0x67, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, + 0x6e, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 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, 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, 0x99, 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, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6c, + 0x61, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, 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, 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, + 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, 0x3e, 0x0a, 0x1b, 0x50, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x57, 0x6f, 0x72, + 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x53, 0x74, 0x61, 0x67, 0x65, + 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x52, + 0x45, 0x41, 0x54, 0x45, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x43, 0x4c, 0x41, 0x49, 0x4d, 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 ( @@ -4060,116 +4118,118 @@ func file_provisionersdk_proto_provisioner_proto_rawDescGZIP() []byte { return file_provisionersdk_proto_provisioner_proto_rawDescData } -var file_provisionersdk_proto_provisioner_proto_enumTypes = make([]protoimpl.EnumInfo, 5) +var file_provisionersdk_proto_provisioner_proto_enumTypes = make([]protoimpl.EnumInfo, 6) var file_provisionersdk_proto_provisioner_proto_msgTypes = make([]protoimpl.MessageInfo, 42) var file_provisionersdk_proto_provisioner_proto_goTypes = []interface{}{ (LogLevel)(0), // 0: provisioner.LogLevel (AppSharingLevel)(0), // 1: provisioner.AppSharingLevel (AppOpenIn)(0), // 2: provisioner.AppOpenIn (WorkspaceTransition)(0), // 3: provisioner.WorkspaceTransition - (TimingState)(0), // 4: provisioner.TimingState - (*Empty)(nil), // 5: provisioner.Empty - (*TemplateVariable)(nil), // 6: provisioner.TemplateVariable - (*RichParameterOption)(nil), // 7: provisioner.RichParameterOption - (*RichParameter)(nil), // 8: provisioner.RichParameter - (*RichParameterValue)(nil), // 9: provisioner.RichParameterValue - (*Prebuild)(nil), // 10: provisioner.Prebuild - (*Preset)(nil), // 11: provisioner.Preset - (*PresetParameter)(nil), // 12: provisioner.PresetParameter - (*VariableValue)(nil), // 13: provisioner.VariableValue - (*Log)(nil), // 14: provisioner.Log - (*InstanceIdentityAuth)(nil), // 15: provisioner.InstanceIdentityAuth - (*ExternalAuthProviderResource)(nil), // 16: provisioner.ExternalAuthProviderResource - (*ExternalAuthProvider)(nil), // 17: provisioner.ExternalAuthProvider - (*Agent)(nil), // 18: provisioner.Agent - (*ResourcesMonitoring)(nil), // 19: provisioner.ResourcesMonitoring - (*MemoryResourceMonitor)(nil), // 20: provisioner.MemoryResourceMonitor - (*VolumeResourceMonitor)(nil), // 21: provisioner.VolumeResourceMonitor - (*DisplayApps)(nil), // 22: provisioner.DisplayApps - (*Env)(nil), // 23: provisioner.Env - (*Script)(nil), // 24: provisioner.Script - (*Devcontainer)(nil), // 25: provisioner.Devcontainer - (*App)(nil), // 26: provisioner.App - (*Healthcheck)(nil), // 27: provisioner.Healthcheck - (*Resource)(nil), // 28: provisioner.Resource - (*Module)(nil), // 29: provisioner.Module - (*Role)(nil), // 30: provisioner.Role - (*Metadata)(nil), // 31: provisioner.Metadata - (*Config)(nil), // 32: provisioner.Config - (*ParseRequest)(nil), // 33: provisioner.ParseRequest - (*ParseComplete)(nil), // 34: provisioner.ParseComplete - (*PlanRequest)(nil), // 35: provisioner.PlanRequest - (*PlanComplete)(nil), // 36: provisioner.PlanComplete - (*ApplyRequest)(nil), // 37: provisioner.ApplyRequest - (*ApplyComplete)(nil), // 38: provisioner.ApplyComplete - (*Timing)(nil), // 39: provisioner.Timing - (*CancelRequest)(nil), // 40: provisioner.CancelRequest - (*Request)(nil), // 41: provisioner.Request - (*Response)(nil), // 42: provisioner.Response - (*Agent_Metadata)(nil), // 43: provisioner.Agent.Metadata - nil, // 44: provisioner.Agent.EnvEntry - (*Resource_Metadata)(nil), // 45: provisioner.Resource.Metadata - nil, // 46: provisioner.ParseComplete.WorkspaceTagsEntry - (*timestamppb.Timestamp)(nil), // 47: google.protobuf.Timestamp + (PrebuiltWorkspaceBuildStage)(0), // 4: provisioner.PrebuiltWorkspaceBuildStage + (TimingState)(0), // 5: provisioner.TimingState + (*Empty)(nil), // 6: provisioner.Empty + (*TemplateVariable)(nil), // 7: provisioner.TemplateVariable + (*RichParameterOption)(nil), // 8: provisioner.RichParameterOption + (*RichParameter)(nil), // 9: provisioner.RichParameter + (*RichParameterValue)(nil), // 10: provisioner.RichParameterValue + (*Prebuild)(nil), // 11: provisioner.Prebuild + (*Preset)(nil), // 12: provisioner.Preset + (*PresetParameter)(nil), // 13: provisioner.PresetParameter + (*VariableValue)(nil), // 14: provisioner.VariableValue + (*Log)(nil), // 15: provisioner.Log + (*InstanceIdentityAuth)(nil), // 16: provisioner.InstanceIdentityAuth + (*ExternalAuthProviderResource)(nil), // 17: provisioner.ExternalAuthProviderResource + (*ExternalAuthProvider)(nil), // 18: provisioner.ExternalAuthProvider + (*Agent)(nil), // 19: provisioner.Agent + (*ResourcesMonitoring)(nil), // 20: provisioner.ResourcesMonitoring + (*MemoryResourceMonitor)(nil), // 21: provisioner.MemoryResourceMonitor + (*VolumeResourceMonitor)(nil), // 22: provisioner.VolumeResourceMonitor + (*DisplayApps)(nil), // 23: provisioner.DisplayApps + (*Env)(nil), // 24: provisioner.Env + (*Script)(nil), // 25: provisioner.Script + (*Devcontainer)(nil), // 26: provisioner.Devcontainer + (*App)(nil), // 27: provisioner.App + (*Healthcheck)(nil), // 28: provisioner.Healthcheck + (*Resource)(nil), // 29: provisioner.Resource + (*Module)(nil), // 30: provisioner.Module + (*Role)(nil), // 31: provisioner.Role + (*Metadata)(nil), // 32: provisioner.Metadata + (*Config)(nil), // 33: provisioner.Config + (*ParseRequest)(nil), // 34: provisioner.ParseRequest + (*ParseComplete)(nil), // 35: provisioner.ParseComplete + (*PlanRequest)(nil), // 36: provisioner.PlanRequest + (*PlanComplete)(nil), // 37: provisioner.PlanComplete + (*ApplyRequest)(nil), // 38: provisioner.ApplyRequest + (*ApplyComplete)(nil), // 39: provisioner.ApplyComplete + (*Timing)(nil), // 40: provisioner.Timing + (*CancelRequest)(nil), // 41: provisioner.CancelRequest + (*Request)(nil), // 42: provisioner.Request + (*Response)(nil), // 43: provisioner.Response + (*Agent_Metadata)(nil), // 44: provisioner.Agent.Metadata + nil, // 45: provisioner.Agent.EnvEntry + (*Resource_Metadata)(nil), // 46: provisioner.Resource.Metadata + nil, // 47: provisioner.ParseComplete.WorkspaceTagsEntry + (*timestamppb.Timestamp)(nil), // 48: google.protobuf.Timestamp } var file_provisionersdk_proto_provisioner_proto_depIdxs = []int32{ - 7, // 0: provisioner.RichParameter.options:type_name -> provisioner.RichParameterOption - 12, // 1: provisioner.Preset.parameters:type_name -> provisioner.PresetParameter - 10, // 2: provisioner.Preset.prebuild:type_name -> provisioner.Prebuild + 8, // 0: provisioner.RichParameter.options:type_name -> provisioner.RichParameterOption + 13, // 1: provisioner.Preset.parameters:type_name -> provisioner.PresetParameter + 11, // 2: provisioner.Preset.prebuild:type_name -> provisioner.Prebuild 0, // 3: provisioner.Log.level:type_name -> provisioner.LogLevel - 44, // 4: provisioner.Agent.env:type_name -> provisioner.Agent.EnvEntry - 26, // 5: provisioner.Agent.apps:type_name -> provisioner.App - 43, // 6: provisioner.Agent.metadata:type_name -> provisioner.Agent.Metadata - 22, // 7: provisioner.Agent.display_apps:type_name -> provisioner.DisplayApps - 24, // 8: provisioner.Agent.scripts:type_name -> provisioner.Script - 23, // 9: provisioner.Agent.extra_envs:type_name -> provisioner.Env - 19, // 10: provisioner.Agent.resources_monitoring:type_name -> provisioner.ResourcesMonitoring - 25, // 11: provisioner.Agent.devcontainers:type_name -> provisioner.Devcontainer - 20, // 12: provisioner.ResourcesMonitoring.memory:type_name -> provisioner.MemoryResourceMonitor - 21, // 13: provisioner.ResourcesMonitoring.volumes:type_name -> provisioner.VolumeResourceMonitor - 27, // 14: provisioner.App.healthcheck:type_name -> provisioner.Healthcheck + 45, // 4: provisioner.Agent.env:type_name -> provisioner.Agent.EnvEntry + 27, // 5: provisioner.Agent.apps:type_name -> provisioner.App + 44, // 6: provisioner.Agent.metadata:type_name -> provisioner.Agent.Metadata + 23, // 7: provisioner.Agent.display_apps:type_name -> provisioner.DisplayApps + 25, // 8: provisioner.Agent.scripts:type_name -> provisioner.Script + 24, // 9: provisioner.Agent.extra_envs:type_name -> provisioner.Env + 20, // 10: provisioner.Agent.resources_monitoring:type_name -> provisioner.ResourcesMonitoring + 26, // 11: provisioner.Agent.devcontainers:type_name -> provisioner.Devcontainer + 21, // 12: provisioner.ResourcesMonitoring.memory:type_name -> provisioner.MemoryResourceMonitor + 22, // 13: provisioner.ResourcesMonitoring.volumes:type_name -> provisioner.VolumeResourceMonitor + 28, // 14: provisioner.App.healthcheck:type_name -> provisioner.Healthcheck 1, // 15: provisioner.App.sharing_level:type_name -> provisioner.AppSharingLevel 2, // 16: provisioner.App.open_in:type_name -> provisioner.AppOpenIn - 18, // 17: provisioner.Resource.agents:type_name -> provisioner.Agent - 45, // 18: provisioner.Resource.metadata:type_name -> provisioner.Resource.Metadata + 19, // 17: provisioner.Resource.agents:type_name -> provisioner.Agent + 46, // 18: provisioner.Resource.metadata:type_name -> provisioner.Resource.Metadata 3, // 19: provisioner.Metadata.workspace_transition:type_name -> provisioner.WorkspaceTransition - 30, // 20: provisioner.Metadata.workspace_owner_rbac_roles:type_name -> provisioner.Role - 6, // 21: provisioner.ParseComplete.template_variables:type_name -> provisioner.TemplateVariable - 46, // 22: provisioner.ParseComplete.workspace_tags:type_name -> provisioner.ParseComplete.WorkspaceTagsEntry - 31, // 23: provisioner.PlanRequest.metadata:type_name -> provisioner.Metadata - 9, // 24: provisioner.PlanRequest.rich_parameter_values:type_name -> provisioner.RichParameterValue - 13, // 25: provisioner.PlanRequest.variable_values:type_name -> provisioner.VariableValue - 17, // 26: provisioner.PlanRequest.external_auth_providers:type_name -> provisioner.ExternalAuthProvider - 28, // 27: provisioner.PlanComplete.resources:type_name -> provisioner.Resource - 8, // 28: provisioner.PlanComplete.parameters:type_name -> provisioner.RichParameter - 16, // 29: provisioner.PlanComplete.external_auth_providers:type_name -> provisioner.ExternalAuthProviderResource - 39, // 30: provisioner.PlanComplete.timings:type_name -> provisioner.Timing - 29, // 31: provisioner.PlanComplete.modules:type_name -> provisioner.Module - 11, // 32: provisioner.PlanComplete.presets:type_name -> provisioner.Preset - 31, // 33: provisioner.ApplyRequest.metadata:type_name -> provisioner.Metadata - 28, // 34: provisioner.ApplyComplete.resources:type_name -> provisioner.Resource - 8, // 35: provisioner.ApplyComplete.parameters:type_name -> provisioner.RichParameter - 16, // 36: provisioner.ApplyComplete.external_auth_providers:type_name -> provisioner.ExternalAuthProviderResource - 39, // 37: provisioner.ApplyComplete.timings:type_name -> provisioner.Timing - 47, // 38: provisioner.Timing.start:type_name -> google.protobuf.Timestamp - 47, // 39: provisioner.Timing.end:type_name -> google.protobuf.Timestamp - 4, // 40: provisioner.Timing.state:type_name -> provisioner.TimingState - 32, // 41: provisioner.Request.config:type_name -> provisioner.Config - 33, // 42: provisioner.Request.parse:type_name -> provisioner.ParseRequest - 35, // 43: provisioner.Request.plan:type_name -> provisioner.PlanRequest - 37, // 44: provisioner.Request.apply:type_name -> provisioner.ApplyRequest - 40, // 45: provisioner.Request.cancel:type_name -> provisioner.CancelRequest - 14, // 46: provisioner.Response.log:type_name -> provisioner.Log - 34, // 47: provisioner.Response.parse:type_name -> provisioner.ParseComplete - 36, // 48: provisioner.Response.plan:type_name -> provisioner.PlanComplete - 38, // 49: provisioner.Response.apply:type_name -> provisioner.ApplyComplete - 41, // 50: provisioner.Provisioner.Session:input_type -> provisioner.Request - 42, // 51: provisioner.Provisioner.Session:output_type -> provisioner.Response - 51, // [51:52] is the sub-list for method output_type - 50, // [50:51] 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 + 31, // 20: provisioner.Metadata.workspace_owner_rbac_roles:type_name -> provisioner.Role + 4, // 21: provisioner.Metadata.prebuilt_workspace_build_stage:type_name -> provisioner.PrebuiltWorkspaceBuildStage + 7, // 22: provisioner.ParseComplete.template_variables:type_name -> provisioner.TemplateVariable + 47, // 23: provisioner.ParseComplete.workspace_tags:type_name -> provisioner.ParseComplete.WorkspaceTagsEntry + 32, // 24: provisioner.PlanRequest.metadata:type_name -> provisioner.Metadata + 10, // 25: provisioner.PlanRequest.rich_parameter_values:type_name -> provisioner.RichParameterValue + 14, // 26: provisioner.PlanRequest.variable_values:type_name -> provisioner.VariableValue + 18, // 27: provisioner.PlanRequest.external_auth_providers:type_name -> provisioner.ExternalAuthProvider + 29, // 28: provisioner.PlanComplete.resources:type_name -> provisioner.Resource + 9, // 29: provisioner.PlanComplete.parameters:type_name -> provisioner.RichParameter + 17, // 30: provisioner.PlanComplete.external_auth_providers:type_name -> provisioner.ExternalAuthProviderResource + 40, // 31: provisioner.PlanComplete.timings:type_name -> provisioner.Timing + 30, // 32: provisioner.PlanComplete.modules:type_name -> provisioner.Module + 12, // 33: provisioner.PlanComplete.presets:type_name -> provisioner.Preset + 32, // 34: provisioner.ApplyRequest.metadata:type_name -> provisioner.Metadata + 29, // 35: provisioner.ApplyComplete.resources:type_name -> provisioner.Resource + 9, // 36: provisioner.ApplyComplete.parameters:type_name -> provisioner.RichParameter + 17, // 37: provisioner.ApplyComplete.external_auth_providers:type_name -> provisioner.ExternalAuthProviderResource + 40, // 38: provisioner.ApplyComplete.timings:type_name -> provisioner.Timing + 48, // 39: provisioner.Timing.start:type_name -> google.protobuf.Timestamp + 48, // 40: provisioner.Timing.end:type_name -> google.protobuf.Timestamp + 5, // 41: provisioner.Timing.state:type_name -> provisioner.TimingState + 33, // 42: provisioner.Request.config:type_name -> provisioner.Config + 34, // 43: provisioner.Request.parse:type_name -> provisioner.ParseRequest + 36, // 44: provisioner.Request.plan:type_name -> provisioner.PlanRequest + 38, // 45: provisioner.Request.apply:type_name -> provisioner.ApplyRequest + 41, // 46: provisioner.Request.cancel:type_name -> provisioner.CancelRequest + 15, // 47: provisioner.Response.log:type_name -> provisioner.Log + 35, // 48: provisioner.Response.parse:type_name -> provisioner.ParseComplete + 37, // 49: provisioner.Response.plan:type_name -> provisioner.PlanComplete + 39, // 50: provisioner.Response.apply:type_name -> provisioner.ApplyComplete + 42, // 51: provisioner.Provisioner.Session:input_type -> provisioner.Request + 43, // 52: provisioner.Provisioner.Session:output_type -> provisioner.Response + 52, // [52:53] is the sub-list for method output_type + 51, // [51:52] is the sub-list for method input_type + 51, // [51:51] is the sub-list for extension type_name + 51, // [51:51] is the sub-list for extension extendee + 0, // [0:51] is the sub-list for field type_name } func init() { file_provisionersdk_proto_provisioner_proto_init() } @@ -4682,7 +4742,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_provisionersdk_proto_provisioner_proto_rawDesc, - NumEnums: 5, + NumEnums: 6, NumMessages: 42, NumExtensions: 0, NumServices: 1, diff --git a/provisionersdk/proto/provisioner.proto b/provisionersdk/proto/provisioner.proto index 3e6841fb24450..9972b3ea148ac 100644 --- a/provisionersdk/proto/provisioner.proto +++ b/provisionersdk/proto/provisioner.proto @@ -272,6 +272,12 @@ message Role { string org_id = 2; } +enum PrebuiltWorkspaceBuildStage { + NONE = 0; // Default value for builds unrelated to prebuilds. + CREATE = 1; // A prebuilt workspace is being provisioned. + CLAIM = 2; // A prebuilt workspace is being claimed. +} + // Metadata is information about a workspace used in the execution of a build message Metadata { string coder_url = 1; @@ -293,8 +299,8 @@ message Metadata { string workspace_build_id = 17; string workspace_owner_login_type = 18; repeated Role workspace_owner_rbac_roles = 19; - bool is_prebuild = 20; - string running_workspace_agent_token = 21; + PrebuiltWorkspaceBuildStage prebuilt_workspace_build_stage = 20; // Indicates that a prebuilt workspace is being built. + string running_workspace_agent_token = 21; // Preserves the running agent token of a prebuilt workspace so it can reinitialize. } // Config represents execution configuration shared by all subsequent requests in the Session diff --git a/site/e2e/provisionerGenerated.ts b/site/e2e/provisionerGenerated.ts index cea6f9cb364af..4090017996598 100644 --- a/site/e2e/provisionerGenerated.ts +++ b/site/e2e/provisionerGenerated.ts @@ -38,6 +38,16 @@ export enum WorkspaceTransition { UNRECOGNIZED = -1, } +export enum PrebuiltWorkspaceBuildStage { + /** NONE - Default value for builds unrelated to prebuilds. */ + NONE = 0, + /** CREATE - A prebuilt workspace is being provisioned. */ + CREATE = 1, + /** CLAIM - A prebuilt workspace is being claimed. */ + CLAIM = 2, + UNRECOGNIZED = -1, +} + export enum TimingState { STARTED = 0, COMPLETED = 1, @@ -307,7 +317,9 @@ export interface Metadata { workspaceBuildId: string; workspaceOwnerLoginType: string; workspaceOwnerRbacRoles: Role[]; - isPrebuild: boolean; + /** Indicates that a prebuilt workspace is being built. */ + prebuiltWorkspaceBuildStage: PrebuiltWorkspaceBuildStage; + /** Preserves the running agent token of a prebuilt workspace so it can reinitialize. */ runningWorkspaceAgentToken: string; } @@ -1027,8 +1039,8 @@ export const Metadata = { for (const v of message.workspaceOwnerRbacRoles) { Role.encode(v!, writer.uint32(154).fork()).ldelim(); } - if (message.isPrebuild === true) { - writer.uint32(160).bool(message.isPrebuild); + if (message.prebuiltWorkspaceBuildStage !== 0) { + writer.uint32(160).int32(message.prebuiltWorkspaceBuildStage); } if (message.runningWorkspaceAgentToken !== "") { writer.uint32(170).string(message.runningWorkspaceAgentToken); From 37832413baa13e13cdc882bd7135924f79560939 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 12 May 2025 10:31:38 -0500 Subject: [PATCH 08/88] chore: resolve internal drpc package conflict (#17770) Our internal drpc package name conflicts with the external one in usage. `drpc.*` == external `drpcsdk.*` == internal --- agent/agenttest/client.go | 2 +- cli/server.go | 6 +++--- coderd/coderd.go | 4 ++-- coderd/coderdtest/coderdtest.go | 4 ++-- coderd/provisionerdserver/provisionerdserver.go | 6 +++--- codersdk/agentsdk/agentsdk.go | 2 +- codersdk/{drpc => drpcsdk}/transport.go | 2 +- codersdk/provisionerdaemons.go | 4 ++-- enterprise/cli/provisionerdaemonstart.go | 4 ++-- enterprise/coderd/coderdenttest/coderdenttest.go | 4 ++-- enterprise/coderd/provisionerdaemons_test.go | 4 ++-- provisioner/echo/serve_test.go | 4 ++-- provisioner/terraform/provision_test.go | 4 ++-- provisionerd/provisionerd_test.go | 6 +++--- provisionersdk/serve_test.go | 6 +++--- tailnet/client.go | 4 ++-- 16 files changed, 33 insertions(+), 33 deletions(-) rename codersdk/{drpc => drpcsdk}/transport.go (99%) diff --git a/agent/agenttest/client.go b/agent/agenttest/client.go index a1d14e32a2c55..73fb40e826519 100644 --- a/agent/agenttest/client.go +++ b/agent/agenttest/client.go @@ -24,7 +24,7 @@ import ( agentproto "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" - drpcsdk "github.com/coder/coder/v2/codersdk/drpc" + "github.com/coder/coder/v2/codersdk/drpcsdk" "github.com/coder/coder/v2/tailnet" "github.com/coder/coder/v2/tailnet/proto" "github.com/coder/coder/v2/testutil" diff --git a/cli/server.go b/cli/server.go index 48ec8492f0a55..d32ed51c06007 100644 --- a/cli/server.go +++ b/cli/server.go @@ -66,6 +66,7 @@ import ( "github.com/coder/coder/v2/coderd/notifications/reports" "github.com/coder/coder/v2/coderd/runtimeconfig" "github.com/coder/coder/v2/coderd/webpush" + "github.com/coder/coder/v2/codersdk/drpcsdk" "github.com/coder/coder/v2/buildinfo" "github.com/coder/coder/v2/cli/clilog" @@ -102,7 +103,6 @@ import ( "github.com/coder/coder/v2/coderd/workspaceapps/appurl" "github.com/coder/coder/v2/coderd/workspacestats" "github.com/coder/coder/v2/codersdk" - "github.com/coder/coder/v2/codersdk/drpc" "github.com/coder/coder/v2/cryptorand" "github.com/coder/coder/v2/provisioner/echo" "github.com/coder/coder/v2/provisioner/terraform" @@ -1447,7 +1447,7 @@ func newProvisionerDaemon( for _, provisionerType := range provisionerTypes { switch provisionerType { case codersdk.ProvisionerTypeEcho: - echoClient, echoServer := drpc.MemTransportPipe() + echoClient, echoServer := drpcsdk.MemTransportPipe() wg.Add(1) go func() { defer wg.Done() @@ -1481,7 +1481,7 @@ func newProvisionerDaemon( } tracer := coderAPI.TracerProvider.Tracer(tracing.TracerName) - terraformClient, terraformServer := drpc.MemTransportPipe() + terraformClient, terraformServer := drpcsdk.MemTransportPipe() wg.Add(1) go func() { defer wg.Done() diff --git a/coderd/coderd.go b/coderd/coderd.go index d0d3471cdd771..1b4b5746b7f7e 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -84,7 +84,7 @@ import ( "github.com/coder/coder/v2/coderd/workspaceapps" "github.com/coder/coder/v2/coderd/workspacestats" "github.com/coder/coder/v2/codersdk" - "github.com/coder/coder/v2/codersdk/drpc" + "github.com/coder/coder/v2/codersdk/drpcsdk" "github.com/coder/coder/v2/codersdk/healthsdk" "github.com/coder/coder/v2/provisionerd/proto" "github.com/coder/coder/v2/provisionersdk" @@ -1725,7 +1725,7 @@ func (api *API) CreateInMemoryProvisionerDaemon(dialCtx context.Context, name st func (api *API) CreateInMemoryTaggedProvisionerDaemon(dialCtx context.Context, name string, provisionerTypes []codersdk.ProvisionerType, provisionerTags map[string]string) (client proto.DRPCProvisionerDaemonClient, err error) { tracer := api.TracerProvider.Tracer(tracing.TracerName) - clientSession, serverSession := drpc.MemTransportPipe() + clientSession, serverSession := drpcsdk.MemTransportPipe() defer func() { if err != nil { _ = clientSession.Close() diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index dbf1f62abfb28..e843d0d748578 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -84,7 +84,7 @@ import ( "github.com/coder/coder/v2/coderd/workspacestats" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" - "github.com/coder/coder/v2/codersdk/drpc" + "github.com/coder/coder/v2/codersdk/drpcsdk" "github.com/coder/coder/v2/codersdk/healthsdk" "github.com/coder/coder/v2/cryptorand" "github.com/coder/coder/v2/provisioner/echo" @@ -657,7 +657,7 @@ func NewTaggedProvisionerDaemon(t testing.TB, coderAPI *coderd.API, name string, // seems t.TempDir() is not safe to call from a different goroutine workDir := t.TempDir() - echoClient, echoServer := drpc.MemTransportPipe() + echoClient, echoServer := drpcsdk.MemTransportPipe() ctx, cancelFunc := context.WithCancel(context.Background()) t.Cleanup(func() { _ = echoClient.Close() diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 68aece517bb2f..a6325eecfd44a 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -26,6 +26,7 @@ import ( protobuf "google.golang.org/protobuf/proto" "cdr.dev/slog" + "github.com/coder/coder/v2/codersdk/drpcsdk" "github.com/coder/quartz" @@ -43,7 +44,6 @@ import ( "github.com/coder/coder/v2/coderd/tracing" "github.com/coder/coder/v2/coderd/wspubsub" "github.com/coder/coder/v2/codersdk" - "github.com/coder/coder/v2/codersdk/drpc" "github.com/coder/coder/v2/provisioner" "github.com/coder/coder/v2/provisionerd/proto" "github.com/coder/coder/v2/provisionersdk" @@ -707,8 +707,8 @@ func (s *server) acquireProtoJob(ctx context.Context, job database.ProvisionerJo default: return nil, failJob(fmt.Sprintf("unsupported storage method: %s", job.StorageMethod)) } - if protobuf.Size(protoJob) > drpc.MaxMessageSize { - return nil, failJob(fmt.Sprintf("payload was too big: %d > %d", protobuf.Size(protoJob), drpc.MaxMessageSize)) + if protobuf.Size(protoJob) > drpcsdk.MaxMessageSize { + return nil, failJob(fmt.Sprintf("payload was too big: %d > %d", protobuf.Size(protoJob), drpcsdk.MaxMessageSize)) } return protoJob, err diff --git a/codersdk/agentsdk/agentsdk.go b/codersdk/agentsdk/agentsdk.go index 109d14b84d050..8a7ed4d525af4 100644 --- a/codersdk/agentsdk/agentsdk.go +++ b/codersdk/agentsdk/agentsdk.go @@ -22,7 +22,7 @@ import ( "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/apiversion" "github.com/coder/coder/v2/codersdk" - drpcsdk "github.com/coder/coder/v2/codersdk/drpc" + "github.com/coder/coder/v2/codersdk/drpcsdk" tailnetproto "github.com/coder/coder/v2/tailnet/proto" "github.com/coder/websocket" ) diff --git a/codersdk/drpc/transport.go b/codersdk/drpcsdk/transport.go similarity index 99% rename from codersdk/drpc/transport.go rename to codersdk/drpcsdk/transport.go index 55ab521afc17d..84cef5e6d8db1 100644 --- a/codersdk/drpc/transport.go +++ b/codersdk/drpcsdk/transport.go @@ -1,4 +1,4 @@ -package drpc +package drpcsdk import ( "context" diff --git a/codersdk/provisionerdaemons.go b/codersdk/provisionerdaemons.go index 014a68bbce72e..11345a115e07f 100644 --- a/codersdk/provisionerdaemons.go +++ b/codersdk/provisionerdaemons.go @@ -17,7 +17,7 @@ import ( "golang.org/x/xerrors" "github.com/coder/coder/v2/buildinfo" - "github.com/coder/coder/v2/codersdk/drpc" + "github.com/coder/coder/v2/codersdk/drpcsdk" "github.com/coder/coder/v2/codersdk/wsjson" "github.com/coder/coder/v2/provisionerd/proto" "github.com/coder/coder/v2/provisionerd/runner" @@ -332,7 +332,7 @@ func (c *Client) ServeProvisionerDaemon(ctx context.Context, req ServeProvisione _ = wsNetConn.Close() return nil, xerrors.Errorf("multiplex client: %w", err) } - return proto.NewDRPCProvisionerDaemonClient(drpc.MultiplexedConn(session)), nil + return proto.NewDRPCProvisionerDaemonClient(drpcsdk.MultiplexedConn(session)), nil } type ProvisionerKeyTags map[string]string diff --git a/enterprise/cli/provisionerdaemonstart.go b/enterprise/cli/provisionerdaemonstart.go index e0b3e00c63ece..582e14e1c8adc 100644 --- a/enterprise/cli/provisionerdaemonstart.go +++ b/enterprise/cli/provisionerdaemonstart.go @@ -25,7 +25,7 @@ import ( "github.com/coder/coder/v2/cli/cliutil" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/codersdk" - "github.com/coder/coder/v2/codersdk/drpc" + "github.com/coder/coder/v2/codersdk/drpcsdk" "github.com/coder/coder/v2/provisioner/terraform" "github.com/coder/coder/v2/provisionerd" provisionerdproto "github.com/coder/coder/v2/provisionerd/proto" @@ -173,7 +173,7 @@ func (r *RootCmd) provisionerDaemonStart() *serpent.Command { return err } - terraformClient, terraformServer := drpc.MemTransportPipe() + terraformClient, terraformServer := drpcsdk.MemTransportPipe() go func() { <-ctx.Done() _ = terraformClient.Close() diff --git a/enterprise/coderd/coderdenttest/coderdenttest.go b/enterprise/coderd/coderdenttest/coderdenttest.go index a72c8c0199695..bd81e5a039599 100644 --- a/enterprise/coderd/coderdenttest/coderdenttest.go +++ b/enterprise/coderd/coderdenttest/coderdenttest.go @@ -25,7 +25,7 @@ import ( "github.com/coder/coder/v2/coderd/database/dbmem" "github.com/coder/coder/v2/coderd/database/pubsub" "github.com/coder/coder/v2/codersdk" - "github.com/coder/coder/v2/codersdk/drpc" + "github.com/coder/coder/v2/codersdk/drpcsdk" "github.com/coder/coder/v2/enterprise/coderd" "github.com/coder/coder/v2/enterprise/coderd/license" "github.com/coder/coder/v2/enterprise/dbcrypt" @@ -344,7 +344,7 @@ func newExternalProvisionerDaemon(t testing.TB, client *codersdk.Client, org uui return nil } - provisionerClient, provisionerSrv := drpc.MemTransportPipe() + provisionerClient, provisionerSrv := drpcsdk.MemTransportPipe() ctx, cancelFunc := context.WithCancel(context.Background()) serveDone := make(chan struct{}) t.Cleanup(func() { diff --git a/enterprise/coderd/provisionerdaemons_test.go b/enterprise/coderd/provisionerdaemons_test.go index a84213f71805f..cdc6267d90971 100644 --- a/enterprise/coderd/provisionerdaemons_test.go +++ b/enterprise/coderd/provisionerdaemons_test.go @@ -25,7 +25,7 @@ import ( "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/codersdk" - "github.com/coder/coder/v2/codersdk/drpc" + "github.com/coder/coder/v2/codersdk/drpcsdk" "github.com/coder/coder/v2/enterprise/coderd/coderdenttest" "github.com/coder/coder/v2/enterprise/coderd/license" "github.com/coder/coder/v2/provisioner/echo" @@ -396,7 +396,7 @@ func TestProvisionerDaemonServe(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - terraformClient, terraformServer := drpc.MemTransportPipe() + terraformClient, terraformServer := drpcsdk.MemTransportPipe() go func() { <-ctx.Done() _ = terraformClient.Close() diff --git a/provisioner/echo/serve_test.go b/provisioner/echo/serve_test.go index dbfdc822eac5a..9168f1be6d22e 100644 --- a/provisioner/echo/serve_test.go +++ b/provisioner/echo/serve_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/coder/coder/v2/codersdk/drpc" + "github.com/coder/coder/v2/codersdk/drpcsdk" "github.com/coder/coder/v2/provisioner/echo" "github.com/coder/coder/v2/provisionersdk" "github.com/coder/coder/v2/provisionersdk/proto" @@ -20,7 +20,7 @@ func TestEcho(t *testing.T) { workdir := t.TempDir() // Create an in-memory provisioner to communicate with. - client, server := drpc.MemTransportPipe() + client, server := drpcsdk.MemTransportPipe() ctx, cancelFunc := context.WithCancel(context.Background()) t.Cleanup(func() { _ = client.Close() diff --git a/provisioner/terraform/provision_test.go b/provisioner/terraform/provision_test.go index ecff965b72984..505fd2df41400 100644 --- a/provisioner/terraform/provision_test.go +++ b/provisioner/terraform/provision_test.go @@ -26,7 +26,7 @@ import ( "cdr.dev/slog" "cdr.dev/slog/sloggers/slogtest" - "github.com/coder/coder/v2/codersdk/drpc" + "github.com/coder/coder/v2/codersdk/drpcsdk" "github.com/coder/coder/v2/provisioner/terraform" "github.com/coder/coder/v2/provisionersdk" "github.com/coder/coder/v2/provisionersdk/proto" @@ -53,7 +53,7 @@ func setupProvisioner(t *testing.T, opts *provisionerServeOptions) (context.Cont logger := testutil.Logger(t) opts.logger = &logger } - client, server := drpc.MemTransportPipe() + client, server := drpcsdk.MemTransportPipe() ctx, cancelFunc := context.WithCancel(context.Background()) serverErr := make(chan error, 1) t.Cleanup(func() { diff --git a/provisionerd/provisionerd_test.go b/provisionerd/provisionerd_test.go index c711e0d4925c8..a9418d9391c8f 100644 --- a/provisionerd/provisionerd_test.go +++ b/provisionerd/provisionerd_test.go @@ -21,7 +21,7 @@ import ( "cdr.dev/slog" "cdr.dev/slog/sloggers/slogtest" - "github.com/coder/coder/v2/codersdk/drpc" + "github.com/coder/coder/v2/codersdk/drpcsdk" "github.com/coder/coder/v2/provisionerd" "github.com/coder/coder/v2/provisionerd/proto" "github.com/coder/coder/v2/provisionersdk" @@ -1107,7 +1107,7 @@ func createProvisionerDaemonClient(t *testing.T, done <-chan struct{}, server pr return &proto.Empty{}, nil } } - clientPipe, serverPipe := drpc.MemTransportPipe() + clientPipe, serverPipe := drpcsdk.MemTransportPipe() t.Cleanup(func() { _ = clientPipe.Close() _ = serverPipe.Close() @@ -1143,7 +1143,7 @@ func createProvisionerDaemonClient(t *testing.T, done <-chan struct{}, server pr // to the server implementation provided. func createProvisionerClient(t *testing.T, done <-chan struct{}, server provisionerTestServer) sdkproto.DRPCProvisionerClient { t.Helper() - clientPipe, serverPipe := drpc.MemTransportPipe() + clientPipe, serverPipe := drpcsdk.MemTransportPipe() t.Cleanup(func() { _ = clientPipe.Close() _ = serverPipe.Close() diff --git a/provisionersdk/serve_test.go b/provisionersdk/serve_test.go index ab6ff8b242de9..a78a573e11b02 100644 --- a/provisionersdk/serve_test.go +++ b/provisionersdk/serve_test.go @@ -10,7 +10,7 @@ import ( "go.uber.org/goleak" "storj.io/drpc/drpcconn" - "github.com/coder/coder/v2/codersdk/drpc" + "github.com/coder/coder/v2/codersdk/drpcsdk" "github.com/coder/coder/v2/provisionersdk" "github.com/coder/coder/v2/provisionersdk/proto" "github.com/coder/coder/v2/testutil" @@ -24,7 +24,7 @@ func TestProvisionerSDK(t *testing.T) { t.Parallel() t.Run("ServeListener", func(t *testing.T) { t.Parallel() - client, server := drpc.MemTransportPipe() + client, server := drpcsdk.MemTransportPipe() defer client.Close() defer server.Close() @@ -66,7 +66,7 @@ func TestProvisionerSDK(t *testing.T) { t.Run("ServeClosedPipe", func(t *testing.T) { t.Parallel() - client, server := drpc.MemTransportPipe() + client, server := drpcsdk.MemTransportPipe() _ = client.Close() _ = server.Close() diff --git a/tailnet/client.go b/tailnet/client.go index 232e534799f13..7712ebc481ef3 100644 --- a/tailnet/client.go +++ b/tailnet/client.go @@ -8,7 +8,7 @@ import ( "golang.org/x/xerrors" "cdr.dev/slog" - "github.com/coder/coder/v2/codersdk/drpc" + "github.com/coder/coder/v2/codersdk/drpcsdk" "github.com/coder/coder/v2/tailnet/proto" ) @@ -20,5 +20,5 @@ func NewDRPCClient(conn net.Conn, logger slog.Logger) (proto.DRPCTailnetClient, if err != nil { return nil, xerrors.Errorf("multiplex client: %w", err) } - return proto.NewDRPCTailnetClient(drpc.MultiplexedConn(session)), nil + return proto.NewDRPCTailnetClient(drpcsdk.MultiplexedConn(session)), nil } From ea2cae0e20b38bd2738adf3542091892aaab9582 Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Mon, 12 May 2025 17:38:25 +0200 Subject: [PATCH 09/88] chore: tune postgres CI tests (#17756) Changes: - use a bigger runner for test-go-pg on Linux - use a depot runner to run postgres tests on Windows - use the same Windows ramdisk action for postgres tests as the one currently used for in-memory tests - put GOTMPDIR on a ramdisk on Windows - tune the number of tests running in parallel on macOS and Windows - use a ramdisk for postgres on macOS - turn off Spotlight indexing on macOS - rerun failing tests to stop flakes from disrupting developers Results: - test-go-pg on Linux completing in 50% of the time it takes to run on main ([run on main](https://github.com/coder/coder/actions/runs/14937632073/job/41968714750), [run on this PR](https://github.com/coder/coder/actions/runs/14956584795/job/42013097674?pr=17756)) - macOS tests completing in 70% of the time ([run on main](https://github.com/coder/coder/actions/runs/14921155015/job/41916639889), [run on this PR](https://github.com/coder/coder/actions/runs/14956590940/job/42013102975)) - Windows tests completing in 50% of the time ([run on main](https://github.com/coder/coder/actions/runs/14921155015/job/41916640058), [run on this PR](https://github.com/coder/coder/actions/runs/14956590940/job/42013103116)) This PR helps unblock https://github.com/coder/coder/issues/15109. --- .github/actions/setup-go/action.yaml | 4 +- .github/workflows/ci.yaml | 2 +- .github/workflows/nightly-gauntlet.yaml | 75 +++++++++++++++++++------ 3 files changed, 62 insertions(+), 19 deletions(-) diff --git a/.github/actions/setup-go/action.yaml b/.github/actions/setup-go/action.yaml index e13e019554a39..4d91a9b2acb07 100644 --- a/.github/actions/setup-go/action.yaml +++ b/.github/actions/setup-go/action.yaml @@ -26,13 +26,15 @@ runs: export GOCACHE_DIR="$RUNNER_TEMP""\go-cache" export GOMODCACHE_DIR="$RUNNER_TEMP""\go-mod-cache" export GOPATH_DIR="$RUNNER_TEMP""\go-path" + export GOTMP_DIR="$RUNNER_TEMP""\go-tmp" mkdir -p "$GOCACHE_DIR" mkdir -p "$GOMODCACHE_DIR" mkdir -p "$GOPATH_DIR" + mkdir -p "$GOTMP_DIR" go env -w GOCACHE="$GOCACHE_DIR" go env -w GOMODCACHE="$GOMODCACHE_DIR" go env -w GOPATH="$GOPATH_DIR" - + go env -w GOTMPDIR="$GOTMP_DIR" - name: Setup Go uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fea76c03c1a4f..f27885314b8e7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -454,7 +454,7 @@ jobs: api-key: ${{ secrets.DATADOG_API_KEY }} test-go-pg: - runs-on: ${{ matrix.os == 'ubuntu-latest' && github.repository_owner == 'coder' && 'depot-ubuntu-22.04-4' || matrix.os }} + runs-on: ${{ matrix.os == 'ubuntu-latest' && github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || matrix.os }} needs: changes if: needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main' # This timeout must be greater than the timeout set by `go test` in diff --git a/.github/workflows/nightly-gauntlet.yaml b/.github/workflows/nightly-gauntlet.yaml index d12a988ca095d..64b520d07ba6e 100644 --- a/.github/workflows/nightly-gauntlet.yaml +++ b/.github/workflows/nightly-gauntlet.yaml @@ -12,8 +12,9 @@ permissions: jobs: test-go-pg: - runs-on: ${{ matrix.os == 'macos-latest' && github.repository_owner == 'coder' && 'depot-macos-latest' || matrix.os == 'windows-2022' && github.repository_owner == 'coder' && 'windows-latest-16-cores' || matrix.os }} - if: github.ref == 'refs/heads/main' + # make sure to adjust NUM_PARALLEL_PACKAGES and NUM_PARALLEL_TESTS below + # when changing runner sizes + runs-on: ${{ matrix.os == 'macos-latest' && github.repository_owner == 'coder' && 'depot-macos-latest' || matrix.os == 'windows-2022' && github.repository_owner == 'coder' && 'depot-windows-2022-16' || matrix.os }} # This timeout must be greater than the timeout set by `go test` in # `make test-postgres` to ensure we receive a trace of running # goroutines. Setting this to the timeout +5m should work quite well @@ -31,6 +32,22 @@ jobs: with: egress-policy: audit + # macOS indexes all new files in the background. Our Postgres tests + # create and destroy thousands of databases on disk, and Spotlight + # tries to index all of them, seriously slowing down the tests. + - name: Disable Spotlight Indexing + if: runner.os == 'macOS' + run: | + sudo mdutil -a -i off + sudo mdutil -X / + sudo launchctl bootout system /System/Library/LaunchDaemons/com.apple.metadata.mds.plist + + # Set up RAM disks to speed up the rest of the job. This action is in + # a separate repository to allow its use before actions/checkout. + - name: Setup RAM Disks + if: runner.os == 'Windows' + uses: coder/setup-ramdisk-action@79dacfe70c47ad6d6c0dd7f45412368802641439 + - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: @@ -38,15 +55,16 @@ jobs: - name: Setup Go uses: ./.github/actions/setup-go + with: + # Runners have Go baked-in and Go will automatically + # download the toolchain configured in go.mod, so we don't + # need to reinstall it. It's faster on Windows runners. + use-preinstalled-go: ${{ runner.os == 'Windows' }} + use-temp-cache-dirs: ${{ runner.os == 'Windows' }} - name: Setup Terraform uses: ./.github/actions/setup-tf - # Sets up the ImDisk toolkit for Windows and creates a RAM disk on drive R:. - - name: Setup ImDisk - if: runner.os == 'Windows' - uses: ./.github/actions/setup-imdisk - - name: Test with PostgreSQL Database env: POSTGRES_VERSION: "13" @@ -55,6 +73,19 @@ jobs: LC_ALL: "en_US.UTF-8" shell: bash run: | + if [ "${{ runner.os }}" == "Windows" ]; then + # Create a temp dir on the R: ramdisk drive for Windows. The default + # C: drive is extremely slow: https://github.com/actions/runner-images/issues/8755 + mkdir -p "R:/temp/embedded-pg" + go run scripts/embedded-pg/main.go -path "R:/temp/embedded-pg" + fi + if [ "${{ runner.os }}" == "macOS" ]; then + # Postgres runs faster on a ramdisk on macOS too + mkdir -p /tmp/tmpfs + sudo mount_tmpfs -o noowners -s 8g /tmp/tmpfs + go run scripts/embedded-pg/main.go -path /tmp/tmpfs/embedded-pg + fi + # if macOS, install google-chrome for scaletests # As another concern, should we really have this kind of external dependency # requirement on standard CI? @@ -72,19 +103,29 @@ jobs: touch ~/.bash_profile && echo "export BASH_SILENCE_DEPRECATION_WARNING=1" >> ~/.bash_profile fi + # Golang's default for these 2 variables is the number of logical CPUs. + # Our Windows and Linux runners have 16 cores, so they match up there. + NUM_PARALLEL_PACKAGES=16 + NUM_PARALLEL_TESTS=16 if [ "${{ runner.os }}" == "Windows" ]; then - # Create a temp dir on the R: ramdisk drive for Windows. The default - # C: drive is extremely slow: https://github.com/actions/runner-images/issues/8755 - mkdir -p "R:/temp/embedded-pg" - go run scripts/embedded-pg/main.go -path "R:/temp/embedded-pg" - else - go run scripts/embedded-pg/main.go + # On Windows Postgres chokes up when we have 16x16=256 tests + # running in parallel, and dbtestutil.NewDB starts to take more than + # 10s to complete sometimes causing test timeouts. With 16x8=128 tests + # Postgres tends not to choke. + NUM_PARALLEL_PACKAGES=8 + fi + if [ "${{ runner.os }}" == "macOS" ]; then + # Our macOS runners have 8 cores. We leave NUM_PARALLEL_TESTS at 16 + # because the tests complete faster and Postgres doesn't choke. It seems + # that macOS's tmpfs is faster than the one on Windows. + NUM_PARALLEL_PACKAGES=8 fi - # Reduce test parallelism, mirroring what we do for race tests. - # We'd been encountering issues with timing related flakes, and - # this seems to help. - DB=ci gotestsum --format standard-quiet -- -v -short -count=1 -parallel 4 -p 4 ./... + # We rerun failing tests to counteract flakiness coming from Postgres + # choking on macOS and Windows sometimes. + DB=ci gotestsum --rerun-fails=2 --rerun-fails-max-failures=1000 \ + --format standard-quiet --packages "./..." \ + -- -v -p $NUM_PARALLEL_PACKAGES -parallel=$NUM_PARALLEL_TESTS -count=1 - name: Upload test stats to Datadog timeout-minutes: 1 From e0dd50d7fbc613e017dd153fb48b82dd5fa2a4bc Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Mon, 12 May 2025 17:15:24 +0100 Subject: [PATCH 10/88] chore(cli): fix test flake in TestExpMcpServer (#17772) Test was failing inside a Coder workspace. --- cli/exp_mcp_test.go | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/cli/exp_mcp_test.go b/cli/exp_mcp_test.go index db60eb898ed85..2d9a0475b0452 100644 --- a/cli/exp_mcp_test.go +++ b/cli/exp_mcp_test.go @@ -133,26 +133,29 @@ func TestExpMcpServer(t *testing.T) { require.Equal(t, 1.0, initializeResponse["id"]) require.NotNil(t, initializeResponse["result"]) }) +} - t.Run("NoCredentials", func(t *testing.T) { - t.Parallel() +func TestExpMcpServerNoCredentials(t *testing.T) { + // Ensure that no credentials are set from the environment. + t.Setenv("CODER_AGENT_TOKEN", "") + t.Setenv("CODER_AGENT_TOKEN_FILE", "") + t.Setenv("CODER_SESSION_TOKEN", "") - ctx := testutil.Context(t, testutil.WaitShort) - cancelCtx, cancel := context.WithCancel(ctx) - t.Cleanup(cancel) + ctx := testutil.Context(t, testutil.WaitShort) + cancelCtx, cancel := context.WithCancel(ctx) + t.Cleanup(cancel) - client := coderdtest.New(t, nil) - inv, root := clitest.New(t, "exp", "mcp", "server") - inv = inv.WithContext(cancelCtx) + client := coderdtest.New(t, nil) + inv, root := clitest.New(t, "exp", "mcp", "server") + inv = inv.WithContext(cancelCtx) - pty := ptytest.New(t) - inv.Stdin = pty.Input() - inv.Stdout = pty.Output() - clitest.SetupConfig(t, client, root) + pty := ptytest.New(t) + inv.Stdin = pty.Input() + inv.Stdout = pty.Output() + clitest.SetupConfig(t, client, root) - err := inv.Run() - assert.ErrorContains(t, err, "are not logged in") - }) + err := inv.Run() + assert.ErrorContains(t, err, "are not logged in") } //nolint:tparallel,paralleltest From 15bd7a3addfba86b1e8ca8e0a36163cb8a25d26d Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Mon, 12 May 2025 13:36:51 -0300 Subject: [PATCH 11/88] chore: replace MUI icons with Lucide icons - 5 (#17750) Replacements: MUI | Lucide OpenInNewOutlined | ExternalLinkIcon HelpOutline | CircleHelpIcon ErrorOutline | CircleAlertIcon --- site/migrate-icons.md | 8 -------- site/src/components/Filter/Filter.tsx | 6 +++--- site/src/components/HelpTooltip/HelpTooltip.tsx | 6 +++--- site/src/components/Latency/Latency.tsx | 6 +++--- .../RichParameterInput/RichParameterInput.tsx | 7 +++++-- .../DeploymentBanner/DeploymentBannerView.tsx | 10 +++++----- site/src/modules/resources/AgentOutdatedTooltip.tsx | 4 ++-- site/src/modules/resources/PortForwardButton.tsx | 9 +++------ .../WorkspaceOutdatedTooltip.tsx | 4 ++-- .../workspaces/WorkspaceTiming/Chart/Tooltip.tsx | 4 ++-- site/src/pages/GroupsPage/GroupPage.tsx | 5 ++--- site/src/pages/HealthPage/Content.tsx | 7 +++---- .../StarterTemplatePage/StarterTemplatePageView.tsx | 4 ++-- .../TemplateVersionStatusBadge.tsx | 6 +++--- .../UserSettingsPage/TokensPage/TokensPageView.tsx | 2 +- site/src/pages/WorkspacePage/AppStatuses.tsx | 7 +++++-- .../WorkspaceParametersPage.tsx | 4 ++-- site/src/pages/WorkspacesPage/WorkspacesButton.tsx | 4 ++-- 18 files changed, 48 insertions(+), 55 deletions(-) delete mode 100644 site/migrate-icons.md diff --git a/site/migrate-icons.md b/site/migrate-icons.md deleted file mode 100644 index 5bf361c2151a1..0000000000000 --- a/site/migrate-icons.md +++ /dev/null @@ -1,8 +0,0 @@ -Look for all the @mui/icons-material icons below and replace them accordinlying with the Lucide icon: - -MUI | Lucide -TaskAlt | CircleCheckBigIcon -InfoOutlined | InfoIcon -ErrorOutline | CircleAlertIcon - -You should update the imports and usage. diff --git a/site/src/components/Filter/Filter.tsx b/site/src/components/Filter/Filter.tsx index 66b0312f804c1..ede669416d743 100644 --- a/site/src/components/Filter/Filter.tsx +++ b/site/src/components/Filter/Filter.tsx @@ -1,5 +1,4 @@ import { useTheme } from "@emotion/react"; -import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined"; import Button from "@mui/material/Button"; import Divider from "@mui/material/Divider"; import Menu from "@mui/material/Menu"; @@ -14,6 +13,7 @@ import { import { InputGroup } from "components/InputGroup/InputGroup"; import { SearchField } from "components/SearchField/SearchField"; import { useDebouncedFunction } from "hooks/debounce"; +import { ExternalLinkIcon } from "lucide-react"; import { ChevronDownIcon } from "lucide-react"; import { type FC, type ReactNode, useEffect, useRef, useState } from "react"; import type { useSearchParams } from "react-router-dom"; @@ -311,7 +311,7 @@ const PresetMenu: FC = ({ setIsOpen(false); }} > - + View advanced filtering )} @@ -325,7 +325,7 @@ const PresetMenu: FC = ({ setIsOpen(false); }} > - + {learnMoreLabel2} )} diff --git a/site/src/components/HelpTooltip/HelpTooltip.tsx b/site/src/components/HelpTooltip/HelpTooltip.tsx index cf30e2b169e33..2ae8700114b3b 100644 --- a/site/src/components/HelpTooltip/HelpTooltip.tsx +++ b/site/src/components/HelpTooltip/HelpTooltip.tsx @@ -5,7 +5,6 @@ import { css, useTheme, } from "@emotion/react"; -import HelpIcon from "@mui/icons-material/HelpOutline"; import OpenInNewIcon from "@mui/icons-material/OpenInNew"; import Link from "@mui/material/Link"; import { Stack } from "components/Stack/Stack"; @@ -17,6 +16,7 @@ import { PopoverTrigger, usePopover, } from "components/deprecated/Popover/Popover"; +import { CircleHelpIcon } from "lucide-react"; import { type FC, type HTMLAttributes, @@ -25,11 +25,11 @@ import { forwardRef, } from "react"; -type Icon = typeof HelpIcon; +type Icon = typeof CircleHelpIcon; type Size = "small" | "medium"; -export const HelpTooltipIcon = HelpIcon; +export const HelpTooltipIcon = CircleHelpIcon; export const HelpTooltip: FC = (props) => { return ; diff --git a/site/src/components/Latency/Latency.tsx b/site/src/components/Latency/Latency.tsx index 706bf106876b5..b5509ba450847 100644 --- a/site/src/components/Latency/Latency.tsx +++ b/site/src/components/Latency/Latency.tsx @@ -1,9 +1,9 @@ import { useTheme } from "@emotion/react"; -import HelpOutline from "@mui/icons-material/HelpOutline"; import CircularProgress from "@mui/material/CircularProgress"; import Tooltip from "@mui/material/Tooltip"; import { visuallyHidden } from "@mui/utils"; import { Abbr } from "components/Abbr/Abbr"; +import { CircleHelpIcon } from "lucide-react"; import type { FC } from "react"; import { getLatencyColor } from "utils/latency"; @@ -41,10 +41,10 @@ export const Latency: FC = ({ <> {notAvailableText} - diff --git a/site/src/components/RichParameterInput/RichParameterInput.tsx b/site/src/components/RichParameterInput/RichParameterInput.tsx index 84fe47bbbfee3..e62f1d57a9a39 100644 --- a/site/src/components/RichParameterInput/RichParameterInput.tsx +++ b/site/src/components/RichParameterInput/RichParameterInput.tsx @@ -1,5 +1,4 @@ import type { Interpolation, Theme } from "@emotion/react"; -import ErrorOutline from "@mui/icons-material/ErrorOutline"; import SettingsIcon from "@mui/icons-material/Settings"; import Button from "@mui/material/Button"; import FormControlLabel from "@mui/material/FormControlLabel"; @@ -14,6 +13,7 @@ import { ExternalImage } from "components/ExternalImage/ExternalImage"; import { MemoizedMarkdown } from "components/Markdown/Markdown"; import { Pill } from "components/Pill/Pill"; import { Stack } from "components/Stack/Stack"; +import { CircleAlertIcon } from "lucide-react"; import { type FC, type ReactNode, useState } from "react"; import type { AutofillBuildParameter, @@ -143,7 +143,10 @@ const ParameterLabel: FC = ({ parameter, isPreset }) => { )} {!parameter.mutable && ( - }> + } + > Immutable diff --git a/site/src/modules/dashboard/DeploymentBanner/DeploymentBannerView.tsx b/site/src/modules/dashboard/DeploymentBanner/DeploymentBannerView.tsx index dd3c29e262986..c4e313d103f02 100644 --- a/site/src/modules/dashboard/DeploymentBanner/DeploymentBannerView.tsx +++ b/site/src/modules/dashboard/DeploymentBanner/DeploymentBannerView.tsx @@ -4,7 +4,6 @@ import BuildingIcon from "@mui/icons-material/Build"; import DownloadIcon from "@mui/icons-material/CloudDownload"; import UploadIcon from "@mui/icons-material/CloudUpload"; import CollectedIcon from "@mui/icons-material/Compare"; -import ErrorIcon from "@mui/icons-material/ErrorOutline"; import RefreshIcon from "@mui/icons-material/Refresh"; import LatencyIcon from "@mui/icons-material/SettingsEthernet"; import WebTerminalIcon from "@mui/icons-material/WebAsset"; @@ -24,6 +23,7 @@ import { VSCodeIcon } from "components/Icons/VSCodeIcon"; import { Stack } from "components/Stack/Stack"; import dayjs from "dayjs"; import { type ClassName, useClassName } from "hooks/useClassName"; +import { CircleAlertIcon } from "lucide-react"; import prettyBytes from "pretty-bytes"; import { type FC, @@ -151,7 +151,7 @@ export const DeploymentBannerView: FC = ({ to="/health" css={[styles.statusBadge, styles.unhealthy]} > - + ) : (
@@ -372,9 +372,9 @@ const HealthIssue: FC = ({ children }) => { return ( - {children} diff --git a/site/src/modules/resources/AgentOutdatedTooltip.tsx b/site/src/modules/resources/AgentOutdatedTooltip.tsx index e5bd25d79b228..c961def910589 100644 --- a/site/src/modules/resources/AgentOutdatedTooltip.tsx +++ b/site/src/modules/resources/AgentOutdatedTooltip.tsx @@ -1,5 +1,4 @@ import { useTheme } from "@emotion/react"; -import RefreshIcon from "@mui/icons-material/RefreshOutlined"; import type { WorkspaceAgent } from "api/typesGenerated"; import { HelpTooltip, @@ -11,6 +10,7 @@ import { } from "components/HelpTooltip/HelpTooltip"; import { Stack } from "components/Stack/Stack"; import { PopoverTrigger } from "components/deprecated/Popover/Popover"; +import { RotateCcwIcon } from "lucide-react"; import type { FC } from "react"; import { agentVersionStatus } from "../../utils/workspace"; @@ -68,7 +68,7 @@ export const AgentOutdatedTooltip: FC = ({ diff --git a/site/src/modules/resources/PortForwardButton.tsx b/site/src/modules/resources/PortForwardButton.tsx index e2670eed65b02..437adf881e745 100644 --- a/site/src/modules/resources/PortForwardButton.tsx +++ b/site/src/modules/resources/PortForwardButton.tsx @@ -1,7 +1,6 @@ import { type Interpolation, type Theme, useTheme } from "@emotion/react"; import LockIcon from "@mui/icons-material/Lock"; import LockOpenIcon from "@mui/icons-material/LockOpen"; -import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined"; import SensorsIcon from "@mui/icons-material/Sensors"; import LoadingButton from "@mui/lab/LoadingButton"; import Button from "@mui/material/Button"; @@ -40,8 +39,7 @@ import { } from "components/deprecated/Popover/Popover"; import { type FormikContextType, useFormik } from "formik"; import { type ClassName, useClassName } from "hooks/useClassName"; -import { X as XIcon } from "lucide-react"; -import { ChevronDownIcon } from "lucide-react"; +import { ChevronDownIcon, ExternalLinkIcon, X as XIcon } from "lucide-react"; import { useDashboard } from "modules/dashboard/useDashboard"; import { type FC, useState } from "react"; import { useMutation, useQuery } from "react-query"; @@ -308,11 +306,10 @@ export const PortForwardPopoverView: FC = ({ minWidth: 0, }} > - diff --git a/site/src/modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx b/site/src/modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx index 9615560840c59..eed553b588ec6 100644 --- a/site/src/modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx +++ b/site/src/modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx @@ -1,6 +1,5 @@ import { type Interpolation, type Theme, useTheme } from "@emotion/react"; import InfoIcon from "@mui/icons-material/InfoOutlined"; -import RefreshIcon from "@mui/icons-material/Refresh"; import Link from "@mui/material/Link"; import Skeleton from "@mui/material/Skeleton"; import { getErrorDetail, getErrorMessage } from "api/errors"; @@ -17,6 +16,7 @@ import { HelpTooltipTrigger, } from "components/HelpTooltip/HelpTooltip"; import { usePopover } from "components/deprecated/Popover/Popover"; +import { RotateCcwIcon } from "lucide-react"; import { linkToTemplate, useLinks } from "modules/navigation"; import type { FC } from "react"; import { useQuery } from "react-query"; @@ -104,7 +104,7 @@ const WorkspaceOutdatedTooltipContent: FC = ({ workspace }) => { Update diff --git a/site/src/modules/workspaces/WorkspaceTiming/Chart/Tooltip.tsx b/site/src/modules/workspaces/WorkspaceTiming/Chart/Tooltip.tsx index fc1ab550a8854..85b556c786a07 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/Chart/Tooltip.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/Chart/Tooltip.tsx @@ -1,9 +1,9 @@ import { css } from "@emotion/css"; import { type Interpolation, type Theme, useTheme } from "@emotion/react"; -import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined"; import MUITooltip, { type TooltipProps as MUITooltipProps, } from "@mui/material/Tooltip"; +import { ExternalLinkIcon } from "lucide-react"; import type { FC, HTMLProps } from "react"; import { Link, type LinkProps } from "react-router-dom"; @@ -36,7 +36,7 @@ export const TooltipShortDescription: FC> = ( export const TooltipLink: FC = (props) => { return ( - + {props.children} ); diff --git a/site/src/pages/GroupsPage/GroupPage.tsx b/site/src/pages/GroupsPage/GroupPage.tsx index 365f105f6ffb4..f97ec2d82be09 100644 --- a/site/src/pages/GroupsPage/GroupPage.tsx +++ b/site/src/pages/GroupsPage/GroupPage.tsx @@ -50,8 +50,7 @@ import { TableToolbar, } from "components/TableToolbar/TableToolbar"; import { MemberAutocomplete } from "components/UserAutocomplete/UserAutocomplete"; -import { TrashIcon } from "lucide-react"; -import { EllipsisVertical } from "lucide-react"; +import { EllipsisVertical, TrashIcon } from "lucide-react"; import { type FC, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQuery, useQueryClient } from "react-query"; @@ -134,7 +133,7 @@ const GroupPage: FC = () => { onClick={() => { setIsDeletingGroup(true); }} - startIcon={} + startIcon={} css={styles.removeButton} > Delete… diff --git a/site/src/pages/HealthPage/Content.tsx b/site/src/pages/HealthPage/Content.tsx index 2bd5e96f2450e..74cbc9a5b87c1 100644 --- a/site/src/pages/HealthPage/Content.tsx +++ b/site/src/pages/HealthPage/Content.tsx @@ -1,10 +1,9 @@ import { css } from "@emotion/css"; import { useTheme } from "@emotion/react"; -import ErrorOutline from "@mui/icons-material/ErrorOutline"; import Link from "@mui/material/Link"; import type { HealthCode, HealthSeverity } from "api/typesGenerated"; -import { CircleCheck as CircleCheckIcon } from "lucide-react"; -import { CircleMinus as CircleMinusIcon } from "lucide-react"; +import { CircleAlertIcon } from "lucide-react"; +import { CircleCheckIcon, CircleMinusIcon } from "lucide-react"; import { type ComponentProps, type FC, @@ -57,7 +56,7 @@ interface HealthIconProps { export const HealthIcon: FC = ({ size, severity }) => { const theme = useTheme(); const color = healthyColor(theme, severity); - const Icon = severity === "error" ? ErrorOutline : CircleCheckIcon; + const Icon = severity === "error" ? CircleAlertIcon : CircleCheckIcon; return ; }; diff --git a/site/src/pages/StarterTemplatePage/StarterTemplatePageView.tsx b/site/src/pages/StarterTemplatePage/StarterTemplatePageView.tsx index c79bfc809556f..00872ed8d5bfb 100644 --- a/site/src/pages/StarterTemplatePage/StarterTemplatePageView.tsx +++ b/site/src/pages/StarterTemplatePage/StarterTemplatePageView.tsx @@ -1,6 +1,5 @@ import { useTheme } from "@emotion/react"; import PlusIcon from "@mui/icons-material/AddOutlined"; -import ViewCodeIcon from "@mui/icons-material/OpenInNewOutlined"; import Button from "@mui/material/Button"; import type { TemplateExample } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; @@ -14,6 +13,7 @@ import { PageHeaderTitle, } from "components/PageHeader/PageHeader"; import { Stack } from "components/Stack/Stack"; +import { ExternalLinkIcon } from "lucide-react"; import type { FC } from "react"; import { Link } from "react-router-dom"; @@ -50,7 +50,7 @@ export const StarterTemplatePageView: FC = ({ target="_blank" href={starterTemplate.url} rel="noreferrer" - startIcon={} + startIcon={} > View source code diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionStatusBadge.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionStatusBadge.tsx index 94f2d17769d08..cfee103357422 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionStatusBadge.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionStatusBadge.tsx @@ -1,8 +1,8 @@ import CheckIcon from "@mui/icons-material/CheckOutlined"; -import ErrorIcon from "@mui/icons-material/ErrorOutline"; import QueuedIcon from "@mui/icons-material/HourglassEmpty"; import type { TemplateVersion } from "api/typesGenerated"; import { Pill, PillSpinner } from "components/Pill/Pill"; +import { CircleAlertIcon } from "lucide-react"; import type { FC, ReactNode } from "react"; import type { ThemeRole } from "theme/roles"; import { getPendingStatusLabel } from "utils/provisionerJob"; @@ -57,14 +57,14 @@ const getStatus = ( return { type: "inactive", text: "Canceled", - icon: , + icon: , }; case "unknown": case "failed": return { type: "error", text: "Failed", - icon: , + icon: , }; case "succeeded": return { diff --git a/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx b/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx index f9a2467196ecb..1be416a48c3b2 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx +++ b/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx @@ -58,7 +58,7 @@ export const TokensPageView: FC = ({ Last Used Expires At Created At - + diff --git a/site/src/pages/WorkspacePage/AppStatuses.tsx b/site/src/pages/WorkspacePage/AppStatuses.tsx index a285f8acc0e53..9d8b8ed4752c3 100644 --- a/site/src/pages/WorkspacePage/AppStatuses.tsx +++ b/site/src/pages/WorkspacePage/AppStatuses.tsx @@ -3,7 +3,6 @@ import { useTheme } from "@emotion/react"; import AppsIcon from "@mui/icons-material/Apps"; import CheckCircle from "@mui/icons-material/CheckCircle"; import ErrorIcon from "@mui/icons-material/Error"; -import HelpOutline from "@mui/icons-material/HelpOutline"; import HourglassEmpty from "@mui/icons-material/HourglassEmpty"; import InsertDriveFile from "@mui/icons-material/InsertDriveFile"; import OpenInNew from "@mui/icons-material/OpenInNew"; @@ -18,6 +17,7 @@ import type { WorkspaceApp, } from "api/typesGenerated"; import { formatDistance, formatDistanceToNow } from "date-fns"; +import { CircleHelpIcon } from "lucide-react"; import { useAppLink } from "modules/apps/useAppLink"; import type { FC } from "react"; @@ -224,7 +224,10 @@ export const AppStatuses: FC = ({ }} > {getStatusIcon(theme, status.state, isLatest) || ( - + )}
diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx index a3bc7964f9558..443e7183cca60 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx @@ -1,4 +1,3 @@ -import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined"; import Button from "@mui/material/Button"; import { API } from "api/api"; import { isApiValidationError } from "api/errors"; @@ -9,6 +8,7 @@ import { ErrorAlert } from "components/Alert/ErrorAlert"; import { EmptyState } from "components/EmptyState/EmptyState"; import { Loader } from "components/Loader/Loader"; import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; +import { ExternalLinkIcon } from "lucide-react"; import type { FC } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQuery } from "react-query"; @@ -143,7 +143,7 @@ export const WorkspaceParametersPageView: FC< @@ -344,7 +343,7 @@ const WorkspaceBuildValue: FC = ({ let statusText = displayStatus.text; let icon = displayStatus.icon; if (status === "starting") { - icon = ; + icon = ; statusText = "Building"; } diff --git a/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicensesSettingsPageView.tsx b/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicensesSettingsPageView.tsx index c4152c7b8f565..a7d39d8536c62 100644 --- a/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicensesSettingsPageView.tsx +++ b/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicensesSettingsPageView.tsx @@ -1,6 +1,5 @@ import { type Interpolation, type Theme, useTheme } from "@emotion/react"; import AddIcon from "@mui/icons-material/AddOutlined"; -import RefreshIcon from "@mui/icons-material/Refresh"; import LoadingButton from "@mui/lab/LoadingButton"; import Button from "@mui/material/Button"; import MuiLink from "@mui/material/Link"; @@ -15,6 +14,7 @@ import { } from "components/SettingsHeader/SettingsHeader"; import { Stack } from "components/Stack/Stack"; import { useWindowSize } from "hooks/useWindowSize"; +import { RotateCwIcon } from "lucide-react"; import type { FC } from "react"; import Confetti from "react-confetti"; import { Link } from "react-router-dom"; @@ -84,7 +84,7 @@ const LicensesSettingsPageView: FC = ({ loadingPosition="start" loading={isRefreshing} onClick={refreshEntitlements} - startIcon={} + startIcon={} > Refresh diff --git a/site/src/pages/ExternalAuthPage/ExternalAuthPageView.tsx b/site/src/pages/ExternalAuthPage/ExternalAuthPageView.tsx index fd379bf0121fa..32809fb08cc7b 100644 --- a/site/src/pages/ExternalAuthPage/ExternalAuthPageView.tsx +++ b/site/src/pages/ExternalAuthPage/ExternalAuthPageView.tsx @@ -1,6 +1,5 @@ import type { Interpolation, Theme } from "@emotion/react"; import OpenInNewIcon from "@mui/icons-material/OpenInNew"; -import RefreshIcon from "@mui/icons-material/Refresh"; import Link from "@mui/material/Link"; import Tooltip from "@mui/material/Tooltip"; import type { ApiErrorResponse } from "api/errors"; @@ -10,6 +9,7 @@ import { Avatar } from "components/Avatar/Avatar"; import { GitDeviceAuth } from "components/GitDeviceAuth/GitDeviceAuth"; import { SignInLayout } from "components/SignInLayout/SignInLayout"; import { Welcome } from "components/Welcome/Welcome"; +import { RotateCwIcon } from "lucide-react"; import type { FC, ReactNode } from "react"; export interface ExternalAuthPageViewProps { @@ -132,7 +132,7 @@ const ExternalAuthPageView: FC = ({ onReauthenticate(); }} > - Reauthenticate + Reauthenticate diff --git a/site/src/pages/IconsPage/IconsPage.tsx b/site/src/pages/IconsPage/IconsPage.tsx index 96c6dd4c459e3..d9dc19c784b57 100644 --- a/site/src/pages/IconsPage/IconsPage.tsx +++ b/site/src/pages/IconsPage/IconsPage.tsx @@ -1,6 +1,4 @@ import { useTheme } from "@emotion/react"; -import ClearIcon from "@mui/icons-material/CloseOutlined"; -import SearchIcon from "@mui/icons-material/SearchOutlined"; import IconButton from "@mui/material/IconButton"; import InputAdornment from "@mui/material/InputAdornment"; import Link from "@mui/material/Link"; @@ -15,6 +13,7 @@ import { PageHeaderTitle, } from "components/PageHeader/PageHeader"; import { Stack } from "components/Stack/Stack"; +import { SearchIcon, XIcon } from "lucide-react"; import { type FC, type ReactNode, useMemo, useState } from "react"; import { Helmet } from "react-helmet-async"; import { @@ -129,8 +128,8 @@ const IconsPage: FC = () => { startAdornment: ( @@ -143,7 +142,7 @@ const IconsPage: FC = () => { size="small" onClick={() => setSearchInputText("")} > - + diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx index 00fcc5f29e6c8..2040fab3878d9 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx @@ -1,7 +1,6 @@ import { type Interpolation, type Theme, useTheme } from "@emotion/react"; import CreateIcon from "@mui/icons-material/AddOutlined"; import ArrowBackOutlined from "@mui/icons-material/ArrowBackOutlined"; -import CloseOutlined from "@mui/icons-material/CloseOutlined"; import WarningOutlined from "@mui/icons-material/WarningOutlined"; import Button from "@mui/material/Button"; import IconButton from "@mui/material/IconButton"; @@ -27,7 +26,7 @@ import { } from "components/FullPageLayout/Topbar"; import { displayError } from "components/GlobalSnackbar/utils"; import { Loader } from "components/Loader/Loader"; -import { PlayIcon } from "lucide-react"; +import { PlayIcon, XIcon } from "lucide-react"; import { linkToTemplate, useLinks } from "modules/navigation"; import { ProvisionerAlert } from "modules/provisioners/ProvisionerAlert"; import { AlertVariant } from "modules/provisioners/ProvisionerAlert"; @@ -567,7 +566,7 @@ export const TemplateVersionEditor: FC = ({ borderRadius: 0, }} > - + )} From 8f64d49b22ae67a88df0e50babe73a42c503a488 Mon Sep 17 00:00:00 2001 From: Charlie Voiselle <464492+angrycub@users.noreply.github.com> Date: Tue, 13 May 2025 11:49:56 -0400 Subject: [PATCH 23/88] chore: update alpine 3.21.2 => 3.21.3 (#17773) Resolves 3 CVEs in base container (1 High, 2 Medium) | CVE ID | CVSS Score | Package / Version | | -------------- | ---------- | ------------------------------ | | CVE-2025-26519 | 8.1 High | apk / alpine/musl / 1.2.5-r8 | | CVE-2024-12797 | 6.3 Medium | apk / alpine/openssl / 3.3.2-r4 | | CVE-2024-13176 | 4.1 Medium | apk / alpine/openssl / 3.3.2-r4 | --- scripts/Dockerfile.base | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/Dockerfile.base b/scripts/Dockerfile.base index fdadd87e55a3a..6c8ab5a544e30 100644 --- a/scripts/Dockerfile.base +++ b/scripts/Dockerfile.base @@ -1,7 +1,7 @@ # This is the base image used for Coder images. It's a multi-arch image that is # built in depot.dev for all supported architectures. Since it's built on real # hardware and not cross-compiled, it can have "RUN" commands. -FROM alpine:3.21.2 +FROM alpine:3.21.3 # We use a single RUN command to reduce the number of layers in the image. # NOTE: Keep the Terraform version in sync with minTerraformVersion and From a1c03b6c5f32dc71661406f140c47d7aca9d83cd Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 13 May 2025 17:24:10 +0100 Subject: [PATCH 24/88] feat: add experimental Chat UI (#17650) Builds on https://github.com/coder/coder/pull/17570 Frontend portion of https://github.com/coder/coder/tree/chat originally authored by @kylecarbs Additional changes: - Addresses linter complaints - Brings `ChatToolInvocation` argument definitions in line with those defined in `codersdk/toolsdk` - Ensures chat-related features are not shown unless `ExperimentAgenticChat` is enabled. Co-authored-by: Kyle Carberry --- site/package.json | 3 + site/pnpm-lock.yaml | 216 +++ site/src/api/api.ts | 24 + site/src/api/queries/chats.ts | 25 + site/src/api/queries/deployment.ts | 7 + site/src/contexts/useAgenticChat.ts | 16 + .../modules/dashboard/Navbar/NavbarView.tsx | 31 +- site/src/pages/ChatPage/ChatLanding.tsx | 164 +++ site/src/pages/ChatPage/ChatLayout.tsx | 246 ++++ site/src/pages/ChatPage/ChatMessages.tsx | 491 +++++++ .../ChatPage/ChatToolInvocation.stories.tsx | 1211 +++++++++++++++++ .../src/pages/ChatPage/ChatToolInvocation.tsx | 872 ++++++++++++ .../pages/ChatPage/LanguageModelSelector.tsx | 73 + site/src/router.tsx | 8 + 14 files changed, 3381 insertions(+), 6 deletions(-) create mode 100644 site/src/api/queries/chats.ts create mode 100644 site/src/contexts/useAgenticChat.ts create mode 100644 site/src/pages/ChatPage/ChatLanding.tsx create mode 100644 site/src/pages/ChatPage/ChatLayout.tsx create mode 100644 site/src/pages/ChatPage/ChatMessages.tsx create mode 100644 site/src/pages/ChatPage/ChatToolInvocation.stories.tsx create mode 100644 site/src/pages/ChatPage/ChatToolInvocation.tsx create mode 100644 site/src/pages/ChatPage/LanguageModelSelector.tsx diff --git a/site/package.json b/site/package.json index 23c1cf9d22428..bc459ce79f7a1 100644 --- a/site/package.json +++ b/site/package.json @@ -35,6 +35,8 @@ "update-emojis": "cp -rf ./node_modules/emoji-datasource-apple/img/apple/64/* ./static/emojis" }, "dependencies": { + "@ai-sdk/provider-utils": "2.2.6", + "@ai-sdk/react": "1.2.6", "@emoji-mart/data": "1.2.1", "@emoji-mart/react": "1.1.1", "@emotion/cache": "11.14.0", @@ -111,6 +113,7 @@ "react-virtualized-auto-sizer": "1.0.24", "react-window": "1.8.11", "recharts": "2.15.0", + "rehype-raw": "7.0.0", "remark-gfm": "4.0.0", "resize-observer-polyfill": "1.5.1", "rollup-plugin-visualizer": "5.14.0", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index 7b8e9c52ea4af..252d7809033ec 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -16,6 +16,12 @@ importers: .: dependencies: + '@ai-sdk/provider-utils': + specifier: 2.2.6 + version: 2.2.6(zod@3.24.3) + '@ai-sdk/react': + specifier: 1.2.6 + version: 1.2.6(react@18.3.1)(zod@3.24.3) '@emoji-mart/data': specifier: 1.2.1 version: 1.2.1 @@ -244,6 +250,9 @@ importers: recharts: specifier: 2.15.0 version: 2.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rehype-raw: + specifier: 7.0.0 + version: 7.0.0 remark-gfm: specifier: 4.0.0 version: 4.0.0 @@ -489,6 +498,42 @@ packages: '@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} + '@ai-sdk/provider-utils@2.2.4': + resolution: {integrity: sha512-13sEGBxB6kgaMPGOgCLYibF6r8iv8mgjhuToFrOTU09bBxbFQd8ZoARarCfJN6VomCUbUvMKwjTBLb1vQnN+WA==, tarball: https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-2.2.4.tgz} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.23.8 + + '@ai-sdk/provider-utils@2.2.6': + resolution: {integrity: sha512-sUlZ7Gnq84DCGWMQRIK8XVbkzIBnvPR1diV4v6JwPgpn5armnLI/j+rqn62MpLrU5ZCQZlDKl/Lw6ed3ulYqaA==, tarball: https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-2.2.6.tgz} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.23.8 + + '@ai-sdk/provider@1.1.0': + resolution: {integrity: sha512-0M+qjp+clUD0R1E5eWQFhxEvWLNaOtGQRUaBn8CUABnSKredagq92hUS9VjOzGsTm37xLfpaxl97AVtbeOsHew==, tarball: https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.0.tgz} + engines: {node: '>=18'} + + '@ai-sdk/provider@1.1.2': + resolution: {integrity: sha512-ITdgNilJZwLKR7X5TnUr1BsQW6UTX5yFp0h66Nfx8XjBYkWD9W3yugr50GOz3CnE9m/U/Cd5OyEbTMI0rgi6ZQ==, tarball: https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.2.tgz} + engines: {node: '>=18'} + + '@ai-sdk/react@1.2.6': + resolution: {integrity: sha512-5BFChNbcYtcY9MBStcDev7WZRHf0NpTrk8yfSoedWctB3jfWkFd1HECBvdc8w3mUQshF2MumLHtAhRO7IFtGGQ==, tarball: https://registry.npmjs.org/@ai-sdk/react/-/react-1.2.6.tgz} + engines: {node: '>=18'} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + zod: ^3.23.8 + peerDependenciesMeta: + zod: + optional: true + + '@ai-sdk/ui-utils@1.2.5': + resolution: {integrity: sha512-XDgqnJcaCkDez7qolvk+PDbs/ceJvgkNkxkOlc9uDWqxfDJxtvCZ+14MP/1qr4IBwGIgKVHzMDYDXvqVhSWLzg==, tarball: https://registry.npmjs.org/@ai-sdk/ui-utils/-/ui-utils-1.2.5.tgz} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.23.8 + '@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} engines: {node: '>=10'} @@ -3942,18 +3987,33 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==, tarball: https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz} engines: {node: '>= 0.4'} + hast-util-from-parse5@8.0.3: + resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==, tarball: https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz} + hast-util-parse-selector@2.2.5: resolution: {integrity: sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==, tarball: https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz} + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==, tarball: https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz} + + hast-util-raw@9.1.0: + resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==, tarball: https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz} + hast-util-to-jsx-runtime@2.3.2: resolution: {integrity: sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg==, tarball: https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.2.tgz} + hast-util-to-parse5@8.0.0: + resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==, tarball: https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz} + hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==, tarball: https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz} hastscript@6.0.0: resolution: {integrity: sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==, tarball: https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz} + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==, tarball: https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz} + headers-polyfill@4.0.3: resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==, tarball: https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz} @@ -3976,6 +4036,9 @@ packages: html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==, tarball: https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz} + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==, tarball: https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz} + http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==, tarball: https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz} engines: {node: '>= 0.8'} @@ -4480,6 +4543,9 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==, tarball: https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz} + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==, tarball: https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==, tarball: https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz} @@ -5236,6 +5302,9 @@ packages: property-information@6.5.0: resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==, tarball: https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz} + property-information@7.0.0: + resolution: {integrity: sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==, tarball: https://registry.npmjs.org/property-information/-/property-information-7.0.0.tgz} + protobufjs@7.4.0: resolution: {integrity: sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==, tarball: https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz} engines: {node: '>=12.0.0'} @@ -5492,6 +5561,9 @@ packages: resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==, tarball: https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz} engines: {node: '>= 0.4'} + rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==, tarball: https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz} + remark-gfm@4.0.0: resolution: {integrity: sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==, tarball: https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.0.tgz} @@ -5599,6 +5671,9 @@ packages: scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==, tarball: https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz} + secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==, tarball: https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz} + semver@7.6.2: resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==, tarball: https://registry.npmjs.org/semver/-/semver-7.6.2.tgz} engines: {node: '>=10'} @@ -5840,6 +5915,11 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==, tarball: https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz} engines: {node: '>= 0.4'} + swr@2.3.3: + resolution: {integrity: sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A==, tarball: https://registry.npmjs.org/swr/-/swr-2.3.3.tgz} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + 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} @@ -5877,6 +5957,10 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==, tarball: https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz} + throttleit@2.1.0: + resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==, tarball: https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz} + engines: {node: '>=18'} + tiny-case@1.0.3: resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==, tarball: https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz} @@ -6163,6 +6247,9 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==, tarball: https://registry.npmjs.org/vary/-/vary-1.1.2.tgz} engines: {node: '>= 0.8'} + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==, tarball: https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz} + vfile-message@4.0.2: resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==, tarball: https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz} @@ -6274,6 +6361,9 @@ packages: wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==, tarball: https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz} + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==, tarball: https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz} + webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==, tarball: https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz} engines: {node: '>=12'} @@ -6405,6 +6495,11 @@ packages: yup@1.6.1: resolution: {integrity: sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA==, tarball: https://registry.npmjs.org/yup/-/yup-1.6.1.tgz} + zod-to-json-schema@3.24.5: + resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==, tarball: https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz} + peerDependencies: + zod: ^3.24.1 + zod-validation-error@3.4.0: resolution: {integrity: sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==, tarball: https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.4.0.tgz} engines: {node: '>=18.0.0'} @@ -6424,6 +6519,45 @@ snapshots: '@adobe/css-tools@4.4.1': {} + '@ai-sdk/provider-utils@2.2.4(zod@3.24.3)': + dependencies: + '@ai-sdk/provider': 1.1.0 + nanoid: 3.3.8 + secure-json-parse: 2.7.0 + zod: 3.24.3 + + '@ai-sdk/provider-utils@2.2.6(zod@3.24.3)': + dependencies: + '@ai-sdk/provider': 1.1.2 + nanoid: 3.3.8 + secure-json-parse: 2.7.0 + zod: 3.24.3 + + '@ai-sdk/provider@1.1.0': + dependencies: + json-schema: 0.4.0 + + '@ai-sdk/provider@1.1.2': + dependencies: + json-schema: 0.4.0 + + '@ai-sdk/react@1.2.6(react@18.3.1)(zod@3.24.3)': + dependencies: + '@ai-sdk/provider-utils': 2.2.4(zod@3.24.3) + '@ai-sdk/ui-utils': 1.2.5(zod@3.24.3) + react: 18.3.1 + swr: 2.3.3(react@18.3.1) + throttleit: 2.1.0 + optionalDependencies: + zod: 3.24.3 + + '@ai-sdk/ui-utils@1.2.5(zod@3.24.3)': + dependencies: + '@ai-sdk/provider': 1.1.0 + '@ai-sdk/provider-utils': 2.2.4(zod@3.24.3) + zod: 3.24.3 + zod-to-json-schema: 3.24.5(zod@3.24.3) + '@alloc/quick-lru@5.2.0': {} '@ampproject/remapping@2.3.0': @@ -10183,8 +10317,39 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-from-parse5@8.0.3: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 9.0.1 + property-information: 7.0.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 + hast-util-parse-selector@2.2.5: {} + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-raw@9.1.0: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + '@ungap/structured-clone': 1.3.0 + hast-util-from-parse5: 8.0.3 + hast-util-to-parse5: 8.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + parse5: 7.1.2 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + hast-util-to-jsx-runtime@2.3.2: dependencies: '@types/estree': 1.0.6 @@ -10205,6 +10370,16 @@ snapshots: transitivePeerDependencies: - supports-color + hast-util-to-parse5@8.0.0: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + hast-util-whitespace@3.0.0: dependencies: '@types/hast': 3.0.4 @@ -10217,6 +10392,14 @@ snapshots: property-information: 5.6.0 space-separated-tokens: 1.1.5 + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.0.0 + space-separated-tokens: 2.0.2 + headers-polyfill@4.0.3: {} highlight.js@10.7.3: {} @@ -10235,6 +10418,8 @@ snapshots: html-url-attributes@3.0.1: {} + html-void-elements@3.0.0: {} + http-errors@2.0.0: dependencies: depd: 2.0.0 @@ -10962,6 +11147,8 @@ snapshots: json-schema-traverse@0.4.1: optional: true + json-schema@0.4.0: {} + json-stable-stringify-without-jsonify@1.0.1: optional: true @@ -11986,6 +12173,8 @@ snapshots: property-information@6.5.0: {} + property-information@7.0.0: {} + protobufjs@7.4.0: dependencies: '@protobufjs/aspromise': 1.1.2 @@ -12303,6 +12492,12 @@ snapshots: define-properties: 1.2.1 set-function-name: 2.0.1 + rehype-raw@7.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-raw: 9.1.0 + vfile: 6.0.3 + remark-gfm@4.0.0: dependencies: '@types/mdast': 4.0.3 @@ -12442,6 +12637,8 @@ snapshots: dependencies: loose-envify: 1.4.0 + secure-json-parse@2.7.0: {} + semver@7.6.2: {} send@0.19.0: @@ -12695,6 +12892,12 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + swr@2.3.3(react@18.3.1): + dependencies: + dequal: 2.0.3 + react: 18.3.1 + use-sync-external-store: 1.4.0(react@18.3.1) + symbol-tree@3.2.4: {} tailwind-merge@2.6.0: {} @@ -12753,6 +12956,8 @@ snapshots: dependencies: any-promise: 1.3.0 + throttleit@2.1.0: {} + tiny-case@1.0.3: {} tiny-invariant@1.3.3: {} @@ -13043,6 +13248,11 @@ snapshots: vary@1.1.2: {} + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + vfile-message@4.0.2: dependencies: '@types/unist': 3.0.3 @@ -13139,6 +13349,8 @@ snapshots: dependencies: defaults: 1.0.4 + web-namespaces@2.0.1: {} + webidl-conversions@7.0.0: {} webpack-sources@3.2.3: {} @@ -13253,6 +13465,10 @@ snapshots: toposort: 2.0.2 type-fest: 2.19.0 + zod-to-json-schema@3.24.5(zod@3.24.3): + dependencies: + zod: 3.24.3 + zod-validation-error@3.4.0(zod@3.24.3): dependencies: zod: 3.24.3 diff --git a/site/src/api/api.ts b/site/src/api/api.ts index ef15beb8166f5..688ba0432e22b 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -827,6 +827,13 @@ class ApiMethods { return response.data; }; + getDeploymentLLMs = async (): Promise => { + const response = await this.axios.get( + "/api/v2/deployment/llms", + ); + return response.data; + }; + getOrganizationIdpSyncClaimFieldValues = async ( organization: string, field: string, @@ -2489,6 +2496,23 @@ class ApiMethods { markAllInboxNotificationsAsRead = async () => { await this.axios.put("/api/v2/notifications/inbox/mark-all-as-read"); }; + + createChat = async () => { + const res = await this.axios.post("/api/v2/chats"); + return res.data; + }; + + getChats = async () => { + const res = await this.axios.get("/api/v2/chats"); + return res.data; + }; + + getChatMessages = async (chatId: string) => { + const res = await this.axios.get( + `/api/v2/chats/${chatId}/messages`, + ); + return res.data; + }; } // This is a hard coded CSRF token/cookie pair for local development. In prod, diff --git a/site/src/api/queries/chats.ts b/site/src/api/queries/chats.ts new file mode 100644 index 0000000000000..196bf4c603597 --- /dev/null +++ b/site/src/api/queries/chats.ts @@ -0,0 +1,25 @@ +import { API } from "api/api"; +import type { QueryClient } from "react-query"; + +export const createChat = (queryClient: QueryClient) => { + return { + mutationFn: API.createChat, + onSuccess: async () => { + await queryClient.invalidateQueries(["chats"]); + }, + }; +}; + +export const getChats = () => { + return { + queryKey: ["chats"], + queryFn: API.getChats, + }; +}; + +export const getChatMessages = (chatID: string) => { + return { + queryKey: ["chatMessages", chatID], + queryFn: () => API.getChatMessages(chatID), + }; +}; diff --git a/site/src/api/queries/deployment.ts b/site/src/api/queries/deployment.ts index 999dd2ee4cbd5..463f555d57761 100644 --- a/site/src/api/queries/deployment.ts +++ b/site/src/api/queries/deployment.ts @@ -36,3 +36,10 @@ export const deploymentIdpSyncFieldValues = (field: string) => { queryFn: () => API.getDeploymentIdpSyncFieldValues(field), }; }; + +export const deploymentLanguageModels = () => { + return { + queryKey: ["deployment", "llms"], + queryFn: API.getDeploymentLLMs, + }; +}; diff --git a/site/src/contexts/useAgenticChat.ts b/site/src/contexts/useAgenticChat.ts new file mode 100644 index 0000000000000..97194b4512340 --- /dev/null +++ b/site/src/contexts/useAgenticChat.ts @@ -0,0 +1,16 @@ +import { experiments } from "api/queries/experiments"; + +import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata"; +import { useQuery } from "react-query"; + +interface AgenticChat { + readonly enabled: boolean; +} + +export const useAgenticChat = (): AgenticChat => { + const { metadata } = useEmbeddedMetadata(); + const enabledExperimentsQuery = useQuery(experiments(metadata.experiments)); + return { + enabled: enabledExperimentsQuery.data?.includes("agentic-chat") ?? false, + }; +}; diff --git a/site/src/modules/dashboard/Navbar/NavbarView.tsx b/site/src/modules/dashboard/Navbar/NavbarView.tsx index 0447e762ed67e..8cefde8cb86e3 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.tsx @@ -4,6 +4,7 @@ import { Button } from "components/Button/Button"; import { ExternalImage } from "components/ExternalImage/ExternalImage"; import { CoderIcon } from "components/Icons/CoderIcon"; import type { ProxyContextValue } from "contexts/ProxyContext"; +import { useAgenticChat } from "contexts/useAgenticChat"; import { useWebpushNotifications } from "contexts/useWebpushNotifications"; import { NotificationsInbox } from "modules/notifications/NotificationsInbox/NotificationsInbox"; import type { FC } from "react"; @@ -45,8 +46,7 @@ export const NavbarView: FC = ({ canViewAuditLog, proxyContextValue, }) => { - const { subscribed, enabled, loading, subscribe, unsubscribe } = - useWebpushNotifications(); + const webPush = useWebpushNotifications(); return (
@@ -76,13 +76,21 @@ export const NavbarView: FC = ({ />
- {enabled ? ( - subscribed ? ( - ) : ( - ) @@ -132,6 +140,7 @@ interface NavItemsProps { const NavItems: FC = ({ className }) => { const location = useLocation(); + const agenticChat = useAgenticChat(); return ( ); }; diff --git a/site/src/pages/ChatPage/ChatLanding.tsx b/site/src/pages/ChatPage/ChatLanding.tsx new file mode 100644 index 0000000000000..060752f895313 --- /dev/null +++ b/site/src/pages/ChatPage/ChatLanding.tsx @@ -0,0 +1,164 @@ +import { useTheme } from "@emotion/react"; +import SendIcon from "@mui/icons-material/Send"; +import Button from "@mui/material/Button"; +import IconButton from "@mui/material/IconButton"; +import Paper from "@mui/material/Paper"; +import Stack from "@mui/material/Stack"; +import TextField from "@mui/material/TextField"; +import { createChat } from "api/queries/chats"; +import type { Chat } from "api/typesGenerated"; +import { Margins } from "components/Margins/Margins"; +import { useAuthenticated } from "hooks"; +import { type FC, type FormEvent, useState } from "react"; +import { useMutation, useQueryClient } from "react-query"; +import { useNavigate } from "react-router-dom"; +import { LanguageModelSelector } from "./LanguageModelSelector"; + +export interface ChatLandingLocationState { + chat: Chat; + message: string; +} + +const ChatLanding: FC = () => { + const { user } = useAuthenticated(); + const theme = useTheme(); + const [input, setInput] = useState(""); + const navigate = useNavigate(); + const queryClient = useQueryClient(); + const createChatMutation = useMutation(createChat(queryClient)); + + return ( + +
+ {/* Initial Welcome Message Area */} +
+

+ Good evening, {user?.name.split(" ")[0]} +

+

+ How can I help you today? +

+
+ + {/* Input Form and Suggestions - Always Visible */} +
+ + + + + + + ) => { + e.preventDefault(); + setInput(""); + const chat = await createChatMutation.mutateAsync(); + navigate(`/chat/${chat.id}`, { + state: { + chat, + message: input, + }, + }); + }} + elevation={2} + css={{ + padding: "16px", + display: "flex", + alignItems: "center", + width: "100%", + borderRadius: "12px", + border: `1px solid ${theme.palette.divider}`, + }} + > + ) => { + setInput(event.target.value); + }} + placeholder="Ask Coder..." + required + fullWidth + variant="outlined" + multiline + maxRows={5} + css={{ + marginRight: theme.spacing(1), + "& .MuiOutlinedInput-root": { + borderRadius: "8px", + padding: "10px 14px", + }, + }} + autoFocus + /> + + + + +
+
+
+ ); +}; + +export default ChatLanding; diff --git a/site/src/pages/ChatPage/ChatLayout.tsx b/site/src/pages/ChatPage/ChatLayout.tsx new file mode 100644 index 0000000000000..77de96af01595 --- /dev/null +++ b/site/src/pages/ChatPage/ChatLayout.tsx @@ -0,0 +1,246 @@ +import { useTheme } from "@emotion/react"; +import AddIcon from "@mui/icons-material/Add"; +import Button from "@mui/material/Button"; +import List from "@mui/material/List"; +import ListItem from "@mui/material/ListItem"; +import ListItemButton from "@mui/material/ListItemButton"; +import ListItemText from "@mui/material/ListItemText"; +import Paper from "@mui/material/Paper"; +import { createChat, getChats } from "api/queries/chats"; +import { deploymentLanguageModels } from "api/queries/deployment"; +import type { LanguageModelConfig } from "api/typesGenerated"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { Loader } from "components/Loader/Loader"; +import { Margins } from "components/Margins/Margins"; +import { useAgenticChat } from "contexts/useAgenticChat"; +import { + type FC, + type PropsWithChildren, + createContext, + useContext, + useEffect, + useState, +} from "react"; +import { useMutation, useQuery, useQueryClient } from "react-query"; +import { Link, Outlet, useNavigate, useParams } from "react-router-dom"; + +export interface ChatContext { + selectedModel: string; + modelConfig: LanguageModelConfig; + + setSelectedModel: (model: string) => void; +} +export const useChatContext = (): ChatContext => { + const context = useContext(ChatContext); + if (!context) { + throw new Error("useChatContext must be used within a ChatProvider"); + } + return context; +}; + +export const ChatContext = createContext(undefined); + +const SELECTED_MODEL_KEY = "coder_chat_selected_model"; + +const ChatProvider: FC = ({ children }) => { + const [selectedModel, setSelectedModel] = useState(() => { + const savedModel = localStorage.getItem(SELECTED_MODEL_KEY); + return savedModel || ""; + }); + const modelConfigQuery = useQuery(deploymentLanguageModels()); + useEffect(() => { + if (!modelConfigQuery.data) { + return; + } + if (selectedModel === "") { + const firstModel = modelConfigQuery.data.models[0]?.id; // Handle empty models array + if (firstModel) { + setSelectedModel(firstModel); + localStorage.setItem(SELECTED_MODEL_KEY, firstModel); + } + } + }, [modelConfigQuery.data, selectedModel]); + + if (modelConfigQuery.error) { + return ; + } + + if (!modelConfigQuery.data) { + return ; + } + + const handleSetSelectedModel = (model: string) => { + setSelectedModel(model); + localStorage.setItem(SELECTED_MODEL_KEY, model); + }; + + return ( + + {children} + + ); +}; + +export const ChatLayout: FC = () => { + const agenticChat = useAgenticChat(); + const queryClient = useQueryClient(); + const { data: chats, isLoading: chatsLoading } = useQuery(getChats()); + const createChatMutation = useMutation(createChat(queryClient)); + const theme = useTheme(); + const navigate = useNavigate(); + const { chatID } = useParams<{ chatID?: string }>(); + + const handleNewChat = () => { + navigate("/chat"); + }; + + if (!agenticChat.enabled) { + return ( + +
+

Agentic Chat is not enabled

+

+ Agentic Chat is an experimental feature and is not enabled by + default. Please contact your administrator for more information. +

+
+
+ ); + } + + return ( + // Outermost container: controls height and prevents page scroll +
+ {/* Sidebar Container (using Paper for background/border) */} + + {/* Sidebar Header */} +
+ {/* Replaced Typography with div + styling */} +
+ Chats +
+ +
+ {/* Sidebar Scrollable List Area */} +
+ {chatsLoading ? ( + + ) : chats && chats.length > 0 ? ( + + {chats.map((chat) => ( + + + + + + ))} + + ) : ( + // Replaced Typography with div + styling +
+ No chats yet. Start a new one! +
+ )} +
+
+ + {/* Main Content Area Container */} +
+ + {/* Outlet renders ChatMessages, which should have its own internal scroll */} + + +
+
+ ); +}; diff --git a/site/src/pages/ChatPage/ChatMessages.tsx b/site/src/pages/ChatPage/ChatMessages.tsx new file mode 100644 index 0000000000000..928b3c9ee2724 --- /dev/null +++ b/site/src/pages/ChatPage/ChatMessages.tsx @@ -0,0 +1,491 @@ +import { type Message, useChat } from "@ai-sdk/react"; +import { type Theme, keyframes, useTheme } from "@emotion/react"; +import SendIcon from "@mui/icons-material/Send"; +import IconButton from "@mui/material/IconButton"; +import Paper from "@mui/material/Paper"; +import TextField from "@mui/material/TextField"; +import { getChatMessages } from "api/queries/chats"; +import type { ChatMessage, CreateChatMessageRequest } from "api/typesGenerated"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { Loader } from "components/Loader/Loader"; +import { + type FC, + type KeyboardEvent, + memo, + useCallback, + useEffect, + useRef, +} from "react"; +import ReactMarkdown from "react-markdown"; +import { useQuery } from "react-query"; +import { useLocation, useParams } from "react-router-dom"; +import rehypeRaw from "rehype-raw"; +import remarkGfm from "remark-gfm"; +import type { ChatLandingLocationState } from "./ChatLanding"; +import { useChatContext } from "./ChatLayout"; +import { ChatToolInvocation } from "./ChatToolInvocation"; +import { LanguageModelSelector } from "./LanguageModelSelector"; + +const fadeIn = keyframes` + from { + opacity: 0; + transform: translateY(5px); + } + to { + opacity: 1; + transform: translateY(0); + } +`; + +const renderReasoning = (reasoning: string, theme: Theme) => ( +
+
+ 💭 Reasoning: +
+
+ {reasoning} +
+
+); + +interface MessageBubbleProps { + message: Message; +} + +const MessageBubble: FC = memo(({ message }) => { + const theme = useTheme(); + const isUser = message.role === "user"; + + return ( +
+ code)": { + backgroundColor: isUser + ? theme.palette.grey[700] + : theme.palette.action.hover, + color: isUser ? theme.palette.grey[50] : theme.palette.text.primary, + padding: theme.spacing(0.25, 0.75), + borderRadius: "4px", + fontSize: "0.875em", + fontFamily: "monospace", + }, + "& pre": { + backgroundColor: isUser + ? theme.palette.common.black + : theme.palette.grey[100], + color: isUser + ? theme.palette.grey[100] + : theme.palette.text.primary, + padding: theme.spacing(1.5), + borderRadius: "8px", + overflowX: "auto", + margin: theme.spacing(1.5, 0), + width: "100%", + "& code": { + backgroundColor: "transparent", + padding: 0, + fontSize: "0.875em", + fontFamily: "monospace", + color: "inherit", + }, + }, + "& a": { + color: isUser + ? theme.palette.grey[100] + : theme.palette.primary.main, + textDecoration: "underline", + fontWeight: 500, + "&:hover": { + textDecoration: "none", + color: isUser + ? theme.palette.grey[300] + : theme.palette.primary.dark, + }, + }, + }} + > + {message.role === "assistant" && message.parts ? ( +
+ {message.parts.map((part) => { + switch (part.type) { + case "text": + return ( + + {part.text} + + ); + case "tool-invocation": + return ( +
+ +
+ ); + case "reasoning": + return ( +
+ {renderReasoning(part.reasoning, theme)} +
+ ); + default: + return null; + } + })} +
+ ) : ( + + {message.content} + + )} +
+
+ ); +}); + +interface ChatViewProps { + messages: Message[]; + input: string; + handleInputChange: React.ChangeEventHandler< + HTMLInputElement | HTMLTextAreaElement + >; + handleSubmit: (e?: React.FormEvent) => void; + isLoading: boolean; + chatID: string; +} + +const ChatView: FC = ({ + messages, + input, + handleInputChange, + handleSubmit, + isLoading, +}) => { + const theme = useTheme(); + const messagesEndRef = useRef(null); + const inputRef = useRef(null); + const chatContext = useChatContext(); + + useEffect(() => { + const timer = setTimeout(() => { + messagesEndRef.current?.scrollIntoView({ + behavior: "smooth", + block: "end", + }); + }, 50); + return () => clearTimeout(timer); + }, []); + + useEffect(() => { + inputRef.current?.focus(); + }, []); + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Enter" && !event.shiftKey) { + event.preventDefault(); + handleSubmit(); + } + }; + + return ( +
+
+
+ {messages.map((message) => ( + + ))} +
+
+
+ +
+ +
+ +
+ + + + +
+
+
+ ); +}; + +export const ChatMessages: FC = () => { + const { chatID } = useParams(); + if (!chatID) { + throw new Error("Chat ID is required in URL path /chat/:chatID"); + } + + const { state } = useLocation(); + const transferredState = state as ChatLandingLocationState | undefined; + + const messagesQuery = useQuery(getChatMessages(chatID)); + + const chatContext = useChatContext(); + + const { + messages, + input, + handleInputChange, + handleSubmit: originalHandleSubmit, + isLoading, + setInput, + setMessages, + } = useChat({ + id: chatID, + api: `/api/v2/chats/${chatID}/messages`, + experimental_prepareRequestBody: (options): CreateChatMessageRequest => { + const userMessages = options.messages.filter( + (message) => message.role === "user", + ); + const mostRecentUserMessage = userMessages.at(-1); + return { + model: chatContext.selectedModel, + message: mostRecentUserMessage, + thinking: false, + }; + }, + initialInput: transferredState?.message, + initialMessages: messagesQuery.data as Message[] | undefined, + }); + + // Update messages from query data when it loads + useEffect(() => { + if (messagesQuery.data && messages.length === 0) { + setMessages(messagesQuery.data as Message[]); + } + }, [messagesQuery.data, messages.length, setMessages]); + + const handleSubmitCallback = useCallback( + (e?: React.FormEvent) => { + if (e) e.preventDefault(); + if (!input.trim()) return; + originalHandleSubmit(); + setInput(""); // Clear input after submit + }, + [input, originalHandleSubmit, setInput], + ); + + // Clear input and potentially submit on initial load with message + useEffect(() => { + if (transferredState?.message && input === transferredState.message) { + // Prevent submitting if messages already exist (e.g., browser back/forward) + if (messages.length === (messagesQuery.data?.length ?? 0)) { + handleSubmitCallback(); // Use the correct callback name + } + // Clear the state to prevent re-submission on subsequent renders/navigation + window.history.replaceState({}, document.title); + } + }, [ + transferredState?.message, + input, + handleSubmitCallback, + messages.length, + messagesQuery.data?.length, + ]); // Use the correct callback name + + useEffect(() => { + if (transferredState?.message) { + // Logic potentially related to transferredState can go here if needed, + } + }, [transferredState?.message]); + + if (messagesQuery.error) { + return ; + } + + if (messagesQuery.isLoading && messages.length === 0) { + return ; + } + + return ( + + ); +}; diff --git a/site/src/pages/ChatPage/ChatToolInvocation.stories.tsx b/site/src/pages/ChatPage/ChatToolInvocation.stories.tsx new file mode 100644 index 0000000000000..03bf31cb095fb --- /dev/null +++ b/site/src/pages/ChatPage/ChatToolInvocation.stories.tsx @@ -0,0 +1,1211 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { + MockStartingWorkspace, + MockStoppedWorkspace, + MockStoppingWorkspace, + MockTemplate, + MockTemplateVersion, + MockUserMember, + MockWorkspace, + MockWorkspaceBuild, +} from "testHelpers/entities"; +import { ChatToolInvocation } from "./ChatToolInvocation"; + +const meta: Meta = { + title: "pages/ChatPage/ChatToolInvocation", + component: ChatToolInvocation, +}; + +export default meta; +type Story = StoryObj; + +export const GetWorkspace: Story = { + render: () => + renderInvocations( + "coder_get_workspace", + { + workspace_id: MockWorkspace.id, + }, + MockWorkspace, + ), +}; + +export const CreateWorkspace: Story = { + render: () => + renderInvocations( + "coder_create_workspace", + { + name: MockWorkspace.name, + rich_parameters: {}, + template_version_id: MockWorkspace.template_active_version_id, + user: MockWorkspace.owner_name, + }, + MockWorkspace, + ), +}; + +export const ListWorkspaces: Story = { + render: () => + renderInvocations( + "coder_list_workspaces", + { + owner: "me", + }, + [ + MockWorkspace, + MockStoppedWorkspace, + MockStoppingWorkspace, + MockStartingWorkspace, + ], + ), +}; + +export const ListTemplates: Story = { + render: () => + renderInvocations("coder_list_templates", {}, [ + { + id: MockTemplate.id, + name: MockTemplate.name, + description: MockTemplate.description, + active_version_id: MockTemplate.active_version_id, + active_user_count: MockTemplate.active_user_count, + }, + { + id: "another-template", + name: "Another Template", + description: "A different template for testing purposes.", + active_version_id: "v2.0", + active_user_count: 5, + }, + ]), +}; + +export const TemplateVersionParameters: Story = { + render: () => + renderInvocations( + "coder_template_version_parameters", + { + template_version_id: MockTemplateVersion.id, + }, + [ + { + name: "region", + display_name: "Region", + description: "Select the deployment region.", + description_plaintext: "Select the deployment region.", + type: "string", + mutable: false, + default_value: "us-west-1", + icon: "", + options: [ + { name: "US West", description: "", value: "us-west-1", icon: "" }, + { name: "US East", description: "", value: "us-east-1", icon: "" }, + ], + required: true, + ephemeral: false, + }, + { + name: "cpu_cores", + display_name: "CPU Cores", + description: "Number of CPU cores.", + description_plaintext: "Number of CPU cores.", + type: "number", + mutable: true, + default_value: "4", + icon: "", + options: [], + required: false, + ephemeral: false, + }, + ], + ), +}; + +export const GetAuthenticatedUser: Story = { + render: () => + renderInvocations("coder_get_authenticated_user", {}, MockUserMember), +}; + +export const CreateWorkspaceBuild: Story = { + render: () => + renderInvocations( + "coder_create_workspace_build", + { + workspace_id: MockWorkspace.id, + transition: "start", + }, + MockWorkspaceBuild, + ), +}; + +export const CreateTemplateVersion: Story = { + render: () => + renderInvocations( + "coder_create_template_version", + { + template_id: MockTemplate.id, + file_id: "file-123", + }, + MockTemplateVersion, + ), +}; + +const mockLogs = [ + "[INFO] Starting build process...", + "[DEBUG] Reading configuration file.", + "[WARN] Deprecated setting detected.", + "[INFO] Applying changes...", + "[ERROR] Failed to connect to database.", +]; + +export const GetWorkspaceAgentLogs: Story = { + render: () => + renderInvocations( + "coder_get_workspace_agent_logs", + { + workspace_agent_id: "agent-456", + }, + mockLogs, + ), +}; + +export const GetWorkspaceBuildLogs: Story = { + render: () => + renderInvocations( + "coder_get_workspace_build_logs", + { + workspace_build_id: MockWorkspaceBuild.id, + }, + mockLogs, + ), +}; + +export const GetTemplateVersionLogs: Story = { + render: () => + renderInvocations( + "coder_get_template_version_logs", + { + template_version_id: MockTemplateVersion.id, + }, + mockLogs, + ), +}; + +export const UpdateTemplateActiveVersion: Story = { + render: () => + renderInvocations( + "coder_update_template_active_version", + { + template_id: MockTemplate.id, + template_version_id: MockTemplateVersion.id, + }, + `Successfully updated active version for template ${MockTemplate.name}.`, + ), +}; + +export const UploadTarFile: Story = { + render: () => + renderInvocations( + "coder_upload_tar_file", + { + files: { "main.tf": templateTerraform, Dockerfile: templateDockerfile }, + }, + { + hash: "sha256:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2", + }, + ), +}; + +export const CreateTemplate: Story = { + render: () => + renderInvocations( + "coder_create_template", + { + name: "new-template", + }, + MockTemplate, + ), +}; + +export const DeleteTemplate: Story = { + render: () => + renderInvocations( + "coder_delete_template", + { + template_id: MockTemplate.id, + }, + `Successfully deleted template ${MockTemplate.name}.`, + ), +}; + +export const GetTemplateVersion: Story = { + render: () => + renderInvocations( + "coder_get_template_version", + { + template_version_id: MockTemplateVersion.id, + }, + MockTemplateVersion, + ), +}; + +export const DownloadTarFile: Story = { + render: () => + renderInvocations( + "coder_download_tar_file", + { + file_id: "file-789", + }, + { "main.tf": templateTerraform, "README.md": "# My Template\n" }, + ), +}; + +const renderInvocations = ( + toolName: T, + args: Extract["args"], + result: Extract< + ChatToolInvocation, + { toolName: T; state: "result" } + >["result"], + error?: string, +) => { + return ( + <> + + + + + + ); +}; + +const templateDockerfile = `FROM rust:slim@sha256:9abf10cc84dfad6ace1b0aae3951dc5200f467c593394288c11db1e17bb4d349 AS rust-utils +# Install rust helper programs +# ENV CARGO_NET_GIT_FETCH_WITH_CLI=true +ENV CARGO_INSTALL_ROOT=/tmp/ +RUN cargo install typos-cli watchexec-cli && \ + # Reduce image size. + rm -rf /usr/local/cargo/registry + +FROM ubuntu:jammy@sha256:0e5e4a57c2499249aafc3b40fcd541e9a456aab7296681a3994d631587203f97 AS go + +# Install Go manually, so that we can control the version +ARG GO_VERSION=1.24.1 + +# Boring Go is needed to build FIPS-compliant binaries. +RUN apt-get update && \ + apt-get install --yes curl && \ + curl --silent --show-error --location \ + "https://go.dev/dl/go\${GO_VERSION}.linux-amd64.tar.gz" \ + -o /usr/local/go.tar.gz && \ + rm -rf /var/lib/apt/lists/* + +ENV PATH=$PATH:/usr/local/go/bin +ARG GOPATH="/tmp/" +# Install Go utilities. +RUN apt-get update && \ + apt-get install --yes gcc && \ + mkdir --parents /usr/local/go && \ + tar --extract --gzip --directory=/usr/local/go --file=/usr/local/go.tar.gz --strip-components=1 && \ + mkdir --parents "$GOPATH" && \ + # moq for Go tests. + go install github.com/matryer/moq@v0.2.3 && \ + # swag for Swagger doc generation + go install github.com/swaggo/swag/cmd/swag@v1.7.4 && \ + # go-swagger tool to generate the go coder api client + go install github.com/go-swagger/go-swagger/cmd/swagger@v0.28.0 && \ + # goimports for updating imports + go install golang.org/x/tools/cmd/goimports@v0.31.0 && \ + # protoc-gen-go is needed to build sysbox from source + go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.30 && \ + # drpc support for v2 + go install storj.io/drpc/cmd/protoc-gen-go-drpc@v0.0.34 && \ + # migrate for migration support for v2 + go install github.com/golang-migrate/migrate/v4/cmd/migrate@v4.15.1 && \ + # goreleaser for compiling v2 binaries + go install github.com/goreleaser/goreleaser@v1.6.1 && \ + # Install the latest version of gopls for editors that support + # the language server protocol + go install golang.org/x/tools/gopls@v0.18.1 && \ + # gotestsum makes test output more readable + go install gotest.tools/gotestsum@v1.9.0 && \ + # goveralls collects code coverage metrics from tests + # and sends to Coveralls + go install github.com/mattn/goveralls@v0.0.11 && \ + # kind for running Kubernetes-in-Docker, needed for tests + go install sigs.k8s.io/kind@v0.10.0 && \ + # helm-docs generates our Helm README based on a template and the + # charts and values files + go install github.com/norwoodj/helm-docs/cmd/helm-docs@v1.5.0 && \ + # sqlc for Go code generation + (CGO_ENABLED=1 go install github.com/sqlc-dev/sqlc/cmd/sqlc@v1.27.0) && \ + # gcr-cleaner-cli used by CI to prune unused images + go install github.com/sethvargo/gcr-cleaner/cmd/gcr-cleaner-cli@v0.5.1 && \ + # ruleguard for checking custom rules, without needing to run all of + # golangci-lint. Check the go.mod in the release of golangci-lint that + # we're using for the version of go-critic that it embeds, then check + # the version of ruleguard in go-critic for that tag. + go install github.com/quasilyte/go-ruleguard/cmd/ruleguard@v0.3.13 && \ + # go-releaser for building 'fat binaries' that work cross-platform + go install github.com/goreleaser/goreleaser@v1.6.1 && \ + go install mvdan.cc/sh/v3/cmd/shfmt@v3.7.0 && \ + # nfpm is used with \`make build\` to make release packages + go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.35.1 && \ + # yq v4 is used to process yaml files in coder v2. Conflicts with + # yq v3 used in v1. + go install github.com/mikefarah/yq/v4@v4.44.3 && \ + mv /tmp/bin/yq /tmp/bin/yq4 && \ + go install go.uber.org/mock/mockgen@v0.5.0 && \ + # Reduce image size. + apt-get remove --yes gcc && \ + apt-get autoremove --yes && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* && \ + rm -rf /usr/local/go && \ + rm -rf /tmp/go/pkg && \ + rm -rf /tmp/go/src + +# alpine:3.18 +FROM gcr.io/coder-dev-1/alpine@sha256:25fad2a32ad1f6f510e528448ae1ec69a28ef81916a004d3629874104f8a7f70 AS proto +WORKDIR /tmp +RUN apk add curl unzip +RUN curl -L -o protoc.zip https://github.com/protocolbuffers/protobuf/releases/download/v23.4/protoc-23.4-linux-x86_64.zip && \ + unzip protoc.zip && \ + rm protoc.zip + +FROM ubuntu:jammy@sha256:0e5e4a57c2499249aafc3b40fcd541e9a456aab7296681a3994d631587203f97 + +SHELL ["/bin/bash", "-c"] + +# Install packages from apt repositories +ARG DEBIAN_FRONTEND="noninteractive" + +# Updated certificates are necessary to use the teraswitch mirror. +# This must be ran before copying in configuration since the config replaces +# the default mirror with teraswitch. +# Also enable the en_US.UTF-8 locale so that we don't generate multiple locales +# and unminimize to include man pages. +RUN apt-get update && \ + apt-get install --yes ca-certificates locales && \ + echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen && \ + locale-gen && \ + yes | unminimize + +COPY files / + +# We used to copy /etc/sudoers.d/* in from files/ but this causes issues with +# permissions and layer caching. Instead, create the file directly. +RUN mkdir -p /etc/sudoers.d && \ + echo 'coder ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/nopasswd && \ + chmod 750 /etc/sudoers.d/ && \ + chmod 640 /etc/sudoers.d/nopasswd + +RUN apt-get update --quiet && apt-get install --yes \ + ansible \ + apt-transport-https \ + apt-utils \ + asciinema \ + bash \ + bash-completion \ + bat \ + bats \ + bind9-dnsutils \ + build-essential \ + ca-certificates \ + cargo \ + cmake \ + containerd.io \ + crypto-policies \ + curl \ + docker-ce \ + docker-ce-cli \ + docker-compose-plugin \ + exa \ + fd-find \ + file \ + fish \ + gettext-base \ + git \ + gnupg \ + google-cloud-sdk \ + google-cloud-sdk-datastore-emulator \ + graphviz \ + helix \ + htop \ + httpie \ + inetutils-tools \ + iproute2 \ + iputils-ping \ + iputils-tracepath \ + jq \ + kubectl \ + language-pack-en \ + less \ + libgbm-dev \ + libssl-dev \ + lsb-release \ + lsof \ + man \ + meld \ + ncdu \ + neovim \ + net-tools \ + openjdk-11-jdk-headless \ + openssh-server \ + openssl \ + packer \ + pkg-config \ + postgresql-16 \ + python3 \ + python3-pip \ + ripgrep \ + rsync \ + screen \ + shellcheck \ + strace \ + sudo \ + tcptraceroute \ + termshark \ + traceroute \ + unzip \ + vim \ + wget \ + xauth \ + zip \ + zsh \ + zstd && \ + # Delete package cache to avoid consuming space in layer + apt-get clean && \ + # Configure FIPS-compliant policies + update-crypto-policies --set FIPS + +# NOTE: In scripts/Dockerfile.base we specifically install Terraform version 1.11.3. +# Installing the same version here to match. +RUN wget -O /tmp/terraform.zip "https://releases.hashicorp.com/terraform/1.11.3/terraform_1.11.3_linux_amd64.zip" && \ + unzip /tmp/terraform.zip -d /usr/local/bin && \ + rm -f /tmp/terraform.zip && \ + chmod +x /usr/local/bin/terraform && \ + terraform --version + +# Install the docker buildx component. +RUN DOCKER_BUILDX_VERSION=$(curl -s "https://api.github.com/repos/docker/buildx/releases/latest" | grep '"tag_name":' | sed -E 's/.*"(v[^"]+)".*/\\1/') && \ + mkdir -p /usr/local/lib/docker/cli-plugins && \ + curl -Lo /usr/local/lib/docker/cli-plugins/docker-buildx "https://github.com/docker/buildx/releases/download/\${DOCKER_BUILDX_VERSION}/buildx-\${DOCKER_BUILDX_VERSION}.linux-amd64" && \ + chmod a+x /usr/local/lib/docker/cli-plugins/docker-buildx + +# See https://github.com/cli/cli/issues/6175#issuecomment-1235984381 for proof +# the apt repository is unreliable +RUN GH_CLI_VERSION=$(curl -s "https://api.github.com/repos/cli/cli/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\\1/') && \ + curl -L https://github.com/cli/cli/releases/download/v\${GH_CLI_VERSION}/gh_\${GH_CLI_VERSION}_linux_amd64.deb -o gh.deb && \ + dpkg -i gh.deb && \ + rm gh.deb + +# Install Lazygit +# See https://github.com/jesseduffield/lazygit#ubuntu +RUN LAZYGIT_VERSION=$(curl -s "https://api.github.com/repos/jesseduffield/lazygit/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v*([^"]+)".*/\\1/') && \ + curl -Lo lazygit.tar.gz "https://github.com/jesseduffield/lazygit/releases/latest/download/lazygit_\${LAZYGIT_VERSION}_Linux_x86_64.tar.gz" && \ + tar xf lazygit.tar.gz -C /usr/local/bin lazygit && \ + rm lazygit.tar.gz + +# Install doctl +# See https://docs.digitalocean.com/reference/doctl/how-to/install +RUN DOCTL_VERSION=$(curl -s "https://api.github.com/repos/digitalocean/doctl/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\\1/') && \ + curl -L https://github.com/digitalocean/doctl/releases/download/v\${DOCTL_VERSION}/doctl-\${DOCTL_VERSION}-linux-amd64.tar.gz -o doctl.tar.gz && \ + tar xf doctl.tar.gz -C /usr/local/bin doctl && \ + rm doctl.tar.gz + +ARG NVM_INSTALL_SHA=bdea8c52186c4dd12657e77e7515509cda5bf9fa5a2f0046bce749e62645076d +# Install frontend utilities +ENV NVM_DIR=/usr/local/nvm +ENV NODE_VERSION=20.16.0 +RUN mkdir -p $NVM_DIR +RUN curl -o nvm_install.sh https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh && \ + echo "\${NVM_INSTALL_SHA} nvm_install.sh" | sha256sum -c && \ + bash nvm_install.sh && \ + rm nvm_install.sh +RUN source $NVM_DIR/nvm.sh && \ + nvm install $NODE_VERSION && \ + nvm use $NODE_VERSION +ENV PATH=$NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH +# Allow patch updates for npm and pnpm +RUN npm install -g npm@10.8.1 --integrity=sha512-Dp1C6SvSMYQI7YHq/y2l94uvI+59Eqbu1EpuKQHQ8p16txXRuRit5gH3Lnaagk2aXDIjg/Iru9pd05bnneKgdw== +RUN npm install -g pnpm@9.15.1 --integrity=sha512-GstWXmGT7769p3JwKVBGkVDPErzHZCYudYfnHRncmKQj3/lTblfqRMSb33kP9pToPCe+X6oj1n4MAztYO+S/zw== + +RUN pnpx playwright@1.47.0 install --with-deps chromium + +# Ensure PostgreSQL binaries are in the users $PATH. +RUN update-alternatives --install /usr/local/bin/initdb initdb /usr/lib/postgresql/16/bin/initdb 100 && \ + update-alternatives --install /usr/local/bin/postgres postgres /usr/lib/postgresql/16/bin/postgres 100 + +# Create links for injected dependencies +RUN ln --symbolic /var/tmp/coder/coder-cli/coder /usr/local/bin/coder && \ + ln --symbolic /var/tmp/coder/code-server/bin/code-server /usr/local/bin/code-server + +# Disable the PostgreSQL systemd service. +# Coder uses a custom timescale container to test the database instead. +RUN systemctl disable \ + postgresql + +# Configure systemd services for CVMs +RUN systemctl enable \ + docker \ + ssh && \ + # Workaround for envbuilder cache probing not working unless the filesystem is modified. + touch /tmp/.envbuilder-systemctl-enable-docker-ssh-workaround + +# Install tools with published releases, where that is the +# preferred/recommended installation method. +ARG CLOUD_SQL_PROXY_VERSION=2.2.0 \ + DIVE_VERSION=0.10.0 \ + DOCKER_GCR_VERSION=2.1.8 \ + GOLANGCI_LINT_VERSION=1.64.8 \ + GRYPE_VERSION=0.61.1 \ + HELM_VERSION=3.12.0 \ + KUBE_LINTER_VERSION=0.6.3 \ + KUBECTX_VERSION=0.9.4 \ + STRIPE_VERSION=1.14.5 \ + TERRAGRUNT_VERSION=0.45.11 \ + TRIVY_VERSION=0.41.0 \ + SYFT_VERSION=1.20.0 \ + COSIGN_VERSION=2.4.3 + +# cloud_sql_proxy, for connecting to cloudsql instances +# the upstream go.mod prevents this from being installed with go install +RUN curl --silent --show-error --location --output /usr/local/bin/cloud_sql_proxy "https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v\${CLOUD_SQL_PROXY_VERSION}/cloud-sql-proxy.linux.amd64" && \ + chmod a=rx /usr/local/bin/cloud_sql_proxy && \ + # dive for scanning image layer utilization metrics in CI + curl --silent --show-error --location "https://github.com/wagoodman/dive/releases/download/v\${DIVE_VERSION}/dive_\${DIVE_VERSION}_linux_amd64.tar.gz" | \ + tar --extract --gzip --directory=/usr/local/bin --file=- dive && \ + # docker-credential-gcr is a Docker credential helper for pushing/pulling + # images from Google Container Registry and Artifact Registry + curl --silent --show-error --location "https://github.com/GoogleCloudPlatform/docker-credential-gcr/releases/download/v\${DOCKER_GCR_VERSION}/docker-credential-gcr_linux_amd64-\${DOCKER_GCR_VERSION}.tar.gz" | \ + tar --extract --gzip --directory=/usr/local/bin --file=- docker-credential-gcr && \ + # golangci-lint performs static code analysis for our Go code + curl --silent --show-error --location "https://github.com/golangci/golangci-lint/releases/download/v\${GOLANGCI_LINT_VERSION}/golangci-lint-\${GOLANGCI_LINT_VERSION}-linux-amd64.tar.gz" | \ + tar --extract --gzip --directory=/usr/local/bin --file=- --strip-components=1 "golangci-lint-\${GOLANGCI_LINT_VERSION}-linux-amd64/golangci-lint" && \ + # Anchore Grype for scanning container images for security issues + curl --silent --show-error --location "https://github.com/anchore/grype/releases/download/v\${GRYPE_VERSION}/grype_\${GRYPE_VERSION}_linux_amd64.tar.gz" | \ + tar --extract --gzip --directory=/usr/local/bin --file=- grype && \ + # Helm is necessary for deploying Coder + curl --silent --show-error --location "https://get.helm.sh/helm-v\${HELM_VERSION}-linux-amd64.tar.gz" | \ + tar --extract --gzip --directory=/usr/local/bin --file=- --strip-components=1 linux-amd64/helm && \ + # kube-linter for linting Kubernetes objects, including those + # that Helm generates from our charts + curl --silent --show-error --location "https://github.com/stackrox/kube-linter/releases/download/\${KUBE_LINTER_VERSION}/kube-linter-linux" --output /usr/local/bin/kube-linter && \ + # kubens and kubectx for managing Kubernetes namespaces and contexts + curl --silent --show-error --location "https://github.com/ahmetb/kubectx/releases/download/v\${KUBECTX_VERSION}/kubectx_v\${KUBECTX_VERSION}_linux_x86_64.tar.gz" | \ + tar --extract --gzip --directory=/usr/local/bin --file=- kubectx && \ + curl --silent --show-error --location "https://github.com/ahmetb/kubectx/releases/download/v\${KUBECTX_VERSION}/kubens_v\${KUBECTX_VERSION}_linux_x86_64.tar.gz" | \ + tar --extract --gzip --directory=/usr/local/bin --file=- kubens && \ + # stripe for coder.com billing API + curl --silent --show-error --location "https://github.com/stripe/stripe-cli/releases/download/v\${STRIPE_VERSION}/stripe_\${STRIPE_VERSION}_linux_x86_64.tar.gz" | \ + tar --extract --gzip --directory=/usr/local/bin --file=- stripe && \ + # terragrunt for running Terraform and Terragrunt files + curl --silent --show-error --location --output /usr/local/bin/terragrunt "https://github.com/gruntwork-io/terragrunt/releases/download/v\${TERRAGRUNT_VERSION}/terragrunt_linux_amd64" && \ + chmod a=rx /usr/local/bin/terragrunt && \ + # AquaSec Trivy for scanning container images for security issues + curl --silent --show-error --location "https://github.com/aquasecurity/trivy/releases/download/v\${TRIVY_VERSION}/trivy_\${TRIVY_VERSION}_Linux-64bit.tar.gz" | \ + tar --extract --gzip --directory=/usr/local/bin --file=- trivy && \ + # Anchore Syft for SBOM generation + curl --silent --show-error --location "https://github.com/anchore/syft/releases/download/v\${SYFT_VERSION}/syft_\${SYFT_VERSION}_linux_amd64.tar.gz" | \ + tar --extract --gzip --directory=/usr/local/bin --file=- syft && \ + # Sigstore Cosign for artifact signing and attestation + curl --silent --show-error --location --output /usr/local/bin/cosign "https://github.com/sigstore/cosign/releases/download/v\${COSIGN_VERSION}/cosign-linux-amd64" && \ + chmod a=rx /usr/local/bin/cosign + +# We use yq during "make deploy" to manually substitute out fields in +# our helm values.yaml file. See https://github.com/helm/helm/issues/3141 +# +# TODO: update to 4.x, we can't do this now because it included breaking +# changes (yq w doesn't work anymore) +# RUN curl --silent --show-error --location "https://github.com/mikefarah/yq/releases/download/v4.9.0/yq_linux_amd64.tar.gz" | \ +# tar --extract --gzip --directory=/usr/local/bin --file=- ./yq_linux_amd64 && \ +# mv /usr/local/bin/yq_linux_amd64 /usr/local/bin/yq + +RUN curl --silent --show-error --location --output /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/download/3.3.0/yq_linux_amd64" && \ + chmod a=rx /usr/local/bin/yq + +# Install GoLand. +RUN mkdir --parents /usr/local/goland && \ + curl --silent --show-error --location "https://download.jetbrains.com/go/goland-2021.2.tar.gz" | \ + tar --extract --gzip --directory=/usr/local/goland --file=- --strip-components=1 && \ + ln --symbolic /usr/local/goland/bin/goland.sh /usr/local/bin/goland + +# Install Antlrv4, needed to generate paramlang lexer/parser +RUN curl --silent --show-error --location --output /usr/local/lib/antlr-4.9.2-complete.jar "https://www.antlr.org/download/antlr-4.9.2-complete.jar" +ENV CLASSPATH="/usr/local/lib/antlr-4.9.2-complete.jar:\${PATH}" + +# Add coder user and allow use of docker/sudo +RUN useradd coder \ + --create-home \ + --shell=/bin/bash \ + --groups=docker \ + --uid=1000 \ + --user-group + +# Adjust OpenSSH config +RUN echo "PermitUserEnvironment yes" >>/etc/ssh/sshd_config && \ + echo "X11Forwarding yes" >>/etc/ssh/sshd_config && \ + echo "X11UseLocalhost no" >>/etc/ssh/sshd_config + +# We avoid copying the extracted directory since COPY slows to minutes when there +# are a lot of small files. +COPY --from=go /usr/local/go.tar.gz /usr/local/go.tar.gz +RUN mkdir /usr/local/go && \ + tar --extract --gzip --directory=/usr/local/go --file=/usr/local/go.tar.gz --strip-components=1 + +ENV PATH=$PATH:/usr/local/go/bin + +RUN update-alternatives --install /usr/local/bin/gofmt gofmt /usr/local/go/bin/gofmt 100 + +COPY --from=go /tmp/bin /usr/local/bin +COPY --from=rust-utils /tmp/bin /usr/local/bin +COPY --from=proto /tmp/bin /usr/local/bin +COPY --from=proto /tmp/include /usr/local/bin/include + +USER coder + +# Ensure go bins are in the 'coder' user's path. Note that no go bins are +# installed in this docker file, as they'd be mounted over by the persistent +# home volume. +ENV PATH="/home/coder/go/bin:\${PATH}" + +# This setting prevents Go from using the public checksum database for +# our module path prefixes. It is required because these are in private +# repositories that require authentication. +# +# For details, see: https://golang.org/ref/mod#private-modules +ENV GOPRIVATE="coder.com,cdr.dev,go.coder.com,github.com/cdr,github.com/coder" + +# Increase memory allocation to NodeJS +ENV NODE_OPTIONS="--max-old-space-size=8192" +`; + +const templateTerraform = `terraform { + required_providers { + coder = { + source = "coder/coder" + version = "2.2.0-pre0" + } + docker = { + source = "kreuzwerker/docker" + version = "~> 3.0.0" + } + } +} + +locals { + // These are cluster service addresses mapped to Tailscale nodes. Ask Dean or + // Kyle for help. + docker_host = { + "" = "tcp://dogfood-ts-cdr-dev.tailscale.svc.cluster.local:2375" + "us-pittsburgh" = "tcp://dogfood-ts-cdr-dev.tailscale.svc.cluster.local:2375" + // For legacy reasons, this host is labelled \`eu-helsinki\` but it's + // actually in Germany now. + "eu-helsinki" = "tcp://katerose-fsn-cdr-dev.tailscale.svc.cluster.local:2375" + "ap-sydney" = "tcp://wolfgang-syd-cdr-dev.tailscale.svc.cluster.local:2375" + "sa-saopaulo" = "tcp://oberstein-sao-cdr-dev.tailscale.svc.cluster.local:2375" + "za-cpt" = "tcp://schonkopf-cpt-cdr-dev.tailscale.svc.cluster.local:2375" + } + + repo_base_dir = data.coder_parameter.repo_base_dir.value == "~" ? "/home/coder" : replace(data.coder_parameter.repo_base_dir.value, "/^~\\//", "/home/coder/") + repo_dir = replace(try(module.git-clone[0].repo_dir, ""), "/^~\\//", "/home/coder/") + container_name = "coder-\${data.coder_workspace_owner.me.name}-\${lower(data.coder_workspace.me.name)}" +} + +data "coder_parameter" "repo_base_dir" { + type = "string" + name = "Coder Repository Base Directory" + default = "~" + description = "The directory specified will be created (if missing) and [coder/coder](https://github.com/coder/coder) will be automatically cloned into [base directory]/coder 🪄." + mutable = true +} + +data "coder_parameter" "image_type" { + type = "string" + name = "Coder Image" + default = "codercom/oss-dogfood:latest" + description = "The Docker image used to run your workspace. Choose between nix and non-nix images." + option { + icon = "/icon/coder.svg" + name = "Dogfood (Default)" + value = "codercom/oss-dogfood:latest" + } + option { + icon = "/icon/nix.svg" + name = "Dogfood Nix (Experimental)" + value = "codercom/oss-dogfood-nix:latest" + } +} + +data "coder_parameter" "region" { + type = "string" + name = "Region" + icon = "/emojis/1f30e.png" + default = "us-pittsburgh" + option { + icon = "/emojis/1f1fa-1f1f8.png" + name = "Pittsburgh" + value = "us-pittsburgh" + } + option { + icon = "/emojis/1f1e9-1f1ea.png" + name = "Falkenstein" + // For legacy reasons, this host is labelled \`eu-helsinki\` but it's + // actually in Germany now. + value = "eu-helsinki" + } + option { + icon = "/emojis/1f1e6-1f1fa.png" + name = "Sydney" + value = "ap-sydney" + } + option { + icon = "/emojis/1f1e7-1f1f7.png" + name = "São Paulo" + value = "sa-saopaulo" + } + option { + icon = "/emojis/1f1ff-1f1e6.png" + name = "Cape Town" + value = "za-cpt" + } +} + +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) +} + +provider "coder" {} + +data "coder_external_auth" "github" { + id = "github" +} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} +data "coder_workspace_tags" "tags" { + tags = { + "cluster" : "dogfood-v2" + "env" : "gke" + } +} + +module "slackme" { + count = data.coder_workspace.me.start_count + source = "dev.registry.coder.com/modules/slackme/coder" + version = ">= 1.0.0" + agent_id = coder_agent.dev.id + auth_provider_id = "slack" +} + +module "dotfiles" { + count = data.coder_workspace.me.start_count + source = "dev.registry.coder.com/modules/dotfiles/coder" + version = ">= 1.0.0" + agent_id = coder_agent.dev.id +} + +module "git-clone" { + count = data.coder_workspace.me.start_count + source = "dev.registry.coder.com/modules/git-clone/coder" + version = ">= 1.0.0" + agent_id = coder_agent.dev.id + url = "https://github.com/coder/coder" + base_dir = local.repo_base_dir +} + +module "personalize" { + count = data.coder_workspace.me.start_count + source = "dev.registry.coder.com/modules/personalize/coder" + version = ">= 1.0.0" + agent_id = coder_agent.dev.id +} + +module "code-server" { + count = data.coder_workspace.me.start_count + source = "dev.registry.coder.com/modules/code-server/coder" + version = ">= 1.0.0" + agent_id = coder_agent.dev.id + folder = local.repo_dir + auto_install_extensions = true +} + +module "vscode-web" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/vscode-web/coder" + version = ">= 1.0.0" + agent_id = coder_agent.dev.id + folder = local.repo_dir + extensions = ["github.copilot"] + auto_install_extensions = true # will install extensions from the repos .vscode/extensions.json file + accept_license = true +} + +module "jetbrains_gateway" { + count = data.coder_workspace.me.start_count + source = "dev.registry.coder.com/modules/jetbrains-gateway/coder" + version = ">= 1.0.0" + agent_id = coder_agent.dev.id + agent_name = "dev" + folder = local.repo_dir + jetbrains_ides = ["GO", "WS"] + default = "GO" + latest = true +} + +module "filebrowser" { + count = data.coder_workspace.me.start_count + source = "dev.registry.coder.com/modules/filebrowser/coder" + version = ">= 1.0.0" + agent_id = coder_agent.dev.id + agent_name = "dev" +} + +module "coder-login" { + count = data.coder_workspace.me.start_count + source = "dev.registry.coder.com/modules/coder-login/coder" + version = ">= 1.0.0" + agent_id = coder_agent.dev.id +} + +module "cursor" { + count = data.coder_workspace.me.start_count + source = "dev.registry.coder.com/modules/cursor/coder" + version = ">= 1.0.0" + agent_id = coder_agent.dev.id + folder = local.repo_dir +} + +module "zed" { + count = data.coder_workspace.me.start_count + source = "./zed" + agent_id = coder_agent.dev.id + folder = local.repo_dir +} + +resource "coder_agent" "dev" { + arch = "amd64" + os = "linux" + dir = local.repo_dir + env = { + OIDC_TOKEN : data.coder_workspace_owner.me.oidc_access_token, + } + startup_script_behavior = "blocking" + + # The following metadata blocks are optional. They are used to display + # information about your workspace in the dashboard. You can remove them + # if you don't want to display any information. + metadata { + display_name = "CPU Usage" + key = "cpu_usage" + order = 0 + script = "coder stat cpu" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "RAM Usage" + key = "ram_usage" + order = 1 + script = "coder stat mem" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "CPU Usage (Host)" + key = "cpu_usage_host" + order = 2 + script = "coder stat cpu --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "RAM Usage (Host)" + key = "ram_usage_host" + order = 3 + script = "coder stat mem --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Swap Usage (Host)" + 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. + trap 'touch /tmp/.coder-startup-script.done' EXIT + + # Start Docker service + sudo service docker start + # Install playwright dependencies + # We want to use the playwright version from site/package.json + # Check if the directory exists At workspace creation as the coder_script runs in parallel so clone might not exist yet. + 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 + EOT + + shutdown_script = <<-EOT + #!/usr/bin/env bash + set -eux -o pipefail + + # Stop the Docker service to prevent errors during workspace destroy. + sudo service docker stop + EOT +} + +# Add a cost so we get some quota usage in dev.coder.com +resource "coder_metadata" "home_volume" { + resource_id = docker_volume.home_volume.id + daily_cost = 1 +} + +resource "docker_volume" "home_volume" { + name = "coder-\${data.coder_workspace.me.id}-home" + # Protect the volume from being deleted due to changes in attributes. + lifecycle { + ignore_changes = all + } + # Add labels in Docker to keep track of orphan resources. + labels { + label = "coder.owner" + value = data.coder_workspace_owner.me.name + } + labels { + label = "coder.owner_id" + value = data.coder_workspace_owner.me.id + } + labels { + label = "coder.workspace_id" + value = data.coder_workspace.me.id + } + # This field becomes outdated if the workspace is renamed but can + # be useful for debugging or cleaning out dangling volumes. + labels { + label = "coder.workspace_name_at_creation" + value = data.coder_workspace.me.name + } +} + +data "docker_registry_image" "dogfood" { + name = data.coder_parameter.image_type.value +} + +resource "docker_image" "dogfood" { + name = "\${data.coder_parameter.image_type.value}@\${data.docker_registry_image.dogfood.sha256_digest}" + pull_triggers = [ + data.docker_registry_image.dogfood.sha256_digest, + sha1(join("", [for f in fileset(path.module, "files/*") : filesha1(f)])), + filesha1("Dockerfile"), + filesha1("nix.hash"), + ] + keep_locally = true +} + +resource "docker_container" "workspace" { + count = data.coder_workspace.me.start_count + image = docker_image.dogfood.name + name = local.container_name + # Hostname makes the shell more user friendly: coder@my-workspace:~$ + hostname = data.coder_workspace.me.name + # Use the docker gateway if the access URL is 127.0.0.1 + entrypoint = ["sh", "-c", coder_agent.dev.init_script] + # CPU limits are unnecessary since Docker will load balance automatically + memory = data.coder_workspace_owner.me.name == "code-asher" ? 65536 : 32768 + runtime = "sysbox-runc" + # Ensure the workspace is given time to execute shutdown scripts. + destroy_grace_seconds = 60 + stop_timeout = 60 + stop_signal = "SIGINT" + env = [ + "CODER_AGENT_TOKEN=\${coder_agent.dev.token}", + "USE_CAP_NET_ADMIN=true", + "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" + ip = "host-gateway" + } + volumes { + container_path = "/home/coder/" + volume_name = docker_volume.home_volume.name + read_only = false + } + capabilities { + add = ["CAP_NET_ADMIN", "CAP_SYS_NICE"] + } + # Add labels in Docker to keep track of orphan resources. + labels { + label = "coder.owner" + value = data.coder_workspace_owner.me.name + } + labels { + label = "coder.owner_id" + value = data.coder_workspace_owner.me.id + } + labels { + label = "coder.workspace_id" + value = data.coder_workspace.me.id + } + labels { + label = "coder.workspace_name" + value = data.coder_workspace.me.name + } +} + +resource "coder_metadata" "container_info" { + count = data.coder_workspace.me.start_count + resource_id = docker_container.workspace[0].id + item { + key = "memory" + value = docker_container.workspace[0].memory + } + item { + key = "runtime" + value = docker_container.workspace[0].runtime + } + item { + key = "region" + value = data.coder_parameter.region.option[index(data.coder_parameter.region.option.*.value, data.coder_parameter.region.value)].name + } +} +`; diff --git a/site/src/pages/ChatPage/ChatToolInvocation.tsx b/site/src/pages/ChatPage/ChatToolInvocation.tsx new file mode 100644 index 0000000000000..6f418edabb4a5 --- /dev/null +++ b/site/src/pages/ChatPage/ChatToolInvocation.tsx @@ -0,0 +1,872 @@ +import type { ToolCall, ToolResult } from "@ai-sdk/provider-utils"; +import { useTheme } from "@emotion/react"; +import ArticleIcon from "@mui/icons-material/Article"; +import BuildIcon from "@mui/icons-material/Build"; +import CheckCircle from "@mui/icons-material/CheckCircle"; +import CodeIcon from "@mui/icons-material/Code"; +import DeleteIcon from "@mui/icons-material/Delete"; +import ErrorIcon from "@mui/icons-material/Error"; +import FileUploadIcon from "@mui/icons-material/FileUpload"; +import PersonIcon from "@mui/icons-material/Person"; +import SettingsIcon from "@mui/icons-material/Settings"; +import CircularProgress from "@mui/material/CircularProgress"; +import Tooltip from "@mui/material/Tooltip"; +import type * as TypesGen from "api/typesGenerated"; +import { Avatar } from "components/Avatar/Avatar"; +import { InfoIcon } from "lucide-react"; +import type React from "react"; +import { type FC, memo, useMemo, useState } from "react"; +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; +import { dracula } from "react-syntax-highlighter/dist/cjs/styles/prism"; +import { vscDarkPlus } from "react-syntax-highlighter/dist/cjs/styles/prism"; +import { TabLink, Tabs, TabsList } from "../../components/Tabs/Tabs"; + +interface ChatToolInvocationProps { + toolInvocation: ChatToolInvocation; +} + +export const ChatToolInvocation: FC = ({ + toolInvocation, +}) => { + const theme = useTheme(); + const friendlyName = useMemo(() => { + return toolInvocation.toolName + .replace("coder_", "") + .replace(/_/g, " ") + .replace(/\b\w/g, (char) => char.toUpperCase()); + }, [toolInvocation.toolName]); + + const hasError = useMemo(() => { + if (toolInvocation.state !== "result") { + return false; + } + return ( + typeof toolInvocation.result === "object" && + toolInvocation.result !== null && + "error" in toolInvocation.result + ); + }, [toolInvocation]); + const statusColor = useMemo(() => { + if (toolInvocation.state !== "result") { + return theme.palette.info.main; + } + return hasError ? theme.palette.error.main : theme.palette.success.main; + }, [toolInvocation, hasError, theme]); + const tooltipContent = useMemo(() => { + return ( + + {JSON.stringify(toolInvocation, null, 2)} + + ); + }, [toolInvocation, theme.shape.borderRadius, theme.spacing]); + + return ( +
+
+ {toolInvocation.state !== "result" && ( + + )} + {toolInvocation.state === "result" ? ( + hasError ? ( + + ) : ( + + ) + ) : null} +
+ {friendlyName} +
+ + + +
+ {toolInvocation.state === "result" ? ( + + ) : ( + + )} +
+ ); +}; + +const ChatToolInvocationCallPreview: FC<{ + toolInvocation: Extract< + ChatToolInvocation, + { state: "call" | "partial-call" } + >; +}> = memo(({ toolInvocation }) => { + const theme = useTheme(); + + let content: React.ReactNode; + switch (toolInvocation.toolName) { + case "coder_upload_tar_file": + content = ( + + ); + break; + } + + if (!content) { + return null; + } + + return
{content}
; +}); + +const ChatToolInvocationResultPreview: FC<{ + toolInvocation: Extract; +}> = memo(({ toolInvocation }) => { + const theme = useTheme(); + + if (!toolInvocation.result) { + return null; + } + + if ( + typeof toolInvocation.result === "object" && + "error" in toolInvocation.result + ) { + return null; + } + + let content: React.ReactNode; + switch (toolInvocation.toolName) { + case "coder_get_workspace": + case "coder_create_workspace": + content = ( +
+ {toolInvocation.result.template_icon && ( + {toolInvocation.result.template_display_name + )} +
+
+ {toolInvocation.result.name} +
+
+ {toolInvocation.result.template_display_name} +
+
+
+ ); + break; + case "coder_list_workspaces": + content = ( +
+ {toolInvocation.result.map((workspace) => ( +
+ {workspace.template_icon && ( + {workspace.template_display_name + )} +
+
+ {workspace.name} +
+
+ {workspace.template_display_name} +
+
+
+ ))} +
+ ); + break; + case "coder_list_templates": { + const templates = toolInvocation.result; + content = ( +
+ {templates.map((template) => ( +
+ +
+
+ {template.name} +
+
+ {template.description} +
+
+
+ ))} + {templates.length === 0 &&
No templates found.
} +
+ ); + break; + } + case "coder_template_version_parameters": { + const params = toolInvocation.result; + content = ( +
+ + {params.length > 0 + ? `${params.length} parameter(s)` + : "No parameters"} +
+ ); + break; + } + case "coder_get_authenticated_user": { + const user = toolInvocation.result; + content = ( +
+ + + +
+
+ {user.username} +
+
+ {user.email} +
+
+
+ ); + break; + } + case "coder_create_workspace_build": { + const build = toolInvocation.result; + content = ( +
+ + Build #{build.build_number} ({build.transition}) status:{" "} + {build.status} +
+ ); + break; + } + case "coder_create_template_version": { + const version = toolInvocation.result; + content = ( +
+ +
+
{version.name}
+ {version.message && ( +
+ {version.message} +
+ )} +
+
+ ); + break; + } + case "coder_get_workspace_agent_logs": + case "coder_get_workspace_build_logs": + case "coder_get_template_version_logs": { + const logs = toolInvocation.result; + const totalLines = logs.length; + const maxLinesToShow = 5; + const lastLogs = logs.slice(-maxLinesToShow); + const hiddenLines = totalLines - lastLogs.length; + + const totalLinesText = `${totalLines} log line${totalLines !== 1 ? "s" : ""}`; + const hiddenLinesText = + hiddenLines > 0 + ? `... hiding ${hiddenLines} more line${hiddenLines !== 1 ? "s" : ""} ...` + : null; + + const logsToShow = hiddenLinesText + ? [hiddenLinesText, ...lastLogs] + : lastLogs; + + content = ( +
+
+ + Retrieved {totalLinesText}. +
+ {logsToShow.length > 0 && ( + + {logsToShow.join("\n")} + + )} +
+ ); + break; + } + case "coder_update_template_active_version": + content = ( +
+ + {toolInvocation.result} +
+ ); + break; + case "coder_upload_tar_file": + content = ( + + ); + break; + case "coder_create_template": { + const template = toolInvocation.result; + content = ( +
+ {template.display_name +
+
+ {template.name} +
+
+ {template.display_name} +
+
+
+ ); + break; + } + case "coder_delete_template": + content = ( +
+ + {toolInvocation.result} +
+ ); + break; + case "coder_get_template_version": { + const version = toolInvocation.result; + content = ( +
+ +
+
{version.name}
+ {version.message && ( +
+ {version.message} +
+ )} +
+
+ ); + break; + } + case "coder_download_tar_file": { + const files = toolInvocation.result; + content = ; + break; + } + // Add default case or handle other tools if necessary + } + return ( +
+ {content} +
+ ); +}); + +// New component to preview files with tabs +const FilePreview: FC<{ files: Record; prefix?: string }> = + memo(({ files, prefix }) => { + const theme = useTheme(); + const [selectedTab, setSelectedTab] = useState(0); + const fileEntries = useMemo(() => Object.entries(files), [files]); + + if (fileEntries.length === 0) { + return null; + } + + const handleTabChange = (index: number) => { + setSelectedTab(index); + }; + + const getLanguage = (filename: string): string => { + if (filename.includes("Dockerfile")) { + return "dockerfile"; + } + const extension = filename.split(".").pop()?.toLowerCase(); + switch (extension) { + case "tf": + return "hcl"; + case "json": + return "json"; + case "yaml": + case "yml": + return "yaml"; + case "js": + case "jsx": + return "javascript"; + case "ts": + case "tsx": + return "typescript"; + case "py": + return "python"; + case "go": + return "go"; + case "rb": + return "ruby"; + case "java": + return "java"; + case "sh": + return "bash"; + case "md": + return "markdown"; + default: + return "plaintext"; + } + }; + + // Get filename and content based on the selectedTab index + const [selectedFilename, selectedContent] = fileEntries[selectedTab] ?? [ + "", + "", + ]; + + return ( +
+ {prefix && ( +
+ + {prefix} +
+ )} + {/* Use custom Tabs component with active prop */} + + + {fileEntries.map(([filename], index) => ( + { + e.preventDefault(); // Prevent any potential default link behavior + handleTabChange(index); + }} + > + {filename} + + ))} + + + + {selectedContent} + +
+ ); + }); + +// TODO: generate these from codersdk/toolsdk.go. +export type ChatToolInvocation = + | ToolInvocation< + "coder_get_workspace", + { + workspace_id: string; + }, + TypesGen.Workspace + > + | ToolInvocation< + "coder_create_workspace", + { + user: string; + template_version_id: string; + name: string; + rich_parameters: Record; + }, + TypesGen.Workspace + > + | ToolInvocation< + "coder_list_workspaces", + { + owner: string; + }, + Pick< + TypesGen.Workspace, + | "id" + | "name" + | "template_id" + | "template_name" + | "template_display_name" + | "template_icon" + | "template_active_version_id" + | "outdated" + >[] + > + | ToolInvocation< + "coder_list_templates", + Record, + Pick< + TypesGen.Template, + | "id" + | "name" + | "description" + | "active_version_id" + | "active_user_count" + >[] + > + | ToolInvocation< + "coder_template_version_parameters", + { + template_version_id: string; + }, + TypesGen.TemplateVersionParameter[] + > + | ToolInvocation< + "coder_get_authenticated_user", + Record, + TypesGen.User + > + | ToolInvocation< + "coder_create_workspace_build", + { + workspace_id: string; + template_version_id?: string; + transition: "start" | "stop" | "delete"; + }, + TypesGen.WorkspaceBuild + > + | ToolInvocation< + "coder_create_template_version", + { + template_id?: string; + file_id: string; + }, + TypesGen.TemplateVersion + > + | ToolInvocation< + "coder_get_workspace_agent_logs", + { + workspace_agent_id: string; + }, + string[] + > + | ToolInvocation< + "coder_get_workspace_build_logs", + { + workspace_build_id: string; + }, + string[] + > + | ToolInvocation< + "coder_get_template_version_logs", + { + template_version_id: string; + }, + string[] + > + | ToolInvocation< + "coder_get_template_version", + { + template_version_id: string; + }, + TypesGen.TemplateVersion + > + | ToolInvocation< + "coder_download_tar_file", + { + file_id: string; + }, + Record + > + | ToolInvocation< + "coder_update_template_active_version", + { + template_id: string; + template_version_id: string; + }, + string + > + | ToolInvocation< + "coder_upload_tar_file", + { + files: Record; + }, + TypesGen.UploadResponse + > + | ToolInvocation< + "coder_create_template", + { + name: string; + }, + TypesGen.Template + > + | ToolInvocation< + "coder_delete_template", + { + template_id: string; + }, + string + >; + +type ToolInvocation = + | ({ + state: "partial-call"; + step?: number; + } & ToolCall) + | ({ + state: "call"; + step?: number; + } & ToolCall) + | ({ + state: "result"; + step?: number; + } & ToolResult< + N, + A, + | R + | { + error: string; + } + >); diff --git a/site/src/pages/ChatPage/LanguageModelSelector.tsx b/site/src/pages/ChatPage/LanguageModelSelector.tsx new file mode 100644 index 0000000000000..2170be22b3196 --- /dev/null +++ b/site/src/pages/ChatPage/LanguageModelSelector.tsx @@ -0,0 +1,73 @@ +import { useTheme } from "@emotion/react"; +import FormControl from "@mui/material/FormControl"; +import InputLabel from "@mui/material/InputLabel"; +import MenuItem from "@mui/material/MenuItem"; +import Select from "@mui/material/Select"; +import { deploymentLanguageModels } from "api/queries/deployment"; +import type { LanguageModel } from "api/typesGenerated"; // Assuming types live here based on project structure +import { Loader } from "components/Loader/Loader"; +import type { FC } from "react"; +import { useQuery } from "react-query"; +import { useChatContext } from "./ChatLayout"; + +export const LanguageModelSelector: FC = () => { + const theme = useTheme(); + const { setSelectedModel, modelConfig, selectedModel } = useChatContext(); + const { + data: languageModelConfig, + isLoading, + error, + } = useQuery(deploymentLanguageModels()); + + if (isLoading) { + return ; + } + + if (error || !languageModelConfig) { + console.error("Failed to load language models:", error); + return ( +
Error loading models.
+ ); + } + + const models = Array.from(languageModelConfig.models).toSorted((a, b) => { + // Sort by provider first, then by display name + const compareProvider = a.provider.localeCompare(b.provider); + if (compareProvider !== 0) { + return compareProvider; + } + return a.display_name.localeCompare(b.display_name); + }); + + if (models.length === 0) { + return ( +
+ No language models available. +
+ ); + } + + return ( + + Model + + + ); +}; diff --git a/site/src/router.tsx b/site/src/router.tsx index 76e9adfd00b09..534d4037d02b3 100644 --- a/site/src/router.tsx +++ b/site/src/router.tsx @@ -1,4 +1,6 @@ import { GlobalErrorBoundary } from "components/ErrorBoundary/GlobalErrorBoundary"; +import { ChatLayout } from "pages/ChatPage/ChatLayout"; +import { ChatMessages } from "pages/ChatPage/ChatMessages"; import { TemplateRedirectController } from "pages/TemplatePage/TemplateRedirectController"; import { Suspense, lazy } from "react"; import { @@ -31,6 +33,7 @@ const NotFoundPage = lazy(() => import("./pages/404Page/404Page")); const DeploymentSettingsLayout = lazy( () => import("./modules/management/DeploymentSettingsLayout"), ); +const ChatLanding = lazy(() => import("./pages/ChatPage/ChatLanding")); const DeploymentConfigProvider = lazy( () => import("./modules/management/DeploymentConfigProvider"), ); @@ -422,6 +425,11 @@ export const router = createBrowserRouter( } /> + }> + } /> + } /> + + }> } /> From 64807e1d614d1d176bdbdfcf3a41549eb1ab0d42 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 13 May 2025 11:24:51 -0500 Subject: [PATCH 25/88] chore: apply the 4mb max limit on drpc protocol message size (#17771) Respect the 4mb max limit on proto messages --- agent/agenttest/client.go | 1 + coderd/agentapi/api.go | 2 + coderd/coderd.go | 3 +- codersdk/drpcsdk/transport.go | 28 ++++++- enterprise/coderd/provisionerdaemons.go | 2 + enterprise/provisionerd/remoteprovisioners.go | 7 +- provisionerd/provisionerd_test.go | 77 ++++++++++++++++++- provisionersdk/serve.go | 5 +- provisionersdk/serve_test.go | 4 +- tailnet/service.go | 2 + 10 files changed, 121 insertions(+), 10 deletions(-) diff --git a/agent/agenttest/client.go b/agent/agenttest/client.go index 73fb40e826519..24658c44d6e18 100644 --- a/agent/agenttest/client.go +++ b/agent/agenttest/client.go @@ -60,6 +60,7 @@ func NewClient(t testing.TB, err = agentproto.DRPCRegisterAgent(mux, fakeAAPI) require.NoError(t, err) server := drpcserver.NewWithOptions(mux, drpcserver.Options{ + Manager: drpcsdk.DefaultDRPCOptions(nil), Log: func(err error) { if xerrors.Is(err, io.EOF) { return diff --git a/coderd/agentapi/api.go b/coderd/agentapi/api.go index 1b2b8d92a10ef..8a0871bc083d4 100644 --- a/coderd/agentapi/api.go +++ b/coderd/agentapi/api.go @@ -30,6 +30,7 @@ import ( "github.com/coder/coder/v2/coderd/wspubsub" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" + "github.com/coder/coder/v2/codersdk/drpcsdk" "github.com/coder/coder/v2/tailnet" tailnetproto "github.com/coder/coder/v2/tailnet/proto" "github.com/coder/quartz" @@ -209,6 +210,7 @@ func (a *API) Server(ctx context.Context) (*drpcserver.Server, error) { return drpcserver.NewWithOptions(&tracing.DRPCHandler{Handler: mux}, drpcserver.Options{ + Manager: drpcsdk.DefaultDRPCOptions(nil), Log: func(err error) { if xerrors.Is(err, io.EOF) { return diff --git a/coderd/coderd.go b/coderd/coderd.go index 1b4b5746b7f7e..86db50d9559a4 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -38,6 +38,7 @@ import ( "tailscale.com/util/singleflight" "cdr.dev/slog" + "github.com/coder/coder/v2/codersdk/drpcsdk" "github.com/coder/quartz" "github.com/coder/serpent" @@ -84,7 +85,6 @@ import ( "github.com/coder/coder/v2/coderd/workspaceapps" "github.com/coder/coder/v2/coderd/workspacestats" "github.com/coder/coder/v2/codersdk" - "github.com/coder/coder/v2/codersdk/drpcsdk" "github.com/coder/coder/v2/codersdk/healthsdk" "github.com/coder/coder/v2/provisionerd/proto" "github.com/coder/coder/v2/provisionersdk" @@ -1803,6 +1803,7 @@ func (api *API) CreateInMemoryTaggedProvisionerDaemon(dialCtx context.Context, n } server := drpcserver.NewWithOptions(&tracing.DRPCHandler{Handler: mux}, drpcserver.Options{ + Manager: drpcsdk.DefaultDRPCOptions(nil), Log: func(err error) { if xerrors.Is(err, io.EOF) { return diff --git a/codersdk/drpcsdk/transport.go b/codersdk/drpcsdk/transport.go index 84cef5e6d8db1..82a0921b41057 100644 --- a/codersdk/drpcsdk/transport.go +++ b/codersdk/drpcsdk/transport.go @@ -9,6 +9,7 @@ import ( "github.com/valyala/fasthttp/fasthttputil" "storj.io/drpc" "storj.io/drpc/drpcconn" + "storj.io/drpc/drpcmanager" "github.com/coder/coder/v2/coderd/tracing" ) @@ -19,6 +20,17 @@ const ( MaxMessageSize = 4 << 20 ) +func DefaultDRPCOptions(options *drpcmanager.Options) drpcmanager.Options { + if options == nil { + options = &drpcmanager.Options{} + } + + if options.Reader.MaximumBufferSize == 0 { + options.Reader.MaximumBufferSize = MaxMessageSize + } + return *options +} + // MultiplexedConn returns a multiplexed dRPC connection from a yamux Session. func MultiplexedConn(session *yamux.Session) drpc.Conn { return &multiplexedDRPC{session} @@ -43,7 +55,9 @@ func (m *multiplexedDRPC) Invoke(ctx context.Context, rpc string, enc drpc.Encod if err != nil { return err } - dConn := drpcconn.New(conn) + dConn := drpcconn.NewWithOptions(conn, drpcconn.Options{ + Manager: DefaultDRPCOptions(nil), + }) defer func() { _ = dConn.Close() }() @@ -55,7 +69,9 @@ func (m *multiplexedDRPC) NewStream(ctx context.Context, rpc string, enc drpc.En if err != nil { return nil, err } - dConn := drpcconn.New(conn) + dConn := drpcconn.NewWithOptions(conn, drpcconn.Options{ + Manager: DefaultDRPCOptions(nil), + }) stream, err := dConn.NewStream(ctx, rpc, enc) if err == nil { go func() { @@ -97,7 +113,9 @@ func (m *memDRPC) Invoke(ctx context.Context, rpc string, enc drpc.Encoding, inM return err } - dConn := &tracing.DRPCConn{Conn: drpcconn.New(conn)} + dConn := &tracing.DRPCConn{Conn: drpcconn.NewWithOptions(conn, drpcconn.Options{ + Manager: DefaultDRPCOptions(nil), + })} defer func() { _ = dConn.Close() _ = conn.Close() @@ -110,7 +128,9 @@ func (m *memDRPC) NewStream(ctx context.Context, rpc string, enc drpc.Encoding) if err != nil { return nil, err } - dConn := &tracing.DRPCConn{Conn: drpcconn.New(conn)} + dConn := &tracing.DRPCConn{Conn: drpcconn.NewWithOptions(conn, drpcconn.Options{ + Manager: DefaultDRPCOptions(nil), + })} stream, err := dConn.NewStream(ctx, rpc, enc) if err != nil { _ = dConn.Close() diff --git a/enterprise/coderd/provisionerdaemons.go b/enterprise/coderd/provisionerdaemons.go index 6ffa15851214d..b9a93144f4931 100644 --- a/enterprise/coderd/provisionerdaemons.go +++ b/enterprise/coderd/provisionerdaemons.go @@ -31,6 +31,7 @@ import ( "github.com/coder/coder/v2/coderd/telemetry" "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/codersdk/drpcsdk" "github.com/coder/coder/v2/provisionerd/proto" "github.com/coder/coder/v2/provisionersdk" "github.com/coder/websocket" @@ -370,6 +371,7 @@ func (api *API) provisionerDaemonServe(rw http.ResponseWriter, r *http.Request) return } server := drpcserver.NewWithOptions(mux, drpcserver.Options{ + Manager: drpcsdk.DefaultDRPCOptions(nil), Log: func(err error) { if xerrors.Is(err, io.EOF) { return diff --git a/enterprise/provisionerd/remoteprovisioners.go b/enterprise/provisionerd/remoteprovisioners.go index 26c93322e662a..1ae02f00312e9 100644 --- a/enterprise/provisionerd/remoteprovisioners.go +++ b/enterprise/provisionerd/remoteprovisioners.go @@ -27,6 +27,7 @@ import ( "cdr.dev/slog" "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/codersdk/drpcsdk" "github.com/coder/coder/v2/provisioner/echo" agpl "github.com/coder/coder/v2/provisionerd" "github.com/coder/coder/v2/provisionerd/proto" @@ -188,8 +189,10 @@ func (r *remoteConnector) handleConn(conn net.Conn) { logger.Info(r.ctx, "provisioner connected") closeConn = false // we're passing the conn over the channel w.respCh <- agpl.ConnectResponse{ - Job: w.job, - Client: sdkproto.NewDRPCProvisionerClient(drpcconn.New(tlsConn)), + Job: w.job, + Client: sdkproto.NewDRPCProvisionerClient(drpcconn.NewWithOptions(tlsConn, drpcconn.Options{ + Manager: drpcsdk.DefaultDRPCOptions(nil), + })), } } diff --git a/provisionerd/provisionerd_test.go b/provisionerd/provisionerd_test.go index a9418d9391c8f..7a5d714befa05 100644 --- a/provisionerd/provisionerd_test.go +++ b/provisionerd/provisionerd_test.go @@ -178,6 +178,79 @@ func TestProvisionerd(t *testing.T) { require.NoError(t, closer.Close()) }) + // LargePayloads sends a 3mb tar file to the provisioner. The provisioner also + // returns large payload messages back. The limit should be 4mb, so all + // these messages should work. + t.Run("LargePayloads", func(t *testing.T) { + t.Parallel() + done := make(chan struct{}) + t.Cleanup(func() { + close(done) + }) + var ( + largeSize = 3 * 1024 * 1024 + completeChan = make(chan struct{}) + completeOnce sync.Once + acq = newAcquireOne(t, &proto.AcquiredJob{ + JobId: "test", + Provisioner: "someprovisioner", + TemplateSourceArchive: testutil.CreateTar(t, map[string]string{ + "toolarge.txt": string(make([]byte, largeSize)), + }), + Type: &proto.AcquiredJob_TemplateImport_{ + TemplateImport: &proto.AcquiredJob_TemplateImport{ + Metadata: &sdkproto.Metadata{}, + }, + }, + }) + ) + + closer := createProvisionerd(t, func(ctx context.Context) (proto.DRPCProvisionerDaemonClient, error) { + return createProvisionerDaemonClient(t, done, provisionerDaemonTestServer{ + acquireJobWithCancel: acq.acquireWithCancel, + updateJob: noopUpdateJob, + completeJob: func(ctx context.Context, job *proto.CompletedJob) (*proto.Empty, error) { + completeOnce.Do(func() { close(completeChan) }) + return &proto.Empty{}, nil + }, + }), nil + }, provisionerd.LocalProvisioners{ + "someprovisioner": createProvisionerClient(t, done, provisionerTestServer{ + parse: func( + s *provisionersdk.Session, + _ *sdkproto.ParseRequest, + cancelOrComplete <-chan struct{}, + ) *sdkproto.ParseComplete { + return &sdkproto.ParseComplete{ + // 6mb readme + Readme: make([]byte, largeSize), + } + }, + plan: func( + _ *provisionersdk.Session, + _ *sdkproto.PlanRequest, + _ <-chan struct{}, + ) *sdkproto.PlanComplete { + return &sdkproto.PlanComplete{ + Resources: []*sdkproto.Resource{}, + Plan: make([]byte, largeSize), + } + }, + apply: func( + _ *provisionersdk.Session, + _ *sdkproto.ApplyRequest, + _ <-chan struct{}, + ) *sdkproto.ApplyComplete { + return &sdkproto.ApplyComplete{ + State: make([]byte, largeSize), + } + }, + }), + }) + require.Condition(t, closedWithin(completeChan, testutil.WaitShort)) + require.NoError(t, closer.Close()) + }) + t.Run("RunningPeriodicUpdate", func(t *testing.T) { t.Parallel() done := make(chan struct{}) @@ -1115,7 +1188,9 @@ func createProvisionerDaemonClient(t *testing.T, done <-chan struct{}, server pr mux := drpcmux.New() err := proto.DRPCRegisterProvisionerDaemon(mux, &server) require.NoError(t, err) - srv := drpcserver.New(mux) + srv := drpcserver.NewWithOptions(mux, drpcserver.Options{ + Manager: drpcsdk.DefaultDRPCOptions(nil), + }) ctx, cancelFunc := context.WithCancel(context.Background()) closed := make(chan struct{}) go func() { diff --git a/provisionersdk/serve.go b/provisionersdk/serve.go index b91329d0665fe..c652cfa94949d 100644 --- a/provisionersdk/serve.go +++ b/provisionersdk/serve.go @@ -15,6 +15,7 @@ import ( "storj.io/drpc/drpcserver" "cdr.dev/slog" + "github.com/coder/coder/v2/codersdk/drpcsdk" "github.com/coder/coder/v2/coderd/tracing" "github.com/coder/coder/v2/provisionersdk/proto" @@ -81,7 +82,9 @@ func Serve(ctx context.Context, server Server, options *ServeOptions) error { if err != nil { return xerrors.Errorf("register provisioner: %w", err) } - srv := drpcserver.New(&tracing.DRPCHandler{Handler: mux}) + srv := drpcserver.NewWithOptions(&tracing.DRPCHandler{Handler: mux}, drpcserver.Options{ + Manager: drpcsdk.DefaultDRPCOptions(nil), + }) if options.Listener != nil { err = srv.Serve(ctx, options.Listener) diff --git a/provisionersdk/serve_test.go b/provisionersdk/serve_test.go index a78a573e11b02..4fc7342b1eed2 100644 --- a/provisionersdk/serve_test.go +++ b/provisionersdk/serve_test.go @@ -94,7 +94,9 @@ func TestProvisionerSDK(t *testing.T) { srvErr <- err }() - api := proto.NewDRPCProvisionerClient(drpcconn.New(client)) + api := proto.NewDRPCProvisionerClient(drpcconn.NewWithOptions(client, drpcconn.Options{ + Manager: drpcsdk.DefaultDRPCOptions(nil), + })) s, err := api.Session(ctx) require.NoError(t, err) err = s.Send(&proto.Request{Type: &proto.Request_Config{Config: &proto.Config{}}}) diff --git a/tailnet/service.go b/tailnet/service.go index abb91acef8772..c3a6b9f2b282b 100644 --- a/tailnet/service.go +++ b/tailnet/service.go @@ -17,6 +17,7 @@ import ( "cdr.dev/slog" "github.com/coder/coder/v2/apiversion" + "github.com/coder/coder/v2/codersdk/drpcsdk" "github.com/coder/coder/v2/tailnet/proto" "github.com/coder/quartz" ) @@ -92,6 +93,7 @@ func NewClientService(options ClientServiceOptions) ( return nil, xerrors.Errorf("register DRPC service: %w", err) } server := drpcserver.NewWithOptions(mux, drpcserver.Options{ + Manager: drpcsdk.DefaultDRPCOptions(nil), Log: func(err error) { if xerrors.Is(err, io.EOF) || xerrors.Is(err, context.Canceled) || From 709445e6fb0ed81dbddae2a8c8c835e21d1bbb74 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Tue, 13 May 2025 14:53:06 -0300 Subject: [PATCH 26/88] chore: replace MUI icons with Lucide icons - 9 (#17796) OpenInNew -> ExternalLinkIcon KeyboardArrowLeft -> ChevronLeftIcon KeyboardArrowRight -> ChevronRightIcon Settings -> SettingsIcon --- site/src/components/HelpTooltip/HelpTooltip.tsx | 4 ++-- .../components/PaginationWidget/PaginationWidgetBase.tsx | 7 +++---- .../components/RichParameterInput/RichParameterInput.tsx | 4 ++-- site/src/pages/GroupsPage/GroupPage.tsx | 4 ++-- site/src/pages/TemplateSettingsPage/Sidebar.tsx | 4 ++-- .../WorkspacePage/WorkspaceActions/WorkspaceActions.tsx | 4 ++-- 6 files changed, 13 insertions(+), 14 deletions(-) diff --git a/site/src/components/HelpTooltip/HelpTooltip.tsx b/site/src/components/HelpTooltip/HelpTooltip.tsx index 2ae8700114b3b..0a46f9a10f199 100644 --- a/site/src/components/HelpTooltip/HelpTooltip.tsx +++ b/site/src/components/HelpTooltip/HelpTooltip.tsx @@ -5,7 +5,6 @@ import { css, useTheme, } from "@emotion/react"; -import OpenInNewIcon from "@mui/icons-material/OpenInNew"; import Link from "@mui/material/Link"; import { Stack } from "components/Stack/Stack"; import { @@ -16,6 +15,7 @@ import { PopoverTrigger, usePopover, } from "components/deprecated/Popover/Popover"; +import { ExternalLinkIcon } from "lucide-react"; import { CircleHelpIcon } from "lucide-react"; import { type FC, @@ -137,7 +137,7 @@ interface HelpTooltipLink { export const HelpTooltipLink: FC = ({ children, href }) => { return ( - + {children} ); diff --git a/site/src/components/PaginationWidget/PaginationWidgetBase.tsx b/site/src/components/PaginationWidget/PaginationWidgetBase.tsx index 488b6fbeab5d6..d163b70740285 100644 --- a/site/src/components/PaginationWidget/PaginationWidgetBase.tsx +++ b/site/src/components/PaginationWidget/PaginationWidgetBase.tsx @@ -1,7 +1,6 @@ import { useTheme } from "@emotion/react"; -import KeyboardArrowLeft from "@mui/icons-material/KeyboardArrowLeft"; -import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight"; import useMediaQuery from "@mui/material/useMediaQuery"; +import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; import type { FC } from "react"; import { NumberedPageButton, PlaceholderPageButton } from "./PageButtons"; import { PaginationNavButton } from "./PaginationNavButton"; @@ -60,7 +59,7 @@ export const PaginationWidgetBase: FC = ({ } }} > - + {isMobile ? ( @@ -87,7 +86,7 @@ export const PaginationWidgetBase: FC = ({ } }} > - +
); diff --git a/site/src/components/RichParameterInput/RichParameterInput.tsx b/site/src/components/RichParameterInput/RichParameterInput.tsx index e62f1d57a9a39..c9a5c895e5825 100644 --- a/site/src/components/RichParameterInput/RichParameterInput.tsx +++ b/site/src/components/RichParameterInput/RichParameterInput.tsx @@ -1,5 +1,4 @@ import type { Interpolation, Theme } from "@emotion/react"; -import SettingsIcon from "@mui/icons-material/Settings"; import Button from "@mui/material/Button"; import FormControlLabel from "@mui/material/FormControlLabel"; import FormHelperText from "@mui/material/FormHelperText"; @@ -13,6 +12,7 @@ import { ExternalImage } from "components/ExternalImage/ExternalImage"; import { MemoizedMarkdown } from "components/Markdown/Markdown"; import { Pill } from "components/Pill/Pill"; import { Stack } from "components/Stack/Stack"; +import { SettingsIcon } from "lucide-react"; import { CircleAlertIcon } from "lucide-react"; import { type FC, type ReactNode, useState } from "react"; import type { @@ -153,7 +153,7 @@ const ParameterLabel: FC = ({ parameter, isPreset }) => { )} {isPreset && ( - }> + }> Preset diff --git a/site/src/pages/GroupsPage/GroupPage.tsx b/site/src/pages/GroupsPage/GroupPage.tsx index f97ec2d82be09..60161a5ee7ec0 100644 --- a/site/src/pages/GroupsPage/GroupPage.tsx +++ b/site/src/pages/GroupsPage/GroupPage.tsx @@ -1,6 +1,5 @@ import type { Interpolation, Theme } from "@emotion/react"; 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 { getErrorMessage } from "api/errors"; @@ -50,6 +49,7 @@ import { TableToolbar, } from "components/TableToolbar/TableToolbar"; import { MemberAutocomplete } from "components/UserAutocomplete/UserAutocomplete"; +import { SettingsIcon } from "lucide-react"; import { EllipsisVertical, TrashIcon } from "lucide-react"; import { type FC, useState } from "react"; import { Helmet } from "react-helmet-async"; @@ -123,7 +123,7 @@ const GroupPage: FC = () => { + + + + + + + Settings + + + + {permissions?.updateWorkspaceVersion && ( + { + setChangeVersionDialogOpen(true); + }} + > + + Change version… + + )} + + + + Duplicate… + + + setIsDownloadDialogOpen(true)}> + + Download logs… + + + + + { + setIsConfirmingDelete(true); + }} + data-testid="delete-button" + > + + Delete… + + + + + setIsDownloadDialogOpen(false)} + /> + + { + changeVersionMutation.reset(); + }} + onUpdate={(buildParameters) => { + if (changeVersionMutation.error instanceof MissingBuildParameters) { + changeVersionMutation.mutate({ + versionId: changeVersionMutation.error.versionId, + buildParameters, + }); + } + }} + /> + + { + setChangeVersionDialogOpen(false); + }} + onConfirm={(version) => { + setChangeVersionDialogOpen(false); + changeVersionMutation.mutate({ versionId: version.id }); + }} + /> + + { + setIsConfirmingDelete(false); + }} + onConfirm={(orphan) => { + deleteWorkspaceMutation.mutate({ orphan }); + setIsConfirmingDelete(false); + }} + /> + + ); +}; diff --git a/site/src/pages/CreateWorkspacePage/useWorkspaceDuplication.test.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/useWorkspaceDuplication.test.tsx similarity index 97% rename from site/src/pages/CreateWorkspacePage/useWorkspaceDuplication.test.tsx rename to site/src/modules/workspaces/WorkspaceMoreActions/useWorkspaceDuplication.test.tsx index ce2fead417c03..8e06e10136f92 100644 --- a/site/src/pages/CreateWorkspacePage/useWorkspaceDuplication.test.tsx +++ b/site/src/modules/workspaces/WorkspaceMoreActions/useWorkspaceDuplication.test.tsx @@ -5,7 +5,7 @@ import { type GetLocationSnapshot, renderHookWithAuth, } from "testHelpers/hooks"; -import CreateWorkspacePage from "./CreateWorkspacePage"; +import CreateWorkspacePage from "../../../pages/CreateWorkspacePage/CreateWorkspacePage"; import { useWorkspaceDuplication } from "./useWorkspaceDuplication"; function render(workspace?: Workspace) { diff --git a/site/src/pages/CreateWorkspacePage/useWorkspaceDuplication.ts b/site/src/modules/workspaces/WorkspaceMoreActions/useWorkspaceDuplication.ts similarity index 96% rename from site/src/pages/CreateWorkspacePage/useWorkspaceDuplication.ts rename to site/src/modules/workspaces/WorkspaceMoreActions/useWorkspaceDuplication.ts index b98434a0b51f9..cde6707be6f26 100644 --- a/site/src/pages/CreateWorkspacePage/useWorkspaceDuplication.ts +++ b/site/src/modules/workspaces/WorkspaceMoreActions/useWorkspaceDuplication.ts @@ -4,7 +4,7 @@ import { linkToTemplate, useLinks } from "modules/navigation"; import { useCallback } from "react"; import { useQuery } from "react-query"; import { useNavigate } from "react-router-dom"; -import type { CreateWorkspaceMode } from "./CreateWorkspacePage"; +import type { CreateWorkspaceMode } from "../../../pages/CreateWorkspacePage/CreateWorkspacePage"; function getDuplicationUrlParams( workspaceParams: readonly WorkspaceBuildParameter[], diff --git a/site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx b/site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx index 741bc12a6539b..0b4e53cedfb36 100644 --- a/site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx +++ b/site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx @@ -8,7 +8,7 @@ import type { } from "api/typesGenerated"; import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; import { MemoizedInlineMarkdown } from "components/Markdown/Markdown"; -import { UpdateBuildParametersDialog } from "pages/WorkspacePage/UpdateBuildParametersDialog"; +import { UpdateBuildParametersDialog } from "modules/workspaces/WorkspaceMoreActions/UpdateBuildParametersDialog"; import { type FC, useState } from "react"; import { useMutation, useQueryClient } from "react-query"; diff --git a/site/src/modules/workspaces/permissions.ts b/site/src/modules/workspaces/permissions.ts new file mode 100644 index 0000000000000..07c4e612cdf61 --- /dev/null +++ b/site/src/modules/workspaces/permissions.ts @@ -0,0 +1,50 @@ +import type { AuthorizationCheck, Workspace } from "api/typesGenerated"; + +export const workspaceChecks = (workspace: Workspace) => + ({ + readWorkspace: { + object: { + resource_type: "workspace", + resource_id: workspace.id, + owner_id: workspace.owner_id, + }, + action: "read", + }, + updateWorkspace: { + object: { + resource_type: "workspace", + resource_id: workspace.id, + owner_id: workspace.owner_id, + }, + action: "update", + }, + updateWorkspaceVersion: { + object: { + resource_type: "template", + resource_id: workspace.template_id, + }, + action: "update", + }, + // We only want to allow template admins to delete failed workspaces since + // they can leave orphaned resources. + deleteFailedWorkspace: { + object: { + resource_type: "template", + resource_id: workspace.template_id, + }, + action: "update", + }, + // To run a build in debug mode we need to be able to read the deployment + // config (enable_terraform_debug_mode). + deploymentConfig: { + object: { + resource_type: "deployment_config", + }, + action: "read", + }, + }) satisfies Record; + +export type WorkspacePermissions = Record< + keyof ReturnType, + boolean +>; diff --git a/site/src/pages/WorkspacePage/ChangeVersionDialog.stories.tsx b/site/src/pages/WorkspacePage/ChangeVersionDialog.stories.tsx deleted file mode 100644 index 2da7ae322c22e..0000000000000 --- a/site/src/pages/WorkspacePage/ChangeVersionDialog.stories.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { - MockTemplate, - MockTemplateVersion, - MockTemplateVersionWithMarkdownMessage, -} from "testHelpers/entities"; -import { ChangeVersionDialog } from "./ChangeVersionDialog"; - -const noMessage = { - ...MockTemplateVersion, - message: "", -}; - -const meta: Meta = { - title: "pages/WorkspacePage/ChangeVersionDialog", - component: ChangeVersionDialog, - args: { - open: true, - template: MockTemplate, - templateVersions: [ - MockTemplateVersion, - MockTemplateVersionWithMarkdownMessage, - noMessage, - ], - }, -}; - -export default meta; -type Story = StoryObj; - -export const NoVersionSelected: Story = {}; - -export const NoMessage: Story = { - args: { - defaultTemplateVersion: noMessage, - }, -}; - -export const ShortMessage: Story = { - args: { - defaultTemplateVersion: MockTemplateVersion, - }, -}; - -export const LongMessage: Story = { - args: { - defaultTemplateVersion: MockTemplateVersionWithMarkdownMessage, - }, -}; diff --git a/site/src/pages/WorkspacePage/Workspace.stories.tsx b/site/src/pages/WorkspacePage/Workspace.stories.tsx index ca782d73b68a0..a59e2f78bcee2 100644 --- a/site/src/pages/WorkspacePage/Workspace.stories.tsx +++ b/site/src/pages/WorkspacePage/Workspace.stories.tsx @@ -4,8 +4,8 @@ import type { ProvisionerJobLog } from "api/typesGenerated"; import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext"; import * as Mocks from "testHelpers/entities"; import { withAuthProvider, withDashboardProvider } from "testHelpers/storybook"; +import type { WorkspacePermissions } from "../../modules/workspaces/permissions"; import { Workspace } from "./Workspace"; -import type { WorkspacePermissions } from "./permissions"; // Helper function to create timestamps easily - Copied from AppStatuses.stories.tsx const createTimestamp = ( @@ -21,8 +21,9 @@ const createTimestamp = ( const permissions: WorkspacePermissions = { readWorkspace: true, updateWorkspace: true, - updateTemplate: true, - viewDeploymentConfig: true, + updateWorkspaceVersion: true, + deploymentConfig: true, + deleteFailedWorkspace: true, }; const meta: Meta = { diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index 1c60b8b86b50b..8c874e71beeb3 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -13,6 +13,7 @@ import { AgentRow } from "modules/resources/AgentRow"; import { WorkspaceTimings } from "modules/workspaces/WorkspaceTiming/WorkspaceTimings"; import { type FC, useMemo } from "react"; import { useNavigate } from "react-router-dom"; +import type { WorkspacePermissions } from "../../modules/workspaces/permissions"; import { AppStatuses } from "./AppStatuses"; import { HistorySidebar } from "./HistorySidebar"; import { ResourceMetadata } from "./ResourceMetadata"; @@ -24,68 +25,57 @@ import { } from "./WorkspaceBuildProgress"; import { WorkspaceDeletedBanner } from "./WorkspaceDeletedBanner"; import { WorkspaceTopbar } from "./WorkspaceTopbar"; -import type { WorkspacePermissions } from "./permissions"; import { resourceOptionValue, useResourcesNav } from "./useResourcesNav"; export interface WorkspaceProps { - handleStart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; - handleStop: () => void; - handleRestart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; - handleDelete: () => void; - handleUpdate: () => void; - handleCancel: () => void; - handleSettings: () => void; - handleChangeVersion: () => void; - handleDormantActivate: () => void; - handleToggleFavorite: () => void; + workspace: TypesGen.Workspace; + template: TypesGen.Template; + permissions: WorkspacePermissions; isUpdating: boolean; isRestarting: boolean; - workspace: TypesGen.Workspace; - canChangeVersions: boolean; hideSSHButton?: boolean; hideVSCodeDesktopButton?: boolean; buildInfo?: TypesGen.BuildInfoResponse; sshPrefix?: string; - template: TypesGen.Template; - canDebugMode: boolean; - handleRetry: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; - handleDebug: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; buildLogs?: TypesGen.ProvisionerJobLog[]; latestVersion?: TypesGen.TemplateVersion; - permissions: WorkspacePermissions; timings?: TypesGen.WorkspaceBuildTimings; + handleStart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; + handleStop: () => void; + handleRestart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; + handleUpdate: () => void; + handleCancel: () => void; + handleDormantActivate: () => void; + handleToggleFavorite: () => void; + handleRetry: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; + handleDebug: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; } /** * Workspace is the top-level component for viewing an individual workspace */ export const Workspace: FC = ({ - handleStart, - handleStop, - handleRestart, - handleDelete, - handleUpdate, - handleCancel, - handleSettings, - handleChangeVersion, - handleDormantActivate, - handleToggleFavorite, workspace, isUpdating, isRestarting, - canChangeVersions, hideSSHButton, hideVSCodeDesktopButton, buildInfo, sshPrefix, template, - canDebugMode, - handleRetry, - handleDebug, buildLogs, latestVersion, permissions, timings, + handleStart, + handleStop, + handleRestart, + handleUpdate, + handleCancel, + handleDormantActivate, + handleToggleFavorite, + handleRetry, + handleDebug, }) => { const navigate = useNavigate(); const theme = useTheme(); @@ -142,26 +132,20 @@ export const Workspace: FC = ({ >
= { component: WorkspaceActions, args: { isUpdating: false, + permissions: { + deleteFailedWorkspace: true, + deploymentConfig: true, + readWorkspace: true, + updateWorkspace: true, + updateWorkspaceVersion: true, + }, }, decorators: [withDashboardProvider, withDesktopViewport, withAuthProvider], parameters: { user: Mocks.MockUserOwner, + queries: [ + { + key: deploymentConfigQueryKey, + data: Mocks.MockDeploymentConfig, + }, + ], }, }; @@ -157,7 +171,13 @@ export const Failed: Story = { export const FailedWithDebug: Story = { args: { workspace: Mocks.MockFailedWorkspace, - canDebug: true, + permissions: { + deploymentConfig: true, + deleteFailedWorkspace: true, + readWorkspace: true, + updateWorkspace: true, + updateWorkspaceVersion: true, + }, }, }; diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx index 85c106202ca20..dd3e135901c84 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx @@ -1,25 +1,14 @@ -import DownloadOutlined from "@mui/icons-material/DownloadOutlined"; -import DuplicateIcon from "@mui/icons-material/FileCopyOutlined"; -import HistoryIcon from "@mui/icons-material/HistoryOutlined"; +import { deploymentConfig } from "api/queries/deployment"; import type { Workspace, WorkspaceBuildParameter } from "api/typesGenerated"; -import { Button } from "components/Button/Button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "components/DropdownMenu/DropdownMenu"; import { useAuthenticated } from "hooks/useAuthenticated"; -import { SettingsIcon } from "lucide-react"; -import { TrashIcon } from "lucide-react"; -import { EllipsisVertical } from "lucide-react"; +import { WorkspaceMoreActions } from "modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions"; import { type ActionType, abilitiesByWorkspaceStatus, } from "modules/workspaces/actions"; -import { useWorkspaceDuplication } from "pages/CreateWorkspacePage/useWorkspaceDuplication"; -import { type FC, Fragment, type ReactNode, useState } from "react"; +import type { WorkspacePermissions } from "modules/workspaces/permissions"; +import { type FC, Fragment, type ReactNode } from "react"; +import { useQuery } from "react-query"; import { mustUpdateWorkspace } from "utils/workspace"; import { ActivateButton, @@ -34,64 +23,61 @@ import { UpdateButton, } from "./Buttons"; import { DebugButton } from "./DebugButton"; -import { DownloadLogsDialog } from "./DownloadLogsDialog"; import { RetryButton } from "./RetryButton"; export interface WorkspaceActionsProps { workspace: Workspace; + isUpdating: boolean; + isRestarting: boolean; + permissions: WorkspacePermissions; handleToggleFavorite: () => void; handleStart: (buildParameters?: WorkspaceBuildParameter[]) => void; handleStop: () => void; handleRestart: (buildParameters?: WorkspaceBuildParameter[]) => void; - handleDelete: () => void; handleUpdate: () => void; handleCancel: () => void; - handleSettings: () => void; - handleChangeVersion: () => void; handleRetry: (buildParameters?: WorkspaceBuildParameter[]) => void; handleDebug: (buildParameters?: WorkspaceBuildParameter[]) => void; handleDormantActivate: () => void; - isUpdating: boolean; - isRestarting: boolean; - children?: ReactNode; - canChangeVersions: boolean; - canDebug: boolean; } export const WorkspaceActions: FC = ({ workspace, + isUpdating, + isRestarting, + permissions, handleToggleFavorite, handleStart, handleStop, handleRestart, - handleDelete, handleUpdate, handleCancel, - handleSettings, handleRetry, handleDebug, - handleChangeVersion, handleDormantActivate, - isUpdating, - isRestarting, - canChangeVersions, - canDebug, }) => { - const { duplicateWorkspace, isDuplicationReady } = - useWorkspaceDuplication(workspace); - - const [isDownloadDialogOpen, setIsDownloadDialogOpen] = useState(false); - const { user } = useAuthenticated(); - const isOwner = - user.roles.find((role) => role.name === "owner") !== undefined; + const { data: deployment } = useQuery({ + ...deploymentConfig(), + enabled: permissions.deploymentConfig, + }); const { actions, canCancel, canAcceptJobs } = abilitiesByWorkspaceStatus( workspace, - { canDebug, isOwner }, + { + canDebug: !!deployment?.config.enable_terraform_debug_mode, + isOwner: user.roles.some((role) => role.name === "owner"), + }, ); - const mustUpdate = mustUpdateWorkspace(workspace, canChangeVersions); - const tooltipText = getTooltipText(workspace, mustUpdate, canChangeVersions); + const mustUpdate = mustUpdateWorkspace( + workspace, + permissions.updateWorkspaceVersion, + ); + const tooltipText = getTooltipText( + workspace, + mustUpdate, + permissions.updateWorkspaceVersion, + ); // A mapping of button type to the corresponding React component const buttonMapping: Record = { @@ -179,66 +165,7 @@ export const WorkspaceActions: FC = ({ onToggle={handleToggleFavorite} /> - - - - - - - - - Settings - - - {canChangeVersions && ( - - - Change version… - - )} - - - - Duplicate… - - - setIsDownloadDialogOpen(true)}> - - Download logs… - - - - - - - Delete… - - - - - setIsDownloadDialogOpen(false)} - onConfirm={() => {}} - /> +
); }; diff --git a/site/src/pages/WorkspacePage/WorkspaceDeleteDialog/index.ts b/site/src/pages/WorkspacePage/WorkspaceDeleteDialog/index.ts deleted file mode 100644 index cc0251b4a2ce0..0000000000000 --- a/site/src/pages/WorkspacePage/WorkspaceDeleteDialog/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./WorkspaceDeleteDialog"; diff --git a/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.stories.tsx b/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.stories.tsx index 6f02d925f6485..a35771971b329 100644 --- a/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.stories.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.stories.tsx @@ -1,6 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { expect, userEvent, waitFor, within } from "@storybook/test"; import { getWorkspaceResolveAutostartQueryKey } from "api/queries/workspaceQuota"; +import type { WorkspacePermissions } from "modules/workspaces/permissions"; import { MockOutdatedWorkspace, MockTemplate, @@ -11,11 +12,12 @@ import { import { withDashboardProvider } from "testHelpers/storybook"; import { WorkspaceNotifications } from "./WorkspaceNotifications"; -const defaultPermissions = { +const defaultPermissions: WorkspacePermissions = { readWorkspace: true, - updateTemplate: true, + updateWorkspaceVersion: true, updateWorkspace: true, - viewDeploymentConfig: true, + deploymentConfig: true, + deleteFailedWorkspace: true, }; const meta: Meta = { diff --git a/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.tsx b/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.tsx index 7c72c9777aaeb..976e7bac4fcbb 100644 --- a/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.tsx @@ -15,7 +15,7 @@ import { useDashboard } from "modules/dashboard/useDashboard"; import { TemplateUpdateMessage } from "modules/templates/TemplateUpdateMessage"; import { type FC, useEffect, useState } from "react"; import { useQuery } from "react-query"; -import type { WorkspacePermissions } from "../permissions"; +import type { WorkspacePermissions } from "../../../modules/workspaces/permissions"; import { NotificationActionButton, type NotificationItem, diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index a9f78f3610983..7489be8772ee4 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -7,6 +7,7 @@ import { DashboardContext, type DashboardProvider, } from "modules/dashboard/DashboardProvider"; +import type { WorkspacePermissions } from "modules/workspaces/permissions"; import { http, HttpResponse } from "msw"; import type { FC } from "react"; import { type Location, useLocation } from "react-router-dom"; @@ -126,11 +127,14 @@ describe("WorkspacePage", () => { // set permissions server.use( http.post("/api/v2/authcheck", async () => { - return HttpResponse.json({ - updateTemplates: true, + const permissions: WorkspacePermissions = { + deleteFailedWorkspace: true, + deploymentConfig: true, + readWorkspace: true, updateWorkspace: true, - updateTemplate: true, - }); + updateWorkspaceVersion: true, + }; + return HttpResponse.json(permissions); }), ); diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index e0b5cf1d14bfe..f4b4af024d06e 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -1,8 +1,10 @@ import { watchWorkspace } from "api/api"; -import { checkAuthorization } from "api/queries/authCheck"; import { template as templateQueryOptions } from "api/queries/templates"; import { workspaceBuildsKey } from "api/queries/workspaceBuilds"; -import { workspaceByOwnerAndName } from "api/queries/workspaces"; +import { + workspaceByOwnerAndName, + workspacePermissions, +} from "api/queries/workspaces"; import type { Workspace } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { displayError } from "components/GlobalSnackbar/utils"; @@ -15,7 +17,6 @@ import { type FC, useEffect } from "react"; import { useQuery, useQueryClient } from "react-query"; import { useParams } from "react-router-dom"; import { WorkspaceReadyPage } from "./WorkspaceReadyPage"; -import { type WorkspacePermissions, workspaceChecks } from "./permissions"; const WorkspacePage: FC = () => { const queryClient = useQueryClient(); @@ -43,13 +44,8 @@ const WorkspacePage: FC = () => { const template = templateQuery.data; // Permissions - const checks = - workspace && template ? workspaceChecks(workspace, template) : {}; - const permissionsQuery = useQuery({ - ...checkAuthorization({ checks }), - enabled: workspace !== undefined && template !== undefined, - }); - const permissions = permissionsQuery.data as WorkspacePermissions | undefined; + const permissionsQuery = useQuery(workspacePermissions(workspace)); + const permissions = permissionsQuery.data; // Watch workspace changes const updateWorkspaceData = useEffectEvent( diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 9ae072e420dd1..5de4eb6b490f7 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -1,13 +1,12 @@ -import { API, MissingBuildParameters } from "api/api"; +import { API } from "api/api"; import { getErrorMessage } from "api/errors"; import { buildInfo } from "api/queries/buildInfo"; -import { deploymentConfig, deploymentSSHConfig } from "api/queries/deployment"; -import { templateVersion, templateVersions } from "api/queries/templates"; +import { deploymentSSHConfig } from "api/queries/deployment"; +import { templateVersion } from "api/queries/templates"; import { workspaceBuildTimings } from "api/queries/workspaceBuilds"; import { activate, cancelBuild, - changeVersion, deleteWorkspace, startWorkspace, stopWorkspace, @@ -19,7 +18,6 @@ import { type ConfirmDialogProps, } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; import { displayError } from "components/GlobalSnackbar/utils"; -import dayjs from "dayjs"; import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata"; import { useWorkspaceBuildLogs } from "hooks/useWorkspaceBuildLogs"; import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; @@ -27,16 +25,12 @@ import { WorkspaceUpdateDialogs, useWorkspaceUpdate, } from "modules/workspaces/WorkspaceUpdateDialogs"; +import type { WorkspacePermissions } from "modules/workspaces/permissions"; import { type FC, useEffect, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQuery, useQueryClient } from "react-query"; -import { useNavigate } from "react-router-dom"; import { pageTitle } from "utils/page"; -import { ChangeVersionDialog } from "./ChangeVersionDialog"; -import { UpdateBuildParametersDialog } from "./UpdateBuildParametersDialog"; import { Workspace } from "./Workspace"; -import { WorkspaceDeleteDialog } from "./WorkspaceDeleteDialog"; -import type { WorkspacePermissions } from "./permissions"; interface WorkspaceReadyPageProps { template: TypesGen.Template; @@ -51,19 +45,8 @@ export const WorkspaceReadyPage: FC = ({ }) => { const { metadata } = useEmbeddedMetadata(); const buildInfoQuery = useQuery(buildInfo(metadata["build-info"])); - const navigate = useNavigate(); const queryClient = useQueryClient(); - const featureVisibility = useFeatureVisibility(); - if (workspace === undefined) { - throw Error("Workspace is undefined"); - } - - // Debug mode - const { data: deploymentValues } = useQuery({ - ...deploymentConfig(), - enabled: permissions.viewDeploymentConfig, - }); // Build logs const shouldStreamBuildLogs = workspace.latest_build.status !== "running"; @@ -98,18 +81,7 @@ export const WorkspaceReadyPage: FC = ({ setFaviconTheme(isDark.matches ? "light" : "dark"); }, []); - // Change version - const canChangeVersions = permissions.updateTemplate; - const [changeVersionDialogOpen, setChangeVersionDialogOpen] = useState(false); - const changeVersionMutation = useMutation( - changeVersion(workspace, queryClient), - ); - - // Versions - const { data: allVersions } = useQuery({ - ...templateVersions(workspace.template_id), - enabled: changeVersionDialogOpen, - }); + // Active version const { data: latestVersion } = useQuery({ ...templateVersion(workspace.template_active_version_id), enabled: workspace.outdated, @@ -121,10 +93,7 @@ export const WorkspaceReadyPage: FC = ({ latestVersion, }); - // If a user can update the template then they can force a delete - // (via orphan). - const canUpdateTemplate = Boolean(permissions.updateTemplate); - const [isConfirmingDelete, setIsConfirmingDelete] = useState(false); + // Delete workspace const deleteWorkspaceMutation = useMutation( deleteWorkspace(workspace, queryClient), ); @@ -232,29 +201,27 @@ export const WorkspaceReadyPage: FC = ({ isUpdating={workspaceUpdate.isUpdating} isRestarting={isRestarting} workspace={workspace} + latestVersion={latestVersion} + hideSSHButton={featureVisibility.browser_only} + hideVSCodeDesktopButton={featureVisibility.browser_only} + buildInfo={buildInfoQuery.data} + sshPrefix={sshPrefixQuery.data?.hostname_prefix} + template={template} + buildLogs={buildLogs} + timings={timingsQuery.data} handleStart={(buildParameters) => { startWorkspaceMutation.mutate({ buildParameters }); }} handleStop={() => { stopWorkspaceMutation.mutate({}); }} - handleDelete={() => { - setIsConfirmingDelete(true); - }} handleRestart={(buildParameters) => { setConfirmingRestart({ open: true, buildParameters }); }} handleUpdate={workspaceUpdate.update} handleCancel={cancelBuildMutation.mutate} - handleSettings={() => navigate("settings")} handleRetry={handleRetry} handleDebug={handleDebug} - canDebugMode={ - deploymentValues?.config.enable_terraform_debug_mode ?? false - } - handleChangeVersion={() => { - setChangeVersionDialogOpen(true); - }} handleDormantActivate={async () => { try { await activateWorkspaceMutation.mutateAsync(); @@ -266,65 +233,6 @@ export const WorkspaceReadyPage: FC = ({ handleToggleFavorite={() => { toggleFavoriteMutation.mutate(); }} - latestVersion={latestVersion} - canChangeVersions={canChangeVersions} - hideSSHButton={featureVisibility.browser_only} - hideVSCodeDesktopButton={featureVisibility.browser_only} - buildInfo={buildInfoQuery.data} - sshPrefix={sshPrefixQuery.data?.hostname_prefix} - template={template} - buildLogs={buildLogs} - timings={timingsQuery.data} - /> - - { - setIsConfirmingDelete(false); - }} - onConfirm={(orphan) => { - deleteWorkspaceMutation.mutate({ orphan }); - setIsConfirmingDelete(false); - }} - workspaceBuildDateStr={dayjs(workspace.created_at).fromNow()} - /> - - { - changeVersionMutation.reset(); - }} - onUpdate={(buildParameters) => { - if (changeVersionMutation.error instanceof MissingBuildParameters) { - changeVersionMutation.mutate({ - versionId: changeVersionMutation.error.versionId, - buildParameters, - }); - } - }} - /> - - workspace.latest_build.template_version_id === v.id, - )} - open={changeVersionDialogOpen} - onClose={() => { - setChangeVersionDialogOpen(false); - }} - onConfirm={(templateVersion) => { - setChangeVersionDialogOpen(false); - changeVersionMutation.mutate({ versionId: templateVersion.id }); - }} /> = { workspace: baseWorkspace, template: MockTemplate, latestVersion: MockTemplateVersion, - canUpdateWorkspace: true, + permissions: { + readWorkspace: true, + updateWorkspaceVersion: true, + updateWorkspace: true, + deploymentConfig: true, + deleteFailedWorkspace: true, + }, }, parameters: { layout: "fullscreen", diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index 2af8d694e120e..32908156c5b5c 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -25,64 +25,45 @@ import type { FC } from "react"; import { useQuery } from "react-query"; import { Link as RouterLink } from "react-router-dom"; import { displayDormantDeletion } from "utils/dormant"; +import type { WorkspacePermissions } from "../../modules/workspaces/permissions"; import { WorkspaceActions } from "./WorkspaceActions/WorkspaceActions"; import { WorkspaceNotifications } from "./WorkspaceNotifications/WorkspaceNotifications"; import { WorkspaceScheduleControls } from "./WorkspaceScheduleControls"; -import type { WorkspacePermissions } from "./permissions"; - -export type WorkspaceError = - | "getBuildsError" - | "buildError" - | "cancellationError"; - -type WorkspaceErrors = Partial>; export interface WorkspaceProps { + isUpdating: boolean; + isRestarting: boolean; + workspace: TypesGen.Workspace; + template: TypesGen.Template; + permissions: WorkspacePermissions; + latestVersion?: TypesGen.TemplateVersion; handleStart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; handleStop: () => void; handleRestart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; - handleDelete: () => void; handleUpdate: () => void; handleCancel: () => void; - handleSettings: () => void; - handleChangeVersion: () => void; handleDormantActivate: () => void; - isUpdating: boolean; - isRestarting: boolean; - workspace: TypesGen.Workspace; - canUpdateWorkspace: boolean; - canChangeVersions: boolean; - canDebugMode: boolean; handleRetry: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; handleDebug: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; - template: TypesGen.Template; - permissions: WorkspacePermissions; - latestVersion?: TypesGen.TemplateVersion; handleToggleFavorite: () => void; } export const WorkspaceTopbar: FC = ({ + workspace, + template, + latestVersion, + permissions, + isUpdating, + isRestarting, handleStart, handleStop, handleRestart, - handleDelete, handleUpdate, handleCancel, - handleSettings, - handleChangeVersion, handleDormantActivate, handleToggleFavorite, - workspace, - isUpdating, - isRestarting, - canUpdateWorkspace, - canChangeVersions, - canDebugMode, handleRetry, handleDebug, - template, - latestVersion, - permissions, }) => { const { entitlements, organizations, showOrganizations } = useDashboard(); const getLink = useLinks(); @@ -230,7 +211,7 @@ export const WorkspaceTopbar: FC = ({ = ({ )} diff --git a/site/src/pages/WorkspacePage/permissions.ts b/site/src/pages/WorkspacePage/permissions.ts deleted file mode 100644 index 3ac1df5a3a7fd..0000000000000 --- a/site/src/pages/WorkspacePage/permissions.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { Template, Workspace } from "api/typesGenerated"; - -export const workspaceChecks = (workspace: Workspace, template: Template) => - ({ - readWorkspace: { - object: { - resource_type: "workspace", - resource_id: workspace.id, - owner_id: workspace.owner_id, - }, - action: "read", - }, - updateWorkspace: { - object: { - resource_type: "workspace", - resource_id: workspace.id, - owner_id: workspace.owner_id, - }, - action: "update", - }, - updateTemplate: { - object: { - resource_type: "template", - resource_id: template.id, - }, - action: "update", - }, - viewDeploymentConfig: { - object: { - resource_type: "deployment_config", - }, - action: "read", - }, - }) as const; - -export type WorkspacePermissions = Record< - keyof ReturnType, - boolean ->; diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx index 443e7183cca60..f17bb246966bf 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx @@ -2,7 +2,6 @@ import Button from "@mui/material/Button"; import { API } from "api/api"; import { isApiValidationError } from "api/errors"; import { checkAuthorization } from "api/queries/authCheck"; -import { templateByName } from "api/queries/templates"; import type { Workspace, WorkspaceBuildParameter } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { EmptyState } from "components/EmptyState/EmptyState"; @@ -18,7 +17,7 @@ import { pageTitle } from "utils/page"; import { type WorkspacePermissions, workspaceChecks, -} from "../../WorkspacePage/permissions"; +} from "../../../modules/workspaces/permissions"; import { useWorkspaceSettings } from "../WorkspaceSettingsLayout"; import { WorkspaceParametersForm, @@ -43,21 +42,14 @@ const WorkspaceParametersPage: FC = () => { }, }); - const templateQuery = useQuery({ - ...templateByName(workspace.organization_id, workspace.template_name ?? ""), - enabled: workspace !== undefined, - }); - const template = templateQuery.data; - // Permissions - const checks = - workspace && template ? workspaceChecks(workspace, template) : {}; + const checks = workspace ? workspaceChecks(workspace) : {}; const permissionsQuery = useQuery({ ...checkAuthorization({ checks }), - enabled: workspace !== undefined && template !== undefined, + enabled: workspace !== undefined, }); const permissions = permissionsQuery.data as WorkspacePermissions | undefined; - const canChangeVersions = Boolean(permissions?.updateTemplate); + const canChangeVersions = Boolean(permissions?.updateWorkspaceVersion); return ( <> @@ -72,14 +64,23 @@ const WorkspaceParametersPage: FC = () => { submitError={updateParameters.error} isSubmitting={updateParameters.isLoading} onSubmit={(values) => { + if (!parameters.data) { + return; + } // When updating the parameters, the API does not accept immutable // values so we need to filter them - const onlyMultableValues = parameters - .data!.templateVersionRichParameters.filter((p) => p.mutable) - .map( - (p) => - values.rich_parameter_values.find((v) => v.name === p.name)!, - ); + const onlyMultableValues = + parameters.data.templateVersionRichParameters + .filter((p) => p.mutable) + .map((p) => { + const value = values.rich_parameter_values.find( + (v) => v.name === p.name, + ); + if (!value) { + throw new Error(`Missing value for parameter ${p.name}`); + } + return value; + }); updateParameters.mutate(onlyMultableValues); }} onCancel={() => { diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx index b1ad1d887e53c..c8577f191d47e 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx @@ -68,7 +68,7 @@ describe("WorkspacesPage", () => { await user.click(getWorkspaceCheckbox(workspaces[0])); await user.click(getWorkspaceCheckbox(workspaces[1])); - await user.click(screen.getByRole("button", { name: /actions/i })); + await user.click(screen.getByRole("button", { name: /bulk actions/i })); const deleteButton = await screen.findByText(/delete/i); await user.click(deleteButton); @@ -106,7 +106,7 @@ describe("WorkspacesPage", () => { await user.click(getWorkspaceCheckbox(workspace)); } - await user.click(screen.getByRole("button", { name: /actions/i })); + await user.click(screen.getByRole("button", { name: /bulk actions/i })); const updateButton = await screen.findByText(/update/i); await user.click(updateButton); @@ -145,7 +145,7 @@ describe("WorkspacesPage", () => { await user.click(getWorkspaceCheckbox(workspace)); } - await user.click(screen.getByRole("button", { name: /actions/i })); + await user.click(screen.getByRole("button", { name: /bulk actions/i })); const updateButton = await screen.findByText(/update/i); await user.click(updateButton); @@ -183,7 +183,7 @@ describe("WorkspacesPage", () => { await user.click(getWorkspaceCheckbox(workspace)); } - await user.click(screen.getByRole("button", { name: /actions/i })); + await user.click(screen.getByRole("button", { name: /bulk actions/i })); const updateButton = await screen.findByText(/update/i); await user.click(updateButton); @@ -223,7 +223,7 @@ describe("WorkspacesPage", () => { await user.click(getWorkspaceCheckbox(workspace)); } - await user.click(screen.getByRole("button", { name: /actions/i })); + await user.click(screen.getByRole("button", { name: /bulk actions/i })); const updateButton = await screen.findByText(/update/i); await user.click(updateButton); @@ -263,7 +263,7 @@ describe("WorkspacesPage", () => { await user.click(getWorkspaceCheckbox(workspaces[0])); await user.click(getWorkspaceCheckbox(workspaces[1])); - await user.click(screen.getByRole("button", { name: /actions/i })); + await user.click(screen.getByRole("button", { name: /bulk actions/i })); const stopButton = await screen.findByRole("menuitem", { name: /stop/i }); await user.click(stopButton); @@ -290,7 +290,7 @@ describe("WorkspacesPage", () => { await user.click(getWorkspaceCheckbox(workspaces[0])); await user.click(getWorkspaceCheckbox(workspaces[1])); - await user.click(screen.getByRole("button", { name: /actions/i })); + await user.click(screen.getByRole("button", { name: /bulk actions/i })); const startButton = await screen.findByRole("menuitem", { name: /start/i }); await user.click(startButton); diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index a62c99d591d7c..569e3df0d347c 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -143,7 +143,7 @@ export const WorkspacesPageView: FC = ({ css={{ borderRadius: 9999, marginLeft: "auto" }} endIcon={} > - Actions + Bulk actions diff --git a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx index 4ec1156b7fcd5..b5453e424dfd7 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx @@ -52,7 +52,7 @@ import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; import { useAuthenticated } from "hooks"; import { useClickableTableRow } from "hooks/useClickableTableRow"; -import { ChevronRightIcon } from "lucide-react"; +import { EllipsisVertical } from "lucide-react"; import { BanIcon, PlayIcon, @@ -68,6 +68,7 @@ import { useAppLink } from "modules/apps/useAppLink"; import { useDashboard } from "modules/dashboard/useDashboard"; import { WorkspaceAppStatus } from "modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus"; import { WorkspaceDormantBadge } from "modules/workspaces/WorkspaceDormantBadge/WorkspaceDormantBadge"; +import { WorkspaceMoreActions } from "modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions"; import { WorkspaceOutdatedTooltip } from "modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip"; import { WorkspaceUpdateDialogs, @@ -185,7 +186,6 @@ export const WorkspacesTable: FC = ({ Template Status - @@ -303,11 +303,6 @@ export const WorkspacesTable: FC = ({ onActionSuccess={onActionSuccess} onActionError={onActionError} /> - -
- -
-
); })} @@ -382,12 +377,12 @@ const TableLoader: FC = ({ canCheckWorkspaces }) => { - - - - -
- + +
+ +
@@ -537,7 +532,12 @@ const WorkspaceActionsCell: FC = ({ }; return ( - + { + // Prevent the click in the actions to trigger the row click + e.stopPropagation(); + }} + >
{workspace.latest_build.status === "running" && ( @@ -585,6 +585,11 @@ const WorkspaceActionsCell: FC = ({ )} + +
); @@ -606,14 +611,7 @@ const PrimaryAction: FC = ({ - @@ -731,6 +729,8 @@ const WorkspaceApps: FC = ({ workspace }) => { ); } + buttons.push(); + return buttons; }; diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 7fa69c006b8e7..5cc689f0bc01a 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -753,6 +753,8 @@ You can add instructions here export const MockTemplateVersionWithMarkdownMessage: TypesGen.TemplateVersion = { ...MockTemplateVersion, + id: "test-template-version-markdown", + name: "test-version-markdown", message: ` # Abiding Grace ## Enchantment From c71839294b6db1664005d039fda6f7ee457d1156 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 14 May 2025 08:41:33 -0300 Subject: [PATCH 33/88] fix: don't open a window for external apps (#17813) This prevents empty windows like the following to happen: ![image](https://github.com/user-attachments/assets/0a444938-316e-4d48-bdfc-770d1b4b2bf0) --- site/src/modules/apps/useAppLink.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/site/src/modules/apps/useAppLink.ts b/site/src/modules/apps/useAppLink.ts index 9916aaf95fe75..efaab474e6db9 100644 --- a/site/src/modules/apps/useAppLink.ts +++ b/site/src/modules/apps/useAppLink.ts @@ -55,6 +55,10 @@ export const useAppLink = ( window.addEventListener("blur", () => { clearTimeout(openAppExternallyFailed); }); + + // External apps don't support open_in since they only should open + // external apps. + return; } switch (app.open_in) { From f87dbe757ecba39710e4f7ff681d04feda96c844 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 14 May 2025 08:48:08 -0300 Subject: [PATCH 34/88] chore: replace MUI icons with Lucide icons - 11 (#17814) PersonOutlined -> UserIcon Schedule -> ClockIcon SettingsSuggest -> SettingsIcon SettingsOutlined -> SettingsIcon CodeOutlined -> CodeIcon TimerOutlined -> TimerIcon --- site/src/pages/HealthPage/DERPRegionPage.tsx | 6 ++++-- site/src/pages/HealthPage/WebsocketPage.tsx | 6 ++++-- .../pages/TemplatePage/TemplatePageHeader.tsx | 4 ++-- site/src/pages/TemplateSettingsPage/Sidebar.tsx | 4 ++-- site/src/pages/WorkspaceSettingsPage/Sidebar.tsx | 6 +++--- .../WorkspacesPage/BatchDeleteConfirmation.tsx | 13 +++++-------- .../WorkspacesPage/BatchUpdateConfirmation.tsx | 16 ++++++---------- 7 files changed, 26 insertions(+), 29 deletions(-) diff --git a/site/src/pages/HealthPage/DERPRegionPage.tsx b/site/src/pages/HealthPage/DERPRegionPage.tsx index 4a1be16138315..a32350e7afe2b 100644 --- a/site/src/pages/HealthPage/DERPRegionPage.tsx +++ b/site/src/pages/HealthPage/DERPRegionPage.tsx @@ -1,6 +1,5 @@ import { type Interpolation, type Theme, useTheme } from "@emotion/react"; import ArrowBackOutlined from "@mui/icons-material/ArrowBackOutlined"; -import CodeOutlined from "@mui/icons-material/CodeOutlined"; import TagOutlined from "@mui/icons-material/TagOutlined"; import Tooltip from "@mui/material/Tooltip"; import type { @@ -11,6 +10,7 @@ import type { HealthcheckReport, } from "api/typesGenerated"; import { Alert } from "components/Alert/Alert"; +import { CodeIcon } from "lucide-react"; import type { FC } from "react"; import { Helmet } from "react-helmet-async"; import { Link, useOutletContext, useParams } from "react-router-dom"; @@ -94,7 +94,9 @@ const DERPRegionPage: FC = () => { }>{region!.RegionID} - }>{region!.RegionCode} + }> + {region!.RegionCode} + Embedded Relay diff --git a/site/src/pages/HealthPage/WebsocketPage.tsx b/site/src/pages/HealthPage/WebsocketPage.tsx index a3f4a92d4da0b..fed223163e8e1 100644 --- a/site/src/pages/HealthPage/WebsocketPage.tsx +++ b/site/src/pages/HealthPage/WebsocketPage.tsx @@ -1,8 +1,8 @@ import { useTheme } from "@emotion/react"; -import CodeOutlined from "@mui/icons-material/CodeOutlined"; import Tooltip from "@mui/material/Tooltip"; import type { HealthcheckReport } from "api/typesGenerated"; import { Alert } from "components/Alert/Alert"; +import { CodeIcon } from "lucide-react"; import { Helmet } from "react-helmet-async"; import { useOutletContext } from "react-router-dom"; import { MONOSPACE_FONT_FAMILY } from "theme/constants"; @@ -49,7 +49,9 @@ const WebsocketPage = () => {
- }>{websocket.code} + }> + {websocket.code} +
diff --git a/site/src/pages/TemplatePage/TemplatePageHeader.tsx b/site/src/pages/TemplatePage/TemplatePageHeader.tsx index 48fe621f2b827..834c83905cbf5 100644 --- a/site/src/pages/TemplatePage/TemplatePageHeader.tsx +++ b/site/src/pages/TemplatePage/TemplatePageHeader.tsx @@ -1,7 +1,6 @@ import AddIcon from "@mui/icons-material/AddOutlined"; import EditIcon from "@mui/icons-material/EditOutlined"; import CopyIcon from "@mui/icons-material/FileCopyOutlined"; -import SettingsIcon from "@mui/icons-material/SettingsOutlined"; import Button from "@mui/material/Button"; import { workspaces } from "api/queries/workspaces"; import type { @@ -29,6 +28,7 @@ import { } from "components/PageHeader/PageHeader"; import { Pill } from "components/Pill/Pill"; import { Stack } from "components/Stack/Stack"; +import { SettingsIcon } from "lucide-react"; import { TrashIcon } from "lucide-react"; import { EllipsisVertical } from "lucide-react"; import { linkToTemplate, useLinks } from "modules/navigation"; @@ -79,7 +79,7 @@ const TemplateMenu: FC = ({ navigate(`${templateLink}/settings`)} > - + Settings diff --git a/site/src/pages/TemplateSettingsPage/Sidebar.tsx b/site/src/pages/TemplateSettingsPage/Sidebar.tsx index f8b41f7829fd4..1aaa426061968 100644 --- a/site/src/pages/TemplateSettingsPage/Sidebar.tsx +++ b/site/src/pages/TemplateSettingsPage/Sidebar.tsx @@ -1,5 +1,3 @@ -import VariablesIcon from "@mui/icons-material/CodeOutlined"; -import ScheduleIcon from "@mui/icons-material/TimerOutlined"; import type { Template } from "api/typesGenerated"; import { Avatar } from "components/Avatar/Avatar"; import { @@ -7,6 +5,8 @@ import { SidebarHeader, SidebarNavItem, } from "components/Sidebar/Sidebar"; +import { CodeIcon as VariablesIcon } from "lucide-react"; +import { TimerIcon as ScheduleIcon } from "lucide-react"; import { SettingsIcon } from "lucide-react"; import { LockIcon } from "lucide-react"; import { linkToTemplate, useLinks } from "modules/navigation"; diff --git a/site/src/pages/WorkspaceSettingsPage/Sidebar.tsx b/site/src/pages/WorkspaceSettingsPage/Sidebar.tsx index 604fc2ed70d23..91aea9ac9cf12 100644 --- a/site/src/pages/WorkspaceSettingsPage/Sidebar.tsx +++ b/site/src/pages/WorkspaceSettingsPage/Sidebar.tsx @@ -1,6 +1,3 @@ -import ParameterIcon from "@mui/icons-material/CodeOutlined"; -import GeneralIcon from "@mui/icons-material/SettingsOutlined"; -import ScheduleIcon from "@mui/icons-material/TimerOutlined"; import type { Workspace } from "api/typesGenerated"; import { Avatar } from "components/Avatar/Avatar"; import { @@ -8,6 +5,9 @@ import { SidebarHeader, SidebarNavItem, } from "components/Sidebar/Sidebar"; +import { CodeIcon as ParameterIcon } from "lucide-react"; +import { SettingsIcon as GeneralIcon } from "lucide-react"; +import { TimerIcon as ScheduleIcon } from "lucide-react"; import type { FC } from "react"; interface SidebarProps { diff --git a/site/src/pages/WorkspacesPage/BatchDeleteConfirmation.tsx b/site/src/pages/WorkspacesPage/BatchDeleteConfirmation.tsx index a4a79c0c1e91f..587cecf25efdd 100644 --- a/site/src/pages/WorkspacesPage/BatchDeleteConfirmation.tsx +++ b/site/src/pages/WorkspacesPage/BatchDeleteConfirmation.tsx @@ -1,6 +1,4 @@ import { type Interpolation, type Theme, useTheme } from "@emotion/react"; -import PersonOutlinedIcon from "@mui/icons-material/PersonOutlined"; -import ScheduleIcon from "@mui/icons-material/Schedule"; import { visuallyHidden } from "@mui/utils"; import type { Workspace } from "api/typesGenerated"; import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; @@ -8,6 +6,7 @@ import { ExternalImage } from "components/ExternalImage/ExternalImage"; import { Stack } from "components/Stack/Stack"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; +import { ClockIcon, UserIcon } from "lucide-react"; import { type FC, type ReactNode, useState } from "react"; import { getResourceIconPath } from "utils/workspace"; @@ -190,7 +189,7 @@ const Workspaces: FC = ({ workspaces }) => { > {dayjs(workspace.last_used_at).fromNow()} - + @@ -209,7 +208,7 @@ const Workspaces: FC = ({ workspaces }) => { {mostRecent && ( - + Last used {dayjs(mostRecent.last_used_at).fromNow()} )} @@ -264,10 +263,8 @@ const Resources: FC = ({ workspaces }) => { }; const PersonIcon: FC = () => { - // This size doesn't match the rest of the icons because MUI is just really - // inconsistent. We have to make it bigger than the rest, and pull things in - // on the sides to compensate. - return ; + // Using the Lucide icon with appropriate size class + return ; }; const styles = { diff --git a/site/src/pages/WorkspacesPage/BatchUpdateConfirmation.tsx b/site/src/pages/WorkspacesPage/BatchUpdateConfirmation.tsx index bd4fef445bf8e..ac36009dacb70 100644 --- a/site/src/pages/WorkspacesPage/BatchUpdateConfirmation.tsx +++ b/site/src/pages/WorkspacesPage/BatchUpdateConfirmation.tsx @@ -1,8 +1,5 @@ import type { Interpolation, Theme } from "@emotion/react"; import InstallDesktopIcon from "@mui/icons-material/InstallDesktop"; -import PersonOutlinedIcon from "@mui/icons-material/PersonOutlined"; -import ScheduleIcon from "@mui/icons-material/Schedule"; -import SettingsSuggestIcon from "@mui/icons-material/SettingsSuggest"; import { API } from "api/api"; import type { TemplateVersion, Workspace } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; @@ -12,6 +9,7 @@ import { MemoizedInlineMarkdown } from "components/Markdown/Markdown"; import { Stack } from "components/Stack/Stack"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; +import { ClockIcon, SettingsIcon, UserIcon } from "lucide-react"; import { type FC, type ReactNode, useEffect, useMemo, useState } from "react"; import { useQueries } from "react-query"; @@ -293,7 +291,7 @@ const DormantWorkspaces: FC = ({ workspaces }) => { - + @@ -317,7 +315,7 @@ const DormantWorkspaces: FC = ({ workspaces }) => { {mostRecent && ( - + Last used {lastUsed(mostRecent.last_used_at)} )} @@ -358,7 +356,7 @@ const Updates: FC = ({ workspaces, updates, error }) => { {updateCount && ( - + {updateCount} )} @@ -433,10 +431,8 @@ const lastUsed = (time: string) => { }; const PersonIcon: FC = () => { - // This size doesn't match the rest of the icons because MUI is just really - // inconsistent. We have to make it bigger than the rest, and pull things in - // on the sides to compensate. - return ; + // Using the Lucide icon with appropriate size class + return ; }; const styles = { From 80e1be0db12b8733a0f50cc2a3226385353b5f1f Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 14 May 2025 09:03:01 -0300 Subject: [PATCH 35/88] fix: replace wrong emoji reference (#17810) Before: Screenshot 2025-05-13 at 19 01 15 After: Screenshot 2025-05-13 at 19 02 22 --- provisioner/terraform/resources.go | 2 +- provisioner/terraform/resources_test.go | 2 +- site/src/modules/resources/AgentLogs/mocks.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/provisioner/terraform/resources.go b/provisioner/terraform/resources.go index ce881480ad3aa..d474c24289ef3 100644 --- a/provisioner/terraform/resources.go +++ b/provisioner/terraform/resources.go @@ -324,7 +324,7 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s if attrs.StartupScript != "" { agent.Scripts = append(agent.Scripts, &proto.Script{ // This is ▶️ - Icon: "/emojis/25b6.png", + Icon: "/emojis/25b6-fe0f.png", LogPath: "coder-startup-script.log", DisplayName: "Startup Script", Script: attrs.StartupScript, diff --git a/provisioner/terraform/resources_test.go b/provisioner/terraform/resources_test.go index 61c21ea532b53..94d63b90a3419 100644 --- a/provisioner/terraform/resources_test.go +++ b/provisioner/terraform/resources_test.go @@ -561,7 +561,7 @@ func TestConvertResources(t *testing.T) { DisplayName: "Startup Script", RunOnStart: true, LogPath: "coder-startup-script.log", - Icon: "/emojis/25b6.png", + Icon: "/emojis/25b6-fe0f.png", Script: " #!/bin/bash\n # home folder can be empty, so copying default bash settings\n if [ ! -f ~/.profile ]; then\n cp /etc/skel/.profile $HOME\n fi\n if [ ! -f ~/.bashrc ]; then\n cp /etc/skel/.bashrc $HOME\n fi\n # install and start code-server\n curl -fsSL https://code-server.dev/install.sh | sh | tee code-server-install.log\n code-server --auth none --port 13337 | tee code-server-install.log &\n", }}, }}, diff --git a/site/src/modules/resources/AgentLogs/mocks.tsx b/site/src/modules/resources/AgentLogs/mocks.tsx index de08e816614c0..44ade3b17f0b1 100644 --- a/site/src/modules/resources/AgentLogs/mocks.tsx +++ b/site/src/modules/resources/AgentLogs/mocks.tsx @@ -8,7 +8,7 @@ export const MockSources = [ id: "d9475581-8a42-4bce-b4d0-e4d2791d5c98", created_at: "2024-03-14T11:31:03.443877Z", display_name: "Startup Script", - icon: "/emojis/25b6.png", + icon: "/emojis/25b6-fe0f.png", }, { workspace_agent_id: "722654da-cd27-4edf-a525-54979c864344", From fcbdd1a28e0097142ba005a352d57ae39390ba93 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 14 May 2025 09:11:25 -0300 Subject: [PATCH 36/88] refactor: replace badge by status indicator (#17811) **Why?** In the workspaces page, it is using the status indicator, and not the badge anymore, so to keep the UI consistent, I'm replacing the badge by the indicator in the workspace page too. **Before:** Screenshot 2025-05-13 at 19 14 17 **After:** Screenshot 2025-05-13 at 19 14 21 --- site/e2e/helpers.ts | 24 +++-- .../WorkspaceStatusBadge.stories.tsx | 93 ------------------- .../WorkspaceStatusBadge.tsx | 90 ------------------ .../WorkspaceStatusIndicator.stories.tsx | 82 ++++++++++++++++ .../WorkspaceStatusIndicator.tsx | 49 ++++++++++ .../pages/WorkspacePage/WorkspaceTopbar.tsx | 15 ++- .../pages/WorkspacesPage/WorkspacesTable.tsx | 35 +------ 7 files changed, 151 insertions(+), 237 deletions(-) delete mode 100644 site/src/modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge.stories.tsx delete mode 100644 site/src/modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx create mode 100644 site/src/modules/workspaces/WorkspaceStatusIndicator/WorkspaceStatusIndicator.stories.tsx create mode 100644 site/src/modules/workspaces/WorkspaceStatusIndicator/WorkspaceStatusIndicator.tsx diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index ffadc3fa342f2..d56dc51f66622 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -152,7 +152,7 @@ export const createWorkspace = async ( const user = currentUser(page); await expectUrl(page).toHavePathName(`/@${user.username}/${name}`); - await page.waitForSelector("[data-testid='build-status'] >> text=Running", { + await page.waitForSelector("text=Workspace status: Running", { state: "visible", }); return name; @@ -364,7 +364,7 @@ export const stopWorkspace = async (page: Page, workspaceName: string) => { await page.getByTestId("workspace-stop-button").click(); - await page.waitForSelector("*[data-testid='build-status'] >> text=Stopped", { + await page.waitForSelector("text=Workspace status: Stopped", { state: "visible", }); }; @@ -389,7 +389,7 @@ export const buildWorkspaceWithParameters = async ( await page.getByTestId("confirm-button").click(); } - await page.waitForSelector("*[data-testid='build-status'] >> text=Running", { + await page.waitForSelector("text=Workspace status: Running", { state: "visible", }); }; @@ -412,11 +412,12 @@ export const startAgent = async ( export const downloadCoderVersion = async ( version: string, ): Promise => { - if (version.startsWith("v")) { - version = version.slice(1); + let versionNumber = version; + if (versionNumber.startsWith("v")) { + versionNumber = versionNumber.slice(1); } - const binaryName = `coder-e2e-${version}`; + const binaryName = `coder-e2e-${versionNumber}`; const tempDir = "/tmp/coder-e2e-cache"; // The install script adds `./bin` automatically to the path :shrug: const binaryPath = path.join(tempDir, "bin", binaryName); @@ -438,7 +439,7 @@ export const downloadCoderVersion = async ( path.join(__dirname, "../../install.sh"), [ "--version", - version, + versionNumber, "--method", "standalone", "--prefix", @@ -551,11 +552,8 @@ const emptyPlan = new TextEncoder().encode("{}"); * converts it into an uploadable tar file. */ const createTemplateVersionTar = async ( - responses?: EchoProvisionerResponses, + responses: EchoProvisionerResponses = {}, ): Promise => { - if (!responses) { - responses = {}; - } if (!responses.parse) { responses.parse = [ { @@ -1012,7 +1010,7 @@ export const updateWorkspace = async ( await fillParameters(page, richParameters, buildParameters); await page.getByRole("button", { name: /update parameters/i }).click(); - await page.waitForSelector("*[data-testid='build-status'] >> text=Running", { + await page.waitForSelector("text=Workspace status: Running", { state: "visible", }); }; @@ -1031,7 +1029,7 @@ export const updateWorkspaceParameters = async ( await fillParameters(page, richParameters, buildParameters); await page.getByRole("button", { name: /submit and restart/i }).click(); - await page.waitForSelector("*[data-testid='build-status'] >> text=Running", { + await page.waitForSelector("text=Workspace status: Running", { state: "visible", }); }; diff --git a/site/src/modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge.stories.tsx b/site/src/modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge.stories.tsx deleted file mode 100644 index 352153cda65db..0000000000000 --- a/site/src/modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge.stories.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { - MockBuildInfo, - MockCanceledWorkspace, - MockCancelingWorkspace, - MockDeletedWorkspace, - MockDeletingWorkspace, - MockFailedWorkspace, - MockPendingWorkspace, - MockStartingWorkspace, - MockStoppedWorkspace, - MockStoppingWorkspace, - MockWorkspace, -} from "testHelpers/entities"; -import { withDashboardProvider } from "testHelpers/storybook"; -import { WorkspaceStatusBadge } from "./WorkspaceStatusBadge"; - -const meta: Meta = { - title: "modules/workspaces/WorkspaceStatusBadge", - component: WorkspaceStatusBadge, - parameters: { - queries: [ - { - key: ["buildInfo"], - data: MockBuildInfo, - }, - ], - }, - decorators: [withDashboardProvider], -}; - -export default meta; -type Story = StoryObj; - -export const Running: Story = { - args: { - workspace: MockWorkspace, - }, -}; - -export const Starting: Story = { - args: { - workspace: MockStartingWorkspace, - }, -}; - -export const Stopped: Story = { - args: { - workspace: MockStoppedWorkspace, - }, -}; - -export const Stopping: Story = { - args: { - workspace: MockStoppingWorkspace, - }, -}; - -export const Deleting: Story = { - args: { - workspace: MockDeletingWorkspace, - }, -}; - -export const Deleted: Story = { - args: { - workspace: MockDeletedWorkspace, - }, -}; - -export const Canceling: Story = { - args: { - workspace: MockCancelingWorkspace, - }, -}; - -export const Canceled: Story = { - args: { - workspace: MockCanceledWorkspace, - }, -}; - -export const Failed: Story = { - args: { - workspace: MockFailedWorkspace, - }, -}; - -export const Pending: Story = { - args: { - workspace: MockPendingWorkspace, - }, -}; diff --git a/site/src/modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx b/site/src/modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx deleted file mode 100644 index 1bde6f9181ba6..0000000000000 --- a/site/src/modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import Tooltip, { - type TooltipProps, - tooltipClasses, -} from "@mui/material/Tooltip"; -import type { Workspace } from "api/typesGenerated"; -import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; -import { Pill } from "components/Pill/Pill"; -import { useClassName } from "hooks/useClassName"; -import { CircleAlertIcon } from "lucide-react"; -import type { FC, ReactNode } from "react"; -import { getDisplayWorkspaceStatus } from "utils/workspace"; - -export type WorkspaceStatusBadgeProps = { - workspace: Workspace; - children?: ReactNode; - className?: string; -}; - -export const WorkspaceStatusBadge: FC = ({ - workspace, - className, -}) => { - const { text, icon, type } = getDisplayWorkspaceStatus( - workspace.latest_build.status, - workspace.latest_build.job, - ); - - return ( - - - -
- } - placement="top" - > - - {text} - - - - - - {text} - - - - ); -}; - -const FailureTooltip: FC = ({ children, ...tooltipProps }) => { - const popper = useClassName( - (css, theme) => css` - & .${tooltipClasses.tooltip} { - background-color: ${theme.palette.background.paper}; - border: 1px solid ${theme.palette.divider}; - font-size: 12px; - padding: 8px 10px; - } - `, - [], - ); - - return ( - - {children} - - ); -}; diff --git a/site/src/modules/workspaces/WorkspaceStatusIndicator/WorkspaceStatusIndicator.stories.tsx b/site/src/modules/workspaces/WorkspaceStatusIndicator/WorkspaceStatusIndicator.stories.tsx new file mode 100644 index 0000000000000..75205db8ff698 --- /dev/null +++ b/site/src/modules/workspaces/WorkspaceStatusIndicator/WorkspaceStatusIndicator.stories.tsx @@ -0,0 +1,82 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { Workspace, WorkspaceStatus } from "api/typesGenerated"; +import { MockWorkspace } from "testHelpers/entities"; +import { WorkspaceStatusIndicator } from "./WorkspaceStatusIndicator"; + +const meta: Meta = { + title: "modules/workspaces/WorkspaceStatusIndicator", + component: WorkspaceStatusIndicator, +}; + +export default meta; +type Story = StoryObj; + +const createWorkspaceWithStatus = (status: WorkspaceStatus): Workspace => { + return { + ...MockWorkspace, + latest_build: { + ...MockWorkspace.latest_build, + status, + }, + } as Workspace; +}; + +export const Running: Story = { + args: { + workspace: createWorkspaceWithStatus("running"), + }, +}; + +export const Stopped: Story = { + args: { + workspace: createWorkspaceWithStatus("stopped"), + }, +}; + +export const Starting: Story = { + args: { + workspace: createWorkspaceWithStatus("starting"), + }, +}; + +export const Stopping: Story = { + args: { + workspace: createWorkspaceWithStatus("stopping"), + }, +}; + +export const Failed: Story = { + args: { + workspace: createWorkspaceWithStatus("failed"), + }, +}; + +export const Canceling: Story = { + args: { + workspace: createWorkspaceWithStatus("canceling"), + }, +}; + +export const Canceled: Story = { + args: { + workspace: createWorkspaceWithStatus("canceled"), + }, +}; + +export const Deleting: Story = { + args: { + workspace: createWorkspaceWithStatus("deleting"), + }, +}; + +export const Deleted: Story = { + args: { + workspace: createWorkspaceWithStatus("deleted"), + }, +}; + +export const Pending: Story = { + args: { + workspace: createWorkspaceWithStatus("pending"), + }, +}; diff --git a/site/src/modules/workspaces/WorkspaceStatusIndicator/WorkspaceStatusIndicator.tsx b/site/src/modules/workspaces/WorkspaceStatusIndicator/WorkspaceStatusIndicator.tsx new file mode 100644 index 0000000000000..bd928844cdc0f --- /dev/null +++ b/site/src/modules/workspaces/WorkspaceStatusIndicator/WorkspaceStatusIndicator.tsx @@ -0,0 +1,49 @@ +import type { Workspace } from "api/typesGenerated"; +import { + StatusIndicator, + StatusIndicatorDot, + type StatusIndicatorProps, +} from "components/StatusIndicator/StatusIndicator"; +import type { FC } from "react"; +import type React from "react"; +import { + type DisplayWorkspaceStatusType, + getDisplayWorkspaceStatus, +} from "utils/workspace"; + +const variantByStatusType: Record< + DisplayWorkspaceStatusType, + StatusIndicatorProps["variant"] +> = { + active: "pending", + inactive: "inactive", + success: "success", + error: "failed", + danger: "warning", + warning: "warning", +}; + +type WorkspaceStatusIndicatorProps = { + workspace: Workspace; + children?: React.ReactNode; +}; + +export const WorkspaceStatusIndicator: FC = ({ + workspace, + children, +}) => { + const { text, type } = getDisplayWorkspaceStatus( + workspace.latest_build.status, + workspace.latest_build.job, + ); + + return ( + + + + Workspace status: {text} + + {children} + + ); +}; diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index 32908156c5b5c..8f75e615895f6 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -20,7 +20,7 @@ import { Popover, PopoverTrigger } from "components/deprecated/Popover/Popover"; import { TrashIcon } from "lucide-react"; import { useDashboard } from "modules/dashboard/useDashboard"; import { linkToTemplate, useLinks } from "modules/navigation"; -import { WorkspaceStatusBadge } from "modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge"; +import { WorkspaceStatusIndicator } from "modules/workspaces/WorkspaceStatusIndicator/WorkspaceStatusIndicator"; import type { FC } from "react"; import { useQuery } from "react-query"; import { Link as RouterLink } from "react-router-dom"; @@ -201,18 +201,13 @@ export const WorkspaceTopbar: FC = ({ {!isImmutable && ( -
+
+ = ({ onUpdateWorkspace={handleUpdate} onActivateWorkspace={handleDormantActivate} /> - + + + = { - active: "pending", - inactive: "inactive", - success: "success", - error: "failed", - danger: "warning", - warning: "warning", -}; - const WorkspaceStatusCell: FC = ({ workspace }) => { - const { text, type } = getDisplayWorkspaceStatus( - workspace.latest_build.status, - workspace.latest_build.job, - ); - return (
- - - {text} + {workspace.latest_build.status === "running" && !workspace.health.healthy && ( = ({ workspace }) => { {workspace.dormant_at && ( )} - + {lastUsedMessage(workspace.last_used_at)} From 425ee6fa55358d59363b6af1592f5f235c82b42f Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Wed, 14 May 2025 14:15:36 +0200 Subject: [PATCH 37/88] feat: reinitialize agents when a prebuilt workspace is claimed (#17475) This pull request allows coder workspace agents to be reinitialized when a prebuilt workspace is claimed by a user. This facilitates the transfer of ownership between the anonymous prebuilds system user and the new owner of the workspace. Only a single agent per prebuilt workspace is supported for now, but plumbing has already been done to facilitate the seamless transition to multi-agent support. --------- Signed-off-by: Danny Kopping Co-authored-by: Danny Kopping --- agent/agent.go | 8 +- cli/agent.go | 114 ++- coderd/apidoc/docs.go | 45 + coderd/apidoc/swagger.json | 37 + coderd/coderd.go | 4 +- coderd/coderdtest/coderdtest.go | 63 ++ coderd/database/dbauthz/dbauthz.go | 9 + coderd/database/dbauthz/dbauthz_test.go | 32 + coderd/database/dbfake/dbfake.go | 28 + coderd/database/dbgen/dbgen.go | 1 + coderd/database/dbmem/dbmem.go | 24 + coderd/database/dbmetrics/querymetrics.go | 7 + coderd/database/dbmock/dbmock.go | 15 + coderd/database/querier.go | 1 + coderd/database/queries.sql.go | 81 +- coderd/database/queries/presets.sql | 2 + coderd/database/queries/workspaceagents.sql | 13 + coderd/prebuilds/claim.go | 82 ++ coderd/prebuilds/claim_test.go | 141 +++ .../provisionerdserver/provisionerdserver.go | 41 + .../provisionerdserver_test.go | 205 ++++- coderd/workspaceagents.go | 55 ++ coderd/workspaceagents_test.go | 70 ++ coderd/workspaces.go | 5 +- coderd/wsbuilder/wsbuilder.go | 3 +- codersdk/agentsdk/agentsdk.go | 190 +++- codersdk/agentsdk/agentsdk_test.go | 122 +++ codersdk/client.go | 2 +- docs/reference/api/agents.md | 32 + docs/reference/api/schemas.md | 30 + enterprise/coderd/workspaceagents_test.go | 169 ++++ enterprise/coderd/workspaces_test.go | 78 ++ provisioner/terraform/executor.go | 64 ++ provisioner/terraform/provision.go | 11 + provisionerd/proto/version.go | 1 + provisionersdk/proto/provisioner.pb.go | 824 ++++++++++-------- provisionersdk/proto/provisioner.proto | 6 +- site/e2e/provisionerGenerated.ts | 24 +- 38 files changed, 2187 insertions(+), 452 deletions(-) create mode 100644 coderd/prebuilds/claim.go create mode 100644 coderd/prebuilds/claim_test.go create mode 100644 codersdk/agentsdk/agentsdk_test.go diff --git a/agent/agent.go b/agent/agent.go index 99868eefe1eb7..1eed8b54edd43 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -368,9 +368,11 @@ func (a *agent) runLoop() { if ctx.Err() != nil { // Context canceled errors may come from websocket pings, so we // don't want to use `errors.Is(err, context.Canceled)` here. + a.logger.Warn(ctx, "runLoop exited with error", slog.Error(ctx.Err())) return } if a.isClosed() { + a.logger.Warn(ctx, "runLoop exited because agent is closed") return } if errors.Is(err, io.EOF) { @@ -1051,7 +1053,11 @@ func (a *agent) run() (retErr error) { return a.statsReporter.reportLoop(ctx, aAPI) }) - return connMan.wait() + err = connMan.wait() + if err != nil { + a.logger.Info(context.Background(), "connection manager errored", slog.Error(err)) + } + return err } // handleManifest returns a function that fetches and processes the manifest diff --git a/cli/agent.go b/cli/agent.go index b0dc00ad8020a..50d9db18beee1 100644 --- a/cli/agent.go +++ b/cli/agent.go @@ -25,6 +25,8 @@ import ( "cdr.dev/slog/sloggers/sloghuman" "cdr.dev/slog/sloggers/slogjson" "cdr.dev/slog/sloggers/slogstackdriver" + "github.com/coder/serpent" + "github.com/coder/coder/v2/agent" "github.com/coder/coder/v2/agent/agentexec" "github.com/coder/coder/v2/agent/agentssh" @@ -33,7 +35,6 @@ import ( "github.com/coder/coder/v2/cli/clilog" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" - "github.com/coder/serpent" ) func (r *RootCmd) workspaceAgent() *serpent.Command { @@ -63,8 +64,10 @@ func (r *RootCmd) workspaceAgent() *serpent.Command { // This command isn't useful to manually execute. Hidden: true, Handler: func(inv *serpent.Invocation) error { - ctx, cancel := context.WithCancel(inv.Context()) - defer cancel() + ctx, cancel := context.WithCancelCause(inv.Context()) + defer func() { + cancel(xerrors.New("agent exited")) + }() var ( ignorePorts = map[int]string{} @@ -281,7 +284,6 @@ func (r *RootCmd) workspaceAgent() *serpent.Command { return xerrors.Errorf("add executable to $PATH: %w", err) } - prometheusRegistry := prometheus.NewRegistry() subsystemsRaw := inv.Environ.Get(agent.EnvAgentSubsystem) subsystems := []codersdk.AgentSubsystem{} for _, s := range strings.Split(subsystemsRaw, ",") { @@ -325,46 +327,70 @@ func (r *RootCmd) workspaceAgent() *serpent.Command { logger.Info(ctx, "agent devcontainer detection not enabled") } - agnt := agent.New(agent.Options{ - Client: client, - Logger: logger, - LogDir: logDir, - ScriptDataDir: scriptDataDir, - // #nosec G115 - Safe conversion as tailnet listen port is within uint16 range (0-65535) - TailnetListenPort: uint16(tailnetListenPort), - ExchangeToken: func(ctx context.Context) (string, error) { - if exchangeToken == nil { - return client.SDK.SessionToken(), nil - } - resp, err := exchangeToken(ctx) - if err != nil { - return "", err - } - client.SetSessionToken(resp.SessionToken) - return resp.SessionToken, nil - }, - EnvironmentVariables: environmentVariables, - IgnorePorts: ignorePorts, - SSHMaxTimeout: sshMaxTimeout, - Subsystems: subsystems, - - PrometheusRegistry: prometheusRegistry, - BlockFileTransfer: blockFileTransfer, - Execer: execer, - SubAgent: subAgent, - - ExperimentalDevcontainersEnabled: experimentalDevcontainersEnabled, - }) - - promHandler := agent.PrometheusMetricsHandler(prometheusRegistry, logger) - prometheusSrvClose := ServeHandler(ctx, logger, promHandler, prometheusAddress, "prometheus") - defer prometheusSrvClose() - - debugSrvClose := ServeHandler(ctx, logger, agnt.HTTPDebug(), debugAddress, "debug") - defer debugSrvClose() - - <-ctx.Done() - return agnt.Close() + reinitEvents := agentsdk.WaitForReinitLoop(ctx, logger, client) + + var ( + lastErr error + mustExit bool + ) + for { + prometheusRegistry := prometheus.NewRegistry() + + agnt := agent.New(agent.Options{ + Client: client, + Logger: logger, + LogDir: logDir, + ScriptDataDir: scriptDataDir, + // #nosec G115 - Safe conversion as tailnet listen port is within uint16 range (0-65535) + TailnetListenPort: uint16(tailnetListenPort), + ExchangeToken: func(ctx context.Context) (string, error) { + if exchangeToken == nil { + return client.SDK.SessionToken(), nil + } + resp, err := exchangeToken(ctx) + if err != nil { + return "", err + } + client.SetSessionToken(resp.SessionToken) + return resp.SessionToken, nil + }, + EnvironmentVariables: environmentVariables, + IgnorePorts: ignorePorts, + SSHMaxTimeout: sshMaxTimeout, + Subsystems: subsystems, + + PrometheusRegistry: prometheusRegistry, + BlockFileTransfer: blockFileTransfer, + Execer: execer, + SubAgent: subAgent, + ExperimentalDevcontainersEnabled: experimentalDevcontainersEnabled, + }) + + promHandler := agent.PrometheusMetricsHandler(prometheusRegistry, logger) + prometheusSrvClose := ServeHandler(ctx, logger, promHandler, prometheusAddress, "prometheus") + + debugSrvClose := ServeHandler(ctx, logger, agnt.HTTPDebug(), debugAddress, "debug") + + select { + case <-ctx.Done(): + logger.Info(ctx, "agent shutting down", slog.Error(context.Cause(ctx))) + mustExit = true + case event := <-reinitEvents: + logger.Info(ctx, "agent received instruction to reinitialize", + slog.F("workspace_id", event.WorkspaceID), slog.F("reason", event.Reason)) + } + + lastErr = agnt.Close() + debugSrvClose() + prometheusSrvClose() + + if mustExit { + break + } + + logger.Info(ctx, "agent reinitializing") + } + return lastErr }, } diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 808090f7e3e82..157cb30383967 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -8446,6 +8446,31 @@ const docTemplate = `{ } } }, + "/workspaceagents/me/reinit": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Agents" + ], + "summary": "Get workspace agent reinitialization", + "operationId": "get-workspace-agent-reinitialization", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/agentsdk.ReinitializationEvent" + } + } + } + } + }, "/workspaceagents/me/rpc": { "get": { "security": [ @@ -10491,6 +10516,26 @@ const docTemplate = `{ } } }, + "agentsdk.ReinitializationEvent": { + "type": "object", + "properties": { + "reason": { + "$ref": "#/definitions/agentsdk.ReinitializationReason" + }, + "workspaceID": { + "type": "string" + } + } + }, + "agentsdk.ReinitializationReason": { + "type": "string", + "enum": [ + "prebuild_claimed" + ], + "x-enum-varnames": [ + "ReinitializeReasonPrebuildClaimed" + ] + }, "aisdk.Attachment": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index e60f1480a78c0..692065af3c642 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7463,6 +7463,27 @@ } } }, + "/workspaceagents/me/reinit": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Agents"], + "summary": "Get workspace agent reinitialization", + "operationId": "get-workspace-agent-reinitialization", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/agentsdk.ReinitializationEvent" + } + } + } + } + }, "/workspaceagents/me/rpc": { "get": { "security": [ @@ -9302,6 +9323,22 @@ } } }, + "agentsdk.ReinitializationEvent": { + "type": "object", + "properties": { + "reason": { + "$ref": "#/definitions/agentsdk.ReinitializationReason" + }, + "workspaceID": { + "type": "string" + } + } + }, + "agentsdk.ReinitializationReason": { + "type": "string", + "enum": ["prebuild_claimed"], + "x-enum-varnames": ["ReinitializeReasonPrebuildClaimed"] + }, "aisdk.Attachment": { "type": "object", "properties": { diff --git a/coderd/coderd.go b/coderd/coderd.go index 86db50d9559a4..b41e0070f6ecc 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -19,6 +19,8 @@ import ( "sync/atomic" "time" + "github.com/coder/coder/v2/coderd/prebuilds" + "github.com/andybalholm/brotli" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" @@ -47,7 +49,6 @@ import ( "github.com/coder/coder/v2/coderd/entitlements" "github.com/coder/coder/v2/coderd/files" "github.com/coder/coder/v2/coderd/idpsync" - "github.com/coder/coder/v2/coderd/prebuilds" "github.com/coder/coder/v2/coderd/runtimeconfig" "github.com/coder/coder/v2/coderd/webpush" @@ -1299,6 +1300,7 @@ func New(options *Options) *API { r.Get("/external-auth", api.workspaceAgentsExternalAuth) r.Get("/gitsshkey", api.agentGitSSHKey) r.Post("/log-source", api.workspaceAgentPostLogSource) + r.Get("/reinit", api.workspaceAgentReinit) }) r.Route("/{workspaceagent}", func(r chi.Router) { r.Use( diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index e843d0d748578..85f000939d75e 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -1105,6 +1105,69 @@ func (w WorkspaceAgentWaiter) MatchResources(m func([]codersdk.WorkspaceResource return w } +// WaitForAgentFn represents a boolean assertion to be made against each agent +// that a given WorkspaceAgentWaited knows about. Each WaitForAgentFn should apply +// the check to a single agent, but it should be named for plural, because `func (w WorkspaceAgentWaiter) WaitFor` +// applies the check to all agents that it is aware of. This ensures that the public API of the waiter +// reads correctly. For example: +// +// waiter := coderdtest.NewWorkspaceAgentWaiter(t, client, r.Workspace.ID) +// waiter.WaitFor(coderdtest.AgentsReady) +type WaitForAgentFn func(agent codersdk.WorkspaceAgent) bool + +// AgentsReady checks that the latest lifecycle state of an agent is "Ready". +func AgentsReady(agent codersdk.WorkspaceAgent) bool { + return agent.LifecycleState == codersdk.WorkspaceAgentLifecycleReady +} + +// AgentsNotReady checks that the latest lifecycle state of an agent is anything except "Ready". +func AgentsNotReady(agent codersdk.WorkspaceAgent) bool { + return !AgentsReady(agent) +} + +func (w WorkspaceAgentWaiter) WaitFor(criteria ...WaitForAgentFn) { + w.t.Helper() + + agentNamesMap := make(map[string]struct{}, len(w.agentNames)) + for _, name := range w.agentNames { + agentNamesMap[name] = struct{}{} + } + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + w.t.Logf("waiting for workspace agents (workspace %s)", w.workspaceID) + require.Eventually(w.t, func() bool { + var err error + workspace, err := w.client.Workspace(ctx, w.workspaceID) + if err != nil { + return false + } + if workspace.LatestBuild.Job.CompletedAt == nil { + return false + } + if workspace.LatestBuild.Job.CompletedAt.IsZero() { + return false + } + + for _, resource := range workspace.LatestBuild.Resources { + for _, agent := range resource.Agents { + if len(w.agentNames) > 0 { + if _, ok := agentNamesMap[agent.Name]; !ok { + continue + } + } + for _, criterium := range criteria { + if !criterium(agent) { + return false + } + } + } + } + return true + }, testutil.WaitLong, testutil.IntervalMedium) +} + // Wait waits for the agent(s) to connect and fails the test if they do not within testutil.WaitLong func (w WorkspaceAgentWaiter) Wait() []codersdk.WorkspaceResource { w.t.Helper() diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 732739b2f09a5..928dee0e30ea3 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -3020,6 +3020,15 @@ func (q *querier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uui return q.db.GetWorkspaceAgentsByResourceIDs(ctx, ids) } +func (q *querier) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Context, arg database.GetWorkspaceAgentsByWorkspaceAndBuildNumberParams) ([]database.WorkspaceAgent, error) { + _, err := q.GetWorkspaceByID(ctx, arg.WorkspaceID) + if err != nil { + return nil, err + } + + return q.db.GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx, arg) +} + func (q *querier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]database.WorkspaceAgent, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return nil, err diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 6dc9a32f03943..9936208ae04c1 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -2009,6 +2009,38 @@ func (s *MethodTestSuite) TestWorkspace() { agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) check.Args(agt.ID).Asserts(w, policy.ActionRead).Returns(agt) })) + s.Run("GetWorkspaceAgentsByWorkspaceAndBuildNumber", s.Subtest(func(db database.Store, check *expects) { + u := dbgen.User(s.T(), db, database.User{}) + o := dbgen.Organization(s.T(), db, database.Organization{}) + tpl := dbgen.Template(s.T(), db, database.Template{ + OrganizationID: o.ID, + CreatedBy: u.ID, + }) + tv := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{ + TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true}, + OrganizationID: o.ID, + CreatedBy: u.ID, + }) + w := dbgen.Workspace(s.T(), db, database.WorkspaceTable{ + TemplateID: tpl.ID, + OrganizationID: o.ID, + OwnerID: u.ID, + }) + j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{ + Type: database.ProvisionerJobTypeWorkspaceBuild, + }) + b := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{ + JobID: j.ID, + WorkspaceID: w.ID, + TemplateVersionID: tv.ID, + }) + res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: b.JobID}) + agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) + check.Args(database.GetWorkspaceAgentsByWorkspaceAndBuildNumberParams{ + WorkspaceID: w.ID, + BuildNumber: 1, + }).Asserts(w, policy.ActionRead).Returns([]database.WorkspaceAgent{agt}) + })) s.Run("GetWorkspaceAgentLifecycleStateByID", s.Subtest(func(db database.Store, check *expects) { u := dbgen.User(s.T(), db, database.User{}) o := dbgen.Organization(s.T(), db, database.Organization{}) diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index abadd78f07b36..fb2ea4bfd56b1 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -294,6 +294,8 @@ type TemplateVersionBuilder struct { ps pubsub.Pubsub resources []*sdkproto.Resource params []database.TemplateVersionParameter + presets []database.TemplateVersionPreset + presetParams []database.TemplateVersionPresetParameter promote bool autoCreateTemplate bool } @@ -339,6 +341,13 @@ func (t TemplateVersionBuilder) Params(ps ...database.TemplateVersionParameter) return t } +func (t TemplateVersionBuilder) Preset(preset database.TemplateVersionPreset, params ...database.TemplateVersionPresetParameter) TemplateVersionBuilder { + // nolint: revive // returns modified struct + t.presets = append(t.presets, preset) + t.presetParams = append(t.presetParams, params...) + return t +} + func (t TemplateVersionBuilder) SkipCreateTemplate() TemplateVersionBuilder { // nolint: revive // returns modified struct t.autoCreateTemplate = false @@ -378,6 +387,25 @@ func (t TemplateVersionBuilder) Do() TemplateVersionResponse { require.NoError(t.t, err) } + for _, preset := range t.presets { + dbgen.Preset(t.t, t.db, database.InsertPresetParams{ + ID: preset.ID, + TemplateVersionID: version.ID, + Name: preset.Name, + CreatedAt: version.CreatedAt, + DesiredInstances: preset.DesiredInstances, + InvalidateAfterSecs: preset.InvalidateAfterSecs, + }) + } + + for _, presetParam := range t.presetParams { + dbgen.PresetParameter(t.t, t.db, database.InsertPresetParametersParams{ + TemplateVersionPresetID: presetParam.TemplateVersionPresetID, + Names: []string{presetParam.Name}, + Values: []string{presetParam.Value}, + }) + } + payload, err := json.Marshal(provisionerdserver.TemplateVersionImportJob{ TemplateVersionID: t.seed.ID, }) diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index b16faba6a8891..fa1f297b908ba 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -1224,6 +1224,7 @@ func TelemetryItem(t testing.TB, db database.Store, seed database.TelemetryItem) func Preset(t testing.TB, db database.Store, seed database.InsertPresetParams) database.TemplateVersionPreset { preset, err := db.InsertPreset(genCtx, database.InsertPresetParams{ + ID: takeFirst(seed.ID, uuid.New()), TemplateVersionID: takeFirst(seed.TemplateVersionID, uuid.New()), Name: takeFirst(seed.Name, testutil.GetRandomName(t)), CreatedAt: takeFirst(seed.CreatedAt, dbtime.Now()), diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 2760c0c929c58..dfa28097ab60c 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -7654,6 +7654,30 @@ func (q *FakeQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, resou return q.getWorkspaceAgentsByResourceIDsNoLock(ctx, resourceIDs) } +func (q *FakeQuerier) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Context, arg database.GetWorkspaceAgentsByWorkspaceAndBuildNumberParams) ([]database.WorkspaceAgent, error) { + err := validateDatabaseType(arg) + if err != nil { + return nil, err + } + + build, err := q.GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx, database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams(arg)) + if err != nil { + return nil, err + } + + resources, err := q.getWorkspaceResourcesByJobIDNoLock(ctx, build.JobID) + if err != nil { + return nil, err + } + + var resourceIDs []uuid.UUID + for _, resource := range resources { + resourceIDs = append(resourceIDs, resource.ID) + } + + return q.GetWorkspaceAgentsByResourceIDs(ctx, resourceIDs) +} + func (q *FakeQuerier) GetWorkspaceAgentsCreatedAfter(_ context.Context, after time.Time) ([]database.WorkspaceAgent, error) { q.mutex.RLock() defer q.mutex.RUnlock() diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 128e741da1d76..a5a22aad1a0bf 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -1754,6 +1754,13 @@ func (m queryMetricsStore) GetWorkspaceAgentsByResourceIDs(ctx context.Context, return agents, err } +func (m queryMetricsStore) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Context, arg database.GetWorkspaceAgentsByWorkspaceAndBuildNumberParams) ([]database.WorkspaceAgent, error) { + start := time.Now() + r0, r1 := m.s.GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx, arg) + m.queryLatencies.WithLabelValues("GetWorkspaceAgentsByWorkspaceAndBuildNumber").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]database.WorkspaceAgent, error) { start := time.Now() agents, err := m.s.GetWorkspaceAgentsCreatedAfter(ctx, createdAt) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 17b263dfb2e07..0d66dcec11848 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -3678,6 +3678,21 @@ func (mr *MockStoreMockRecorder) GetWorkspaceAgentsByResourceIDs(ctx, ids any) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentsByResourceIDs", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentsByResourceIDs), ctx, ids) } +// GetWorkspaceAgentsByWorkspaceAndBuildNumber mocks base method. +func (m *MockStore) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Context, arg database.GetWorkspaceAgentsByWorkspaceAndBuildNumberParams) ([]database.WorkspaceAgent, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetWorkspaceAgentsByWorkspaceAndBuildNumber", ctx, arg) + ret0, _ := ret[0].([]database.WorkspaceAgent) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetWorkspaceAgentsByWorkspaceAndBuildNumber indicates an expected call of GetWorkspaceAgentsByWorkspaceAndBuildNumber. +func (mr *MockStoreMockRecorder) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentsByWorkspaceAndBuildNumber", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentsByWorkspaceAndBuildNumber), ctx, arg) +} + // GetWorkspaceAgentsCreatedAfter mocks base method. func (m *MockStore) GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]database.WorkspaceAgent, error) { m.ctrl.T.Helper() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index d0f74ee609724..81b8d58758ada 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -400,6 +400,7 @@ type sqlcQuerier interface { GetWorkspaceAgentUsageStats(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentUsageStatsRow, error) GetWorkspaceAgentUsageStatsAndLabels(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentUsageStatsAndLabelsRow, error) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgent, error) + GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Context, arg GetWorkspaceAgentsByWorkspaceAndBuildNumberParams) ([]WorkspaceAgent, error) GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceAgent, error) GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgent, error) GetWorkspaceAppByAgentIDAndSlug(ctx context.Context, arg GetWorkspaceAppByAgentIDAndSlugParams) (WorkspaceApp, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index e2e38c36fe10a..bd1d5cddd43ed 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -6678,6 +6678,7 @@ func (q *sqlQuerier) GetPresetsByTemplateVersionID(ctx context.Context, template const insertPreset = `-- name: InsertPreset :one INSERT INTO template_version_presets ( + id, template_version_id, name, created_at, @@ -6689,11 +6690,13 @@ VALUES ( $2, $3, $4, - $5 + $5, + $6 ) RETURNING id, template_version_id, name, created_at, desired_instances, invalidate_after_secs ` type InsertPresetParams 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"` @@ -6703,6 +6706,7 @@ type InsertPresetParams struct { func (q *sqlQuerier) InsertPreset(ctx context.Context, arg InsertPresetParams) (TemplateVersionPreset, error) { row := q.db.QueryRowContext(ctx, insertPreset, + arg.ID, arg.TemplateVersionID, arg.Name, arg.CreatedAt, @@ -14416,6 +14420,81 @@ func (q *sqlQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids [] return items, nil } +const getWorkspaceAgentsByWorkspaceAndBuildNumber = `-- name: GetWorkspaceAgentsByWorkspaceAndBuildNumber :many +SELECT + 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_agents.parent_id +FROM + workspace_agents +JOIN + workspace_resources ON workspace_agents.resource_id = workspace_resources.id +JOIN + workspace_builds ON workspace_resources.job_id = workspace_builds.job_id +WHERE + workspace_builds.workspace_id = $1 :: uuid AND + workspace_builds.build_number = $2 :: int +` + +type GetWorkspaceAgentsByWorkspaceAndBuildNumberParams struct { + WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` + BuildNumber int32 `db:"build_number" json:"build_number"` +} + +func (q *sqlQuerier) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Context, arg GetWorkspaceAgentsByWorkspaceAndBuildNumberParams) ([]WorkspaceAgent, error) { + rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsByWorkspaceAndBuildNumber, arg.WorkspaceID, arg.BuildNumber) + if err != nil { + return nil, err + } + defer rows.Close() + var items []WorkspaceAgent + for rows.Next() { + var i WorkspaceAgent + if err := rows.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.FirstConnectedAt, + &i.LastConnectedAt, + &i.DisconnectedAt, + &i.ResourceID, + &i.AuthToken, + &i.AuthInstanceID, + &i.Architecture, + &i.EnvironmentVariables, + &i.OperatingSystem, + &i.InstanceMetadata, + &i.ResourceMetadata, + &i.Directory, + &i.Version, + &i.LastConnectedReplicaID, + &i.ConnectionTimeoutSeconds, + &i.TroubleshootingURL, + &i.MOTDFile, + &i.LifecycleState, + &i.ExpandedDirectory, + &i.LogsLength, + &i.LogsOverflowed, + &i.StartedAt, + &i.ReadyAt, + pq.Array(&i.Subsystems), + pq.Array(&i.DisplayApps), + &i.APIVersion, + &i.DisplayOrder, + &i.ParentID, + ); 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 getWorkspaceAgentsCreatedAfter = `-- name: GetWorkspaceAgentsCreatedAfter :many SELECT id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id FROM workspace_agents WHERE created_at > $1 ` diff --git a/coderd/database/queries/presets.sql b/coderd/database/queries/presets.sql index 15bcea0c28fb5..6d5646a285b4a 100644 --- a/coderd/database/queries/presets.sql +++ b/coderd/database/queries/presets.sql @@ -1,5 +1,6 @@ -- name: InsertPreset :one INSERT INTO template_version_presets ( + id, template_version_id, name, created_at, @@ -7,6 +8,7 @@ INSERT INTO template_version_presets ( invalidate_after_secs ) VALUES ( + @id, @template_version_id, @name, @created_at, diff --git a/coderd/database/queries/workspaceagents.sql b/coderd/database/queries/workspaceagents.sql index 2e186461931b2..cb4fa3f8cf968 100644 --- a/coderd/database/queries/workspaceagents.sql +++ b/coderd/database/queries/workspaceagents.sql @@ -253,6 +253,19 @@ WHERE wb.workspace_id = @workspace_id :: uuid ); +-- name: GetWorkspaceAgentsByWorkspaceAndBuildNumber :many +SELECT + workspace_agents.* +FROM + workspace_agents +JOIN + workspace_resources ON workspace_agents.resource_id = workspace_resources.id +JOIN + workspace_builds ON workspace_resources.job_id = workspace_builds.job_id +WHERE + workspace_builds.workspace_id = @workspace_id :: uuid AND + workspace_builds.build_number = @build_number :: int; + -- name: GetWorkspaceAgentAndLatestBuildByAuthToken :one SELECT sqlc.embed(workspaces), diff --git a/coderd/prebuilds/claim.go b/coderd/prebuilds/claim.go new file mode 100644 index 0000000000000..b5155b8f2a568 --- /dev/null +++ b/coderd/prebuilds/claim.go @@ -0,0 +1,82 @@ +package prebuilds + +import ( + "context" + "sync" + + "github.com/google/uuid" + "golang.org/x/xerrors" + + "cdr.dev/slog" + "github.com/coder/coder/v2/coderd/database/pubsub" + "github.com/coder/coder/v2/codersdk/agentsdk" +) + +func NewPubsubWorkspaceClaimPublisher(ps pubsub.Pubsub) *PubsubWorkspaceClaimPublisher { + return &PubsubWorkspaceClaimPublisher{ps: ps} +} + +type PubsubWorkspaceClaimPublisher struct { + ps pubsub.Pubsub +} + +func (p PubsubWorkspaceClaimPublisher) PublishWorkspaceClaim(claim agentsdk.ReinitializationEvent) error { + channel := agentsdk.PrebuildClaimedChannel(claim.WorkspaceID) + if err := p.ps.Publish(channel, []byte(claim.Reason)); err != nil { + return xerrors.Errorf("failed to trigger prebuilt workspace agent reinitialization: %w", err) + } + return nil +} + +func NewPubsubWorkspaceClaimListener(ps pubsub.Pubsub, logger slog.Logger) *PubsubWorkspaceClaimListener { + return &PubsubWorkspaceClaimListener{ps: ps, logger: logger} +} + +type PubsubWorkspaceClaimListener struct { + logger slog.Logger + ps pubsub.Pubsub +} + +// ListenForWorkspaceClaims subscribes to a pubsub channel and sends any received events on the chan that it returns. +// pubsub.Pubsub does not communicate when its last callback has been called after it has been closed. As such the chan +// returned by this method is never closed. Call the returned cancel() function to close the subscription when it is no longer needed. +// cancel() will be called if ctx expires or is canceled. +func (p PubsubWorkspaceClaimListener) ListenForWorkspaceClaims(ctx context.Context, workspaceID uuid.UUID, reinitEvents chan<- agentsdk.ReinitializationEvent) (func(), error) { + select { + case <-ctx.Done(): + return func() {}, ctx.Err() + default: + } + + cancelSub, err := p.ps.Subscribe(agentsdk.PrebuildClaimedChannel(workspaceID), func(inner context.Context, reason []byte) { + claim := agentsdk.ReinitializationEvent{ + WorkspaceID: workspaceID, + Reason: agentsdk.ReinitializationReason(reason), + } + + select { + case <-ctx.Done(): + return + case <-inner.Done(): + return + case reinitEvents <- claim: + } + }) + if err != nil { + return func() {}, xerrors.Errorf("failed to subscribe to prebuild claimed channel: %w", err) + } + + var once sync.Once + cancel := func() { + once.Do(func() { + cancelSub() + }) + } + + go func() { + <-ctx.Done() + cancel() + }() + + return cancel, nil +} diff --git a/coderd/prebuilds/claim_test.go b/coderd/prebuilds/claim_test.go new file mode 100644 index 0000000000000..670bb64eec756 --- /dev/null +++ b/coderd/prebuilds/claim_test.go @@ -0,0 +1,141 @@ +package prebuilds_test + +import ( + "context" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "golang.org/x/xerrors" + + "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/coderd/database/pubsub" + "github.com/coder/coder/v2/coderd/prebuilds" + "github.com/coder/coder/v2/codersdk/agentsdk" + "github.com/coder/coder/v2/testutil" +) + +func TestPubsubWorkspaceClaimPublisher(t *testing.T) { + t.Parallel() + t.Run("published claim is received by a listener for the same workspace", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitShort) + logger := testutil.Logger(t) + ps := pubsub.NewInMemory() + workspaceID := uuid.New() + reinitEvents := make(chan agentsdk.ReinitializationEvent, 1) + publisher := prebuilds.NewPubsubWorkspaceClaimPublisher(ps) + listener := prebuilds.NewPubsubWorkspaceClaimListener(ps, logger) + + cancel, err := listener.ListenForWorkspaceClaims(ctx, workspaceID, reinitEvents) + require.NoError(t, err) + defer cancel() + + claim := agentsdk.ReinitializationEvent{ + WorkspaceID: workspaceID, + Reason: agentsdk.ReinitializeReasonPrebuildClaimed, + } + err = publisher.PublishWorkspaceClaim(claim) + require.NoError(t, err) + + gotEvent := testutil.RequireReceive(ctx, t, reinitEvents) + require.Equal(t, workspaceID, gotEvent.WorkspaceID) + require.Equal(t, claim.Reason, gotEvent.Reason) + }) + + t.Run("fail to publish claim", func(t *testing.T) { + t.Parallel() + + ps := &brokenPubsub{} + + publisher := prebuilds.NewPubsubWorkspaceClaimPublisher(ps) + claim := agentsdk.ReinitializationEvent{ + WorkspaceID: uuid.New(), + Reason: agentsdk.ReinitializeReasonPrebuildClaimed, + } + + err := publisher.PublishWorkspaceClaim(claim) + require.ErrorContains(t, err, "failed to trigger prebuilt workspace agent reinitialization") + }) +} + +func TestPubsubWorkspaceClaimListener(t *testing.T) { + t.Parallel() + t.Run("finds claim events for its workspace", func(t *testing.T) { + t.Parallel() + + ps := pubsub.NewInMemory() + listener := prebuilds.NewPubsubWorkspaceClaimListener(ps, slogtest.Make(t, nil)) + + claims := make(chan agentsdk.ReinitializationEvent, 1) // Buffer to avoid messing with goroutines in the rest of the test + + workspaceID := uuid.New() + cancelFunc, err := listener.ListenForWorkspaceClaims(context.Background(), workspaceID, claims) + require.NoError(t, err) + defer cancelFunc() + + // Publish a claim + channel := agentsdk.PrebuildClaimedChannel(workspaceID) + reason := agentsdk.ReinitializeReasonPrebuildClaimed + err = ps.Publish(channel, []byte(reason)) + require.NoError(t, err) + + // Verify we receive the claim + ctx := testutil.Context(t, testutil.WaitShort) + claim := testutil.RequireReceive(ctx, t, claims) + require.Equal(t, workspaceID, claim.WorkspaceID) + require.Equal(t, reason, claim.Reason) + }) + + t.Run("ignores claim events for other workspaces", func(t *testing.T) { + t.Parallel() + + ps := pubsub.NewInMemory() + listener := prebuilds.NewPubsubWorkspaceClaimListener(ps, slogtest.Make(t, nil)) + + claims := make(chan agentsdk.ReinitializationEvent) + workspaceID := uuid.New() + otherWorkspaceID := uuid.New() + cancelFunc, err := listener.ListenForWorkspaceClaims(context.Background(), workspaceID, claims) + require.NoError(t, err) + defer cancelFunc() + + // Publish a claim for a different workspace + channel := agentsdk.PrebuildClaimedChannel(otherWorkspaceID) + err = ps.Publish(channel, []byte(agentsdk.ReinitializeReasonPrebuildClaimed)) + require.NoError(t, err) + + // Verify we don't receive the claim + select { + case <-claims: + t.Fatal("received claim for wrong workspace") + case <-time.After(100 * time.Millisecond): + // Expected - no claim received + } + }) + + t.Run("communicates the error if it can't subscribe", func(t *testing.T) { + t.Parallel() + + claims := make(chan agentsdk.ReinitializationEvent) + ps := &brokenPubsub{} + listener := prebuilds.NewPubsubWorkspaceClaimListener(ps, slogtest.Make(t, nil)) + + _, err := listener.ListenForWorkspaceClaims(context.Background(), uuid.New(), claims) + require.ErrorContains(t, err, "failed to subscribe to prebuild claimed channel") + }) +} + +type brokenPubsub struct { + pubsub.Pubsub +} + +func (brokenPubsub) Subscribe(_ string, _ pubsub.Listener) (func(), error) { + return nil, xerrors.New("broken") +} + +func (brokenPubsub) Publish(_ string, _ []byte) error { + return xerrors.New("broken") +} diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 1206d81025d7a..f8a33d3a55188 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -40,12 +40,14 @@ import ( "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/prebuilds" "github.com/coder/coder/v2/coderd/promoauth" "github.com/coder/coder/v2/coderd/schedule" "github.com/coder/coder/v2/coderd/telemetry" "github.com/coder/coder/v2/coderd/tracing" "github.com/coder/coder/v2/coderd/wspubsub" "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/coder/v2/provisioner" "github.com/coder/coder/v2/provisionerd/proto" "github.com/coder/coder/v2/provisionersdk" @@ -647,6 +649,30 @@ func (s *server) acquireProtoJob(ctx context.Context, job database.ProvisionerJo } } + runningAgentAuthTokens := []*sdkproto.RunningAgentAuthToken{} + if input.PrebuiltWorkspaceBuildStage == sdkproto.PrebuiltWorkspaceBuildStage_CLAIM { + // runningAgentAuthTokens are *only* used for prebuilds. We fetch them when we want to rebuild a prebuilt workspace + // but not generate new agent tokens. The provisionerdserver will push them down to + // the provisioner (and ultimately to the `coder_agent` resource in the Terraform provider) where they will be + // reused. Context: the agent token is often used in immutable attributes of workspace resource (e.g. VM/container) + // to initialize the agent, so if that value changes it will necessitate a replacement of that resource, thus + // obviating the whole point of the prebuild. + agents, err := s.Database.GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx, database.GetWorkspaceAgentsByWorkspaceAndBuildNumberParams{ + WorkspaceID: workspace.ID, + BuildNumber: 1, + }) + if err != nil { + s.Logger.Error(ctx, "failed to retrieve running agents of claimed prebuilt workspace", + slog.F("workspace_id", workspace.ID), slog.Error(err)) + } + for _, agent := range agents { + runningAgentAuthTokens = append(runningAgentAuthTokens, &sdkproto.RunningAgentAuthToken{ + AgentId: agent.ID.String(), + Token: agent.AuthToken.String(), + }) + } + } + protoJob.Type = &proto.AcquiredJob_WorkspaceBuild_{ WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{ WorkspaceBuildId: workspaceBuild.ID.String(), @@ -676,6 +702,7 @@ func (s *server) acquireProtoJob(ctx context.Context, job database.ProvisionerJo WorkspaceBuildId: workspaceBuild.ID.String(), WorkspaceOwnerLoginType: string(owner.LoginType), WorkspaceOwnerRbacRoles: ownerRbacRoles, + RunningAgentAuthTokens: runningAgentAuthTokens, PrebuiltWorkspaceBuildStage: input.PrebuiltWorkspaceBuildStage, }, LogLevel: input.LogLevel, @@ -1812,6 +1839,19 @@ func (s *server) CompleteJob(ctx context.Context, completed *proto.CompletedJob) if err != nil { return nil, xerrors.Errorf("update workspace: %w", err) } + + if input.PrebuiltWorkspaceBuildStage == sdkproto.PrebuiltWorkspaceBuildStage_CLAIM { + s.Logger.Info(ctx, "workspace prebuild successfully claimed by user", + slog.F("workspace_id", workspace.ID)) + + err = prebuilds.NewPubsubWorkspaceClaimPublisher(s.Pubsub).PublishWorkspaceClaim(agentsdk.ReinitializationEvent{ + WorkspaceID: workspace.ID, + Reason: agentsdk.ReinitializeReasonPrebuildClaimed, + }) + if err != nil { + s.Logger.Error(ctx, "failed to publish workspace claim event", slog.Error(err)) + } + } case *proto.CompletedJob_TemplateDryRun_: for _, resource := range jobType.TemplateDryRun.Resources { s.Logger.Info(ctx, "inserting template dry-run job resource", @@ -1955,6 +1995,7 @@ func InsertWorkspacePresetAndParameters(ctx context.Context, db database.Store, } } dbPreset, err := tx.InsertPreset(ctx, database.InsertPresetParams{ + ID: uuid.New(), TemplateVersionID: templateVersionID, Name: protoPreset.Name, CreatedAt: t, diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index 9cfb3449ea522..876cc5fc2d755 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -26,7 +26,10 @@ import ( "github.com/coder/quartz" "github.com/coder/serpent" + "github.com/coder/coder/v2/coderd/prebuilds" + "github.com/coder/coder/v2/coderd/provisionerdserver" "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/coder/v2/buildinfo" "github.com/coder/coder/v2/coderd/audit" @@ -39,7 +42,6 @@ import ( "github.com/coder/coder/v2/coderd/externalauth" "github.com/coder/coder/v2/coderd/notifications" "github.com/coder/coder/v2/coderd/notifications/notificationstest" - "github.com/coder/coder/v2/coderd/provisionerdserver" "github.com/coder/coder/v2/coderd/schedule" "github.com/coder/coder/v2/coderd/schedule/cron" "github.com/coder/coder/v2/coderd/telemetry" @@ -167,8 +169,12 @@ func TestAcquireJob(t *testing.T) { _, err = tc.acquire(ctx, srv) require.ErrorContains(t, err, "sql: no rows in result set") }) - for _, prebuiltWorkspace := range []bool{false, true} { - prebuiltWorkspace := prebuiltWorkspace + for _, prebuiltWorkspaceBuildStage := range []sdkproto.PrebuiltWorkspaceBuildStage{ + sdkproto.PrebuiltWorkspaceBuildStage_NONE, + sdkproto.PrebuiltWorkspaceBuildStage_CREATE, + sdkproto.PrebuiltWorkspaceBuildStage_CLAIM, + } { + prebuiltWorkspaceBuildStage := prebuiltWorkspaceBuildStage t.Run(tc.name+"_WorkspaceBuildJob", func(t *testing.T) { t.Parallel() // Set the max session token lifetime so we can assert we @@ -212,7 +218,7 @@ func TestAcquireJob(t *testing.T) { Roles: []string{rbac.RoleOrgAuditor()}, }) - // Add extra erronous roles + // Add extra erroneous roles secondOrg := dbgen.Organization(t, db, database.Organization{}) dbgen.OrganizationMember(t, db, database.OrganizationMember{ UserID: user.ID, @@ -287,36 +293,74 @@ func TestAcquireJob(t *testing.T) { Required: true, Sensitive: false, }) - workspace := dbgen.Workspace(t, db, database.WorkspaceTable{ + workspace := database.WorkspaceTable{ TemplateID: template.ID, OwnerID: user.ID, OrganizationID: pd.OrganizationID, - }) - build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ + } + workspace = dbgen.Workspace(t, db, workspace) + build := database.WorkspaceBuild{ WorkspaceID: workspace.ID, BuildNumber: 1, JobID: uuid.New(), TemplateVersionID: version.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator, - }) - var buildState sdkproto.PrebuiltWorkspaceBuildStage - if prebuiltWorkspace { - buildState = sdkproto.PrebuiltWorkspaceBuildStage_CREATE } - _ = dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{ - ID: build.ID, + build = dbgen.WorkspaceBuild(t, db, build) + input := provisionerdserver.WorkspaceProvisionJob{ + WorkspaceBuildID: build.ID, + } + dbJob := database.ProvisionerJob{ + ID: build.JobID, OrganizationID: pd.OrganizationID, InitiatorID: user.ID, Provisioner: database.ProvisionerTypeEcho, StorageMethod: database.ProvisionerStorageMethodFile, FileID: file.ID, Type: database.ProvisionerJobTypeWorkspaceBuild, - Input: must(json.Marshal(provisionerdserver.WorkspaceProvisionJob{ + Input: must(json.Marshal(input)), + } + dbJob = dbgen.ProvisionerJob(t, db, ps, dbJob) + + var agent database.WorkspaceAgent + if prebuiltWorkspaceBuildStage == sdkproto.PrebuiltWorkspaceBuildStage_CLAIM { + resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ + JobID: dbJob.ID, + }) + agent = dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ + ResourceID: resource.ID, + AuthToken: uuid.New(), + }) + // At this point we have an unclaimed workspace and build, now we need to setup the claim + // build + build = database.WorkspaceBuild{ + WorkspaceID: workspace.ID, + BuildNumber: 2, + JobID: uuid.New(), + TemplateVersionID: version.ID, + Transition: database.WorkspaceTransitionStart, + Reason: database.BuildReasonInitiator, + InitiatorID: user.ID, + } + build = dbgen.WorkspaceBuild(t, db, build) + + input = provisionerdserver.WorkspaceProvisionJob{ WorkspaceBuildID: build.ID, - PrebuiltWorkspaceBuildStage: buildState, - })), - }) + PrebuiltWorkspaceBuildStage: prebuiltWorkspaceBuildStage, + } + dbJob = database.ProvisionerJob{ + ID: build.JobID, + OrganizationID: pd.OrganizationID, + InitiatorID: user.ID, + Provisioner: database.ProvisionerTypeEcho, + StorageMethod: database.ProvisionerStorageMethodFile, + FileID: file.ID, + Type: database.ProvisionerJobTypeWorkspaceBuild, + Input: must(json.Marshal(input)), + } + dbJob = dbgen.ProvisionerJob(t, db, ps, dbJob) + } startPublished := make(chan struct{}) var closed bool @@ -350,6 +394,19 @@ func TestAcquireJob(t *testing.T) { <-startPublished + if prebuiltWorkspaceBuildStage == sdkproto.PrebuiltWorkspaceBuildStage_CLAIM { + for { + // In the case of a prebuild claim, there is a second build, which is the + // one that we're interested in. + job, err = tc.acquire(ctx, srv) + require.NoError(t, err) + if _, ok := job.Type.(*proto.AcquiredJob_WorkspaceBuild_); ok { + break + } + } + <-startPublished + } + got, err := json.Marshal(job.Type) require.NoError(t, err) @@ -384,8 +441,14 @@ func TestAcquireJob(t *testing.T) { WorkspaceOwnerLoginType: string(user.LoginType), WorkspaceOwnerRbacRoles: []*sdkproto.Role{{Name: rbac.RoleOrgMember(), OrgId: pd.OrganizationID.String()}, {Name: "member", OrgId: ""}, {Name: rbac.RoleOrgAuditor(), OrgId: pd.OrganizationID.String()}}, } - if prebuiltWorkspace { - wantedMetadata.PrebuiltWorkspaceBuildStage = sdkproto.PrebuiltWorkspaceBuildStage_CREATE + if prebuiltWorkspaceBuildStage == sdkproto.PrebuiltWorkspaceBuildStage_CLAIM { + // For claimed prebuilds, we expect the prebuild state to be set to CLAIM + // and we expect tokens from the first build to be set for reuse + wantedMetadata.PrebuiltWorkspaceBuildStage = prebuiltWorkspaceBuildStage + wantedMetadata.RunningAgentAuthTokens = append(wantedMetadata.RunningAgentAuthTokens, &sdkproto.RunningAgentAuthToken{ + AgentId: agent.ID.String(), + Token: agent.AuthToken.String(), + }) } slices.SortFunc(wantedMetadata.WorkspaceOwnerRbacRoles, func(a, b *sdkproto.Role) int { @@ -1750,6 +1813,110 @@ func TestCompleteJob(t *testing.T) { }) } }) + + t.Run("ReinitializePrebuiltAgents", func(t *testing.T) { + t.Parallel() + type testcase struct { + name string + shouldReinitializeAgent bool + } + + for _, tc := range []testcase{ + // Whether or not there are presets and those presets define prebuilds, etc + // are all irrelevant at this level. Those factors are useful earlier in the process. + // Everything relevant to this test is determined by the value of `PrebuildClaimedByUser` + // on the provisioner job. As such, there are only two significant test cases: + { + name: "claimed prebuild", + shouldReinitializeAgent: true, + }, + { + name: "not a claimed prebuild", + shouldReinitializeAgent: false, + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // GIVEN an enqueued provisioner job and its dependencies: + + srv, db, ps, pd := setup(t, false, &overrides{}) + + buildID := uuid.New() + jobInput := provisionerdserver.WorkspaceProvisionJob{ + WorkspaceBuildID: buildID, + } + if tc.shouldReinitializeAgent { // This is the key lever in the test + // GIVEN the enqueued provisioner job is for a workspace being claimed by a user: + jobInput.PrebuiltWorkspaceBuildStage = sdkproto.PrebuiltWorkspaceBuildStage_CLAIM + } + input, err := json.Marshal(jobInput) + require.NoError(t, err) + + ctx := testutil.Context(t, testutil.WaitShort) + job, err := db.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{ + Input: input, + Provisioner: database.ProvisionerTypeEcho, + StorageMethod: database.ProvisionerStorageMethodFile, + Type: database.ProvisionerJobTypeWorkspaceBuild, + }) + require.NoError(t, err) + + tpl := dbgen.Template(t, db, database.Template{ + OrganizationID: pd.OrganizationID, + }) + tv := dbgen.TemplateVersion(t, db, database.TemplateVersion{ + TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true}, + JobID: job.ID, + }) + workspace := dbgen.Workspace(t, db, database.WorkspaceTable{ + TemplateID: tpl.ID, + }) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ + ID: buildID, + JobID: job.ID, + WorkspaceID: workspace.ID, + TemplateVersionID: tv.ID, + }) + _, err = db.AcquireProvisionerJob(ctx, database.AcquireProvisionerJobParams{ + WorkerID: uuid.NullUUID{ + UUID: pd.ID, + Valid: true, + }, + Types: []database.ProvisionerType{database.ProvisionerTypeEcho}, + }) + require.NoError(t, err) + + // GIVEN something is listening to process workspace reinitialization: + reinitChan := make(chan agentsdk.ReinitializationEvent, 1) // Buffered to simplify test structure + cancel, err := prebuilds.NewPubsubWorkspaceClaimListener(ps, testutil.Logger(t)).ListenForWorkspaceClaims(ctx, workspace.ID, reinitChan) + require.NoError(t, err) + defer cancel() + + // WHEN the job is completed + completedJob := proto.CompletedJob{ + JobId: job.ID.String(), + Type: &proto.CompletedJob_WorkspaceBuild_{ + WorkspaceBuild: &proto.CompletedJob_WorkspaceBuild{}, + }, + } + _, err = srv.CompleteJob(ctx, &completedJob) + require.NoError(t, err) + + if tc.shouldReinitializeAgent { + event := testutil.RequireReceive(ctx, t, reinitChan) + require.Equal(t, workspace.ID, event.WorkspaceID) + } else { + select { + case <-reinitChan: + t.Fatal("unexpected reinitialization event published") + default: + // OK + } + } + }) + } + }) } func TestInsertWorkspacePresetsAndParameters(t *testing.T) { diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 050537705d107..5af9fc009b5aa 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -35,6 +35,7 @@ import ( "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/coderd/httpmw/loggermw" "github.com/coder/coder/v2/coderd/jwtutils" + "github.com/coder/coder/v2/coderd/prebuilds" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" "github.com/coder/coder/v2/coderd/telemetry" @@ -1183,6 +1184,60 @@ func (api *API) workspaceAgentPostLogSource(rw http.ResponseWriter, r *http.Requ httpapi.Write(ctx, rw, http.StatusCreated, apiSource) } +// @Summary Get workspace agent reinitialization +// @ID get-workspace-agent-reinitialization +// @Security CoderSessionToken +// @Produce json +// @Tags Agents +// @Success 200 {object} agentsdk.ReinitializationEvent +// @Router /workspaceagents/me/reinit [get] +func (api *API) workspaceAgentReinit(rw http.ResponseWriter, r *http.Request) { + // Allow us to interrupt watch via cancel. + ctx, cancel := context.WithCancel(r.Context()) + defer cancel() + r = r.WithContext(ctx) // Rewire context for SSE cancellation. + + workspaceAgent := httpmw.WorkspaceAgent(r) + log := api.Logger.Named("workspace_agent_reinit_watcher").With( + slog.F("workspace_agent_id", workspaceAgent.ID), + ) + + workspace, err := api.Database.GetWorkspaceByAgentID(ctx, workspaceAgent.ID) + if err != nil { + log.Error(ctx, "failed to retrieve workspace from agent token", slog.Error(err)) + httpapi.InternalServerError(rw, xerrors.New("failed to determine workspace from agent token")) + } + + log.Info(ctx, "agent waiting for reinit instruction") + + reinitEvents := make(chan agentsdk.ReinitializationEvent) + cancel, err = prebuilds.NewPubsubWorkspaceClaimListener(api.Pubsub, log).ListenForWorkspaceClaims(ctx, workspace.ID, reinitEvents) + if err != nil { + log.Error(ctx, "subscribe to prebuild claimed channel", slog.Error(err)) + httpapi.InternalServerError(rw, xerrors.New("failed to subscribe to prebuild claimed channel")) + return + } + defer cancel() + + transmitter := agentsdk.NewSSEAgentReinitTransmitter(log, rw, r) + + err = transmitter.Transmit(ctx, reinitEvents) + switch { + case errors.Is(err, agentsdk.ErrTransmissionSourceClosed): + log.Info(ctx, "agent reinitialization subscription closed", slog.F("workspace_agent_id", workspaceAgent.ID)) + case errors.Is(err, agentsdk.ErrTransmissionTargetClosed): + log.Info(ctx, "agent connection closed", slog.F("workspace_agent_id", workspaceAgent.ID)) + case errors.Is(err, context.Canceled): + log.Info(ctx, "agent reinitialization", slog.Error(err)) + case err != nil: + log.Error(ctx, "failed to stream agent reinit events", slog.Error(err)) + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error streaming agent reinitialization events.", + Detail: err.Error(), + }) + } +} + // convertProvisionedApps converts applications that are in the middle of provisioning process. // It means that they may not have an agent or workspace assigned (dry-run job). func convertProvisionedApps(dbApps []database.WorkspaceApp) []codersdk.WorkspaceApp { diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 6b757a52ec06d..10403f1ac00ae 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -11,6 +11,7 @@ import ( "runtime" "strconv" "strings" + "sync" "sync/atomic" "testing" "time" @@ -44,10 +45,12 @@ import ( "github.com/coder/coder/v2/coderd/database/dbfake" "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" "github.com/coder/coder/v2/coderd/jwtutils" + "github.com/coder/coder/v2/coderd/prebuilds" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/telemetry" "github.com/coder/coder/v2/coderd/util/ptr" @@ -2641,3 +2644,70 @@ func TestAgentConnectionInfo(t *testing.T) { require.True(t, info.DisableDirectConnections) require.True(t, info.DERPForceWebSockets) } + +func TestReinit(t *testing.T) { + t.Parallel() + + db, ps := dbtestutil.NewDB(t) + pubsubSpy := pubsubReinitSpy{ + Pubsub: ps, + subscribed: make(chan string), + } + client := coderdtest.New(t, &coderdtest.Options{ + Database: db, + Pubsub: &pubsubSpy, + }) + user := coderdtest.CreateFirstUser(t, client) + + r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OrganizationID: user.OrganizationID, + OwnerID: user.UserID, + }).WithAgent().Do() + + pubsubSpy.Mutex.Lock() + pubsubSpy.expectedEvent = agentsdk.PrebuildClaimedChannel(r.Workspace.ID) + pubsubSpy.Mutex.Unlock() + + agentCtx := testutil.Context(t, testutil.WaitShort) + agentClient := agentsdk.New(client.URL) + agentClient.SetSessionToken(r.AgentToken) + + agentReinitializedCh := make(chan *agentsdk.ReinitializationEvent) + go func() { + reinitEvent, err := agentClient.WaitForReinit(agentCtx) + assert.NoError(t, err) + agentReinitializedCh <- reinitEvent + }() + + // We need to subscribe before we publish, lest we miss the event + ctx := testutil.Context(t, testutil.WaitShort) + testutil.TryReceive(ctx, t, pubsubSpy.subscribed) // Wait for the appropriate subscription + + // Now that we're subscribed, publish the event + err := prebuilds.NewPubsubWorkspaceClaimPublisher(ps).PublishWorkspaceClaim(agentsdk.ReinitializationEvent{ + WorkspaceID: r.Workspace.ID, + Reason: agentsdk.ReinitializeReasonPrebuildClaimed, + }) + require.NoError(t, err) + + ctx = testutil.Context(t, testutil.WaitShort) + reinitEvent := testutil.TryReceive(ctx, t, agentReinitializedCh) + require.NotNil(t, reinitEvent) + require.Equal(t, r.Workspace.ID, reinitEvent.WorkspaceID) +} + +type pubsubReinitSpy struct { + pubsub.Pubsub + sync.Mutex + subscribed chan string + expectedEvent string +} + +func (p *pubsubReinitSpy) Subscribe(event string, listener pubsub.Listener) (cancel func(), err error) { + p.Lock() + if p.expectedEvent != "" && event == p.expectedEvent { + close(p.subscribed) + } + p.Unlock() + return p.Pubsub.Subscribe(event, listener) +} diff --git a/coderd/workspaces.go b/coderd/workspaces.go index b61564c5039a2..203c9f8599298 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -628,9 +628,9 @@ func createWorkspace( err = api.Database.InTx(func(db database.Store) error { var ( + prebuildsClaimer = *api.PrebuildsClaimer.Load() workspaceID uuid.UUID claimedWorkspace *database.Workspace - prebuildsClaimer = *api.PrebuildsClaimer.Load() ) // If a template preset was chosen, try claim a prebuilt workspace. @@ -704,8 +704,7 @@ func createWorkspace( Reason(database.BuildReasonInitiator). Initiator(initiatorID). ActiveVersion(). - RichParameterValues(req.RichParameterValues). - TemplateVersionPresetID(req.TemplateVersionPresetID) + RichParameterValues(req.RichParameterValues) if req.TemplateVersionID != uuid.Nil { builder = builder.VersionID(req.TemplateVersionID) } diff --git a/coderd/wsbuilder/wsbuilder.go b/coderd/wsbuilder/wsbuilder.go index b6eb621c55620..91638c63e436f 100644 --- a/coderd/wsbuilder/wsbuilder.go +++ b/coderd/wsbuilder/wsbuilder.go @@ -77,8 +77,7 @@ type Builder struct { parameterValues *[]string templateVersionPresetParameterValues []database.TemplateVersionPresetParameter - prebuiltWorkspaceBuildStage sdkproto.PrebuiltWorkspaceBuildStage - + prebuiltWorkspaceBuildStage sdkproto.PrebuiltWorkspaceBuildStage verifyNoLegacyParametersOnce bool } diff --git a/codersdk/agentsdk/agentsdk.go b/codersdk/agentsdk/agentsdk.go index 8a7ed4d525af4..ba3ff5681b742 100644 --- a/codersdk/agentsdk/agentsdk.go +++ b/codersdk/agentsdk/agentsdk.go @@ -19,12 +19,15 @@ import ( "tailscale.com/tailcfg" "cdr.dev/slog" + "github.com/coder/retry" + "github.com/coder/websocket" + "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/apiversion" + "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/drpcsdk" tailnetproto "github.com/coder/coder/v2/tailnet/proto" - "github.com/coder/websocket" ) // ExternalLogSourceID is the statically-defined ID of a log-source that @@ -686,3 +689,188 @@ func LogsNotifyChannel(agentID uuid.UUID) string { type LogsNotifyMessage struct { CreatedAfter int64 `json:"created_after"` } + +type ReinitializationReason string + +const ( + ReinitializeReasonPrebuildClaimed ReinitializationReason = "prebuild_claimed" +) + +type ReinitializationEvent struct { + WorkspaceID uuid.UUID + Reason ReinitializationReason `json:"reason"` +} + +func PrebuildClaimedChannel(id uuid.UUID) string { + return fmt.Sprintf("prebuild_claimed_%s", id) +} + +// WaitForReinit polls a SSE endpoint, and receives an event back under the following conditions: +// - ping: ignored, keepalive +// - prebuild claimed: a prebuilt workspace is claimed, so the agent must reinitialize. +func (c *Client) WaitForReinit(ctx context.Context) (*ReinitializationEvent, error) { + rpcURL, err := c.SDK.URL.Parse("/api/v2/workspaceagents/me/reinit") + if err != nil { + return nil, xerrors.Errorf("parse url: %w", err) + } + + jar, err := cookiejar.New(nil) + if err != nil { + return nil, xerrors.Errorf("create cookie jar: %w", err) + } + jar.SetCookies(rpcURL, []*http.Cookie{{ + Name: codersdk.SessionTokenCookie, + Value: c.SDK.SessionToken(), + }}) + httpClient := &http.Client{ + Jar: jar, + Transport: c.SDK.HTTPClient.Transport, + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, rpcURL.String(), nil) + if err != nil { + return nil, xerrors.Errorf("build request: %w", err) + } + + res, err := httpClient.Do(req) + if err != nil { + return nil, xerrors.Errorf("execute request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return nil, codersdk.ReadBodyAsError(res) + } + + reinitEvent, err := NewSSEAgentReinitReceiver(res.Body).Receive(ctx) + if err != nil { + return nil, xerrors.Errorf("listening for reinitialization events: %w", err) + } + return reinitEvent, nil +} + +func WaitForReinitLoop(ctx context.Context, logger slog.Logger, client *Client) <-chan ReinitializationEvent { + reinitEvents := make(chan ReinitializationEvent) + + go func() { + for retrier := retry.New(100*time.Millisecond, 10*time.Second); retrier.Wait(ctx); { + logger.Debug(ctx, "waiting for agent reinitialization instructions") + reinitEvent, err := client.WaitForReinit(ctx) + if err != nil { + logger.Error(ctx, "failed to wait for agent reinitialization instructions", slog.Error(err)) + continue + } + retrier.Reset() + select { + case <-ctx.Done(): + close(reinitEvents) + return + case reinitEvents <- *reinitEvent: + } + } + }() + + return reinitEvents +} + +func NewSSEAgentReinitTransmitter(logger slog.Logger, rw http.ResponseWriter, r *http.Request) *SSEAgentReinitTransmitter { + return &SSEAgentReinitTransmitter{logger: logger, rw: rw, r: r} +} + +type SSEAgentReinitTransmitter struct { + rw http.ResponseWriter + r *http.Request + logger slog.Logger +} + +var ( + ErrTransmissionSourceClosed = xerrors.New("transmission source closed") + ErrTransmissionTargetClosed = xerrors.New("transmission target closed") +) + +// Transmit will read from the given chan and send events for as long as: +// * the chan remains open +// * the context has not been canceled +// * not timed out +// * the connection to the receiver remains open +func (s *SSEAgentReinitTransmitter) Transmit(ctx context.Context, reinitEvents <-chan ReinitializationEvent) error { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + sseSendEvent, sseSenderClosed, err := httpapi.ServerSentEventSender(s.rw, s.r) + if err != nil { + return xerrors.Errorf("failed to create sse transmitter: %w", err) + } + + defer func() { + // Block returning until the ServerSentEventSender is closed + // to avoid a race condition where we might write or flush to rw after the handler returns. + <-sseSenderClosed + }() + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-sseSenderClosed: + return ErrTransmissionTargetClosed + case reinitEvent, ok := <-reinitEvents: + if !ok { + return ErrTransmissionSourceClosed + } + err := sseSendEvent(codersdk.ServerSentEvent{ + Type: codersdk.ServerSentEventTypeData, + Data: reinitEvent, + }) + if err != nil { + return err + } + } + } +} + +func NewSSEAgentReinitReceiver(r io.ReadCloser) *SSEAgentReinitReceiver { + return &SSEAgentReinitReceiver{r: r} +} + +type SSEAgentReinitReceiver struct { + r io.ReadCloser +} + +func (s *SSEAgentReinitReceiver) Receive(ctx context.Context) (*ReinitializationEvent, error) { + nextEvent := codersdk.ServerSentEventReader(ctx, s.r) + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + sse, err := nextEvent() + switch { + case err != nil: + return nil, xerrors.Errorf("failed to read server-sent event: %w", err) + case sse.Type == codersdk.ServerSentEventTypeError: + return nil, xerrors.Errorf("unexpected server sent event type error") + case sse.Type == codersdk.ServerSentEventTypePing: + continue + case sse.Type != codersdk.ServerSentEventTypeData: + return nil, xerrors.Errorf("unexpected server sent event type: %s", sse.Type) + } + + // At this point we know that the sent event is of type codersdk.ServerSentEventTypeData + var reinitEvent ReinitializationEvent + b, ok := sse.Data.([]byte) + if !ok { + return nil, xerrors.Errorf("expected data as []byte, got %T", sse.Data) + } + err = json.Unmarshal(b, &reinitEvent) + if err != nil { + return nil, xerrors.Errorf("unmarshal reinit response: %w", err) + } + return &reinitEvent, nil + } +} diff --git a/codersdk/agentsdk/agentsdk_test.go b/codersdk/agentsdk/agentsdk_test.go new file mode 100644 index 0000000000000..8ad2d69be0b98 --- /dev/null +++ b/codersdk/agentsdk/agentsdk_test.go @@ -0,0 +1,122 @@ +package agentsdk_test + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/codersdk/agentsdk" + "github.com/coder/coder/v2/testutil" +) + +func TestStreamAgentReinitEvents(t *testing.T) { + t.Parallel() + + t.Run("transmitted events are received", func(t *testing.T) { + t.Parallel() + + eventToSend := agentsdk.ReinitializationEvent{ + WorkspaceID: uuid.New(), + Reason: agentsdk.ReinitializeReasonPrebuildClaimed, + } + + events := make(chan agentsdk.ReinitializationEvent, 1) + events <- eventToSend + + transmitCtx := testutil.Context(t, testutil.WaitShort) + transmitErrCh := make(chan error, 1) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + transmitter := agentsdk.NewSSEAgentReinitTransmitter(slogtest.Make(t, nil), w, r) + transmitErrCh <- transmitter.Transmit(transmitCtx, events) + })) + defer srv.Close() + + requestCtx := testutil.Context(t, testutil.WaitShort) + req, err := http.NewRequestWithContext(requestCtx, "GET", srv.URL, nil) + require.NoError(t, err) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + receiveCtx := testutil.Context(t, testutil.WaitShort) + receiver := agentsdk.NewSSEAgentReinitReceiver(resp.Body) + sentEvent, receiveErr := receiver.Receive(receiveCtx) + require.Nil(t, receiveErr) + require.Equal(t, eventToSend, *sentEvent) + }) + + t.Run("doesn't transmit events if the transmitter context is canceled", func(t *testing.T) { + t.Parallel() + + eventToSend := agentsdk.ReinitializationEvent{ + WorkspaceID: uuid.New(), + Reason: agentsdk.ReinitializeReasonPrebuildClaimed, + } + + events := make(chan agentsdk.ReinitializationEvent, 1) + events <- eventToSend + + transmitCtx, cancelTransmit := context.WithCancel(testutil.Context(t, testutil.WaitShort)) + cancelTransmit() + transmitErrCh := make(chan error, 1) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + transmitter := agentsdk.NewSSEAgentReinitTransmitter(slogtest.Make(t, nil), w, r) + transmitErrCh <- transmitter.Transmit(transmitCtx, events) + })) + + defer srv.Close() + + requestCtx := testutil.Context(t, testutil.WaitShort) + req, err := http.NewRequestWithContext(requestCtx, "GET", srv.URL, nil) + require.NoError(t, err) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + receiveCtx := testutil.Context(t, testutil.WaitShort) + receiver := agentsdk.NewSSEAgentReinitReceiver(resp.Body) + sentEvent, receiveErr := receiver.Receive(receiveCtx) + require.Nil(t, sentEvent) + require.ErrorIs(t, receiveErr, io.EOF) + }) + + t.Run("does not receive events if the receiver context is canceled", func(t *testing.T) { + t.Parallel() + + eventToSend := agentsdk.ReinitializationEvent{ + WorkspaceID: uuid.New(), + Reason: agentsdk.ReinitializeReasonPrebuildClaimed, + } + + events := make(chan agentsdk.ReinitializationEvent, 1) + events <- eventToSend + + transmitCtx := testutil.Context(t, testutil.WaitShort) + transmitErrCh := make(chan error, 1) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + transmitter := agentsdk.NewSSEAgentReinitTransmitter(slogtest.Make(t, nil), w, r) + transmitErrCh <- transmitter.Transmit(transmitCtx, events) + })) + defer srv.Close() + + requestCtx := testutil.Context(t, testutil.WaitShort) + req, err := http.NewRequestWithContext(requestCtx, "GET", srv.URL, nil) + require.NoError(t, err) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + receiveCtx, cancelReceive := context.WithCancel(context.Background()) + cancelReceive() + receiver := agentsdk.NewSSEAgentReinitReceiver(resp.Body) + sentEvent, receiveErr := receiver.Receive(receiveCtx) + require.Nil(t, sentEvent) + require.ErrorIs(t, receiveErr, context.Canceled) + }) +} diff --git a/codersdk/client.go b/codersdk/client.go index 8ab5a289b2cf5..4492066785d6f 100644 --- a/codersdk/client.go +++ b/codersdk/client.go @@ -631,7 +631,7 @@ func (h *HeaderTransport) RoundTrip(req *http.Request) (*http.Response, error) { } } if h.Transport == nil { - h.Transport = http.DefaultTransport + return http.DefaultTransport.RoundTrip(req) } return h.Transport.RoundTrip(req) } diff --git a/docs/reference/api/agents.md b/docs/reference/api/agents.md index c0ddd79cd2052..eced88f4f72cc 100644 --- a/docs/reference/api/agents.md +++ b/docs/reference/api/agents.md @@ -470,6 +470,38 @@ curl -X PATCH http://coder-server:8080/api/v2/workspaceagents/me/logs \ To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Get workspace agent reinitialization + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/reinit \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /workspaceagents/me/reinit` + +### Example responses + +> 200 Response + +```json +{ + "reason": "prebuild_claimed", + "workspaceID": "string" +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|----------------------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [agentsdk.ReinitializationEvent](schemas.md#agentsdkreinitializationevent) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Get workspace agent by ID ### Code samples diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 8c1c86167b9d4..eeb0014bd521e 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -182,6 +182,36 @@ | `icon` | string | false | | | | `id` | string | false | | ID is a unique identifier for the log source. It is scoped to a workspace agent, and can be statically defined inside code to prevent duplicate sources from being created for the same agent. | +## agentsdk.ReinitializationEvent + +```json +{ + "reason": "prebuild_claimed", + "workspaceID": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|---------------|--------------------------------------------------------------------|----------|--------------|-------------| +| `reason` | [agentsdk.ReinitializationReason](#agentsdkreinitializationreason) | false | | | +| `workspaceID` | string | false | | | + +## agentsdk.ReinitializationReason + +```json +"prebuild_claimed" +``` + +### Properties + +#### Enumerated Values + +| Value | +|--------------------| +| `prebuild_claimed` | + ## aisdk.Attachment ```json diff --git a/enterprise/coderd/workspaceagents_test.go b/enterprise/coderd/workspaceagents_test.go index 4ac374a3c8c8e..44aba69b9ffaa 100644 --- a/enterprise/coderd/workspaceagents_test.go +++ b/enterprise/coderd/workspaceagents_test.go @@ -5,12 +5,19 @@ import ( "crypto/tls" "fmt" "net/http" + "os" + "regexp" "testing" + "time" + + "github.com/coder/coder/v2/coderd/database/dbtestutil" + "github.com/coder/serpent" "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/coder/coder/v2/agent" + "github.com/coder/coder/v2/cli/clitest" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" @@ -73,6 +80,168 @@ func TestBlockNonBrowser(t *testing.T) { }) } +func TestReinitializeAgent(t *testing.T) { + t.Parallel() + + tempAgentLog := testutil.CreateTemp(t, "", "testReinitializeAgent") + + if !dbtestutil.WillUsePostgres() { + t.Skip("dbmem cannot currently claim a workspace") + } + + db, ps := dbtestutil.NewDB(t) + // GIVEN a live enterprise API with the prebuilds feature enabled + client, user := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + Database: db, + Pubsub: ps, + DeploymentValues: coderdtest.DeploymentValues(t, func(dv *codersdk.DeploymentValues) { + dv.Prebuilds.ReconciliationInterval = serpent.Duration(time.Second) + dv.Experiments.Append(string(codersdk.ExperimentWorkspacePrebuilds)) + }), + IncludeProvisionerDaemon: true, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureWorkspacePrebuilds: 1, + }, + }, + }) + + // GIVEN a template, template version, preset and a prebuilt workspace that uses them all + agentToken := uuid.UUID{3} + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionPlan: []*proto.Response{ + { + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{ + Presets: []*proto.Preset{ + { + Name: "test-preset", + Prebuild: &proto.Prebuild{ + Instances: 1, + }, + }, + }, + Resources: []*proto.Resource{ + { + Agents: []*proto.Agent{ + { + Name: "smith", + OperatingSystem: "linux", + Architecture: "i386", + }, + }, + }, + }, + }, + }, + }, + }, + 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", + Scripts: []*proto.Script{ + { + RunOnStart: true, + Script: fmt.Sprintf("printenv >> %s; echo '---\n' >> %s", tempAgentLog.Name(), tempAgentLog.Name()), // Make reinitialization take long enough to assert that it happened + }, + }, + Auth: &proto.Agent_Token{ + Token: agentToken.String(), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + + coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + + // Wait for prebuilds to create a prebuilt workspace + ctx := context.Background() + // ctx := testutil.Context(t, testutil.WaitLong) + var ( + prebuildID uuid.UUID + ) + require.Eventually(t, func() bool { + agentAndBuild, err := db.GetWorkspaceAgentAndLatestBuildByAuthToken(ctx, agentToken) + if err != nil { + return false + } + prebuildID = agentAndBuild.WorkspaceBuild.ID + return true + }, testutil.WaitLong, testutil.IntervalFast) + + prebuild := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, prebuildID) + + preset, err := db.GetPresetByWorkspaceBuildID(ctx, prebuildID) + require.NoError(t, err) + + // GIVEN a running agent + logDir := t.TempDir() + inv, _ := clitest.New(t, + "agent", + "--auth", "token", + "--agent-token", agentToken.String(), + "--agent-url", client.URL.String(), + "--log-dir", logDir, + ) + clitest.Start(t, inv) + + // GIVEN the agent is in a happy steady state + waiter := coderdtest.NewWorkspaceAgentWaiter(t, client, prebuild.WorkspaceID) + waiter.WaitFor(coderdtest.AgentsReady) + + // WHEN a workspace is created that can benefit from prebuilds + anotherClient, anotherUser := coderdtest.CreateAnotherUser(t, client, user.OrganizationID) + workspace, err := anotherClient.CreateUserWorkspace(ctx, anotherUser.ID.String(), codersdk.CreateWorkspaceRequest{ + TemplateVersionID: version.ID, + TemplateVersionPresetID: preset.ID, + Name: "claimed-workspace", + }) + require.NoError(t, err) + + coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) + + // THEN reinitialization completes + waiter.WaitFor(coderdtest.AgentsReady) + + var matches [][]byte + require.Eventually(t, func() bool { + // THEN the agent script ran again and reused the same agent token + contents, err := os.ReadFile(tempAgentLog.Name()) + if err != nil { + return false + } + // UUID regex pattern (matches UUID v4-like strings) + uuidRegex := regexp.MustCompile(`\bCODER_AGENT_TOKEN=(.+)\b`) + + matches = uuidRegex.FindAll(contents, -1) + // When an agent reinitializes, we expect it to run startup scripts again. + // As such, we expect to have written the agent environment to the temp file twice. + // Once on initial startup and then once on reinitialization. + return len(matches) == 2 + }, testutil.WaitLong, testutil.IntervalMedium) + require.Equal(t, matches[0], matches[1]) +} + type setupResp struct { workspace codersdk.Workspace sdkAgent codersdk.WorkspaceAgent diff --git a/enterprise/coderd/workspaces_test.go b/enterprise/coderd/workspaces_test.go index 85b414960f85c..7005c93ca36f5 100644 --- a/enterprise/coderd/workspaces_test.go +++ b/enterprise/coderd/workspaces_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "database/sql" + "encoding/json" "fmt" "net/http" "os" @@ -13,6 +14,7 @@ import ( "testing" "time" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -30,6 +32,8 @@ import ( "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/coderd/notifications" + "github.com/coder/coder/v2/coderd/prebuilds" + "github.com/coder/coder/v2/coderd/provisionerdserver" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" agplschedule "github.com/coder/coder/v2/coderd/schedule" @@ -43,6 +47,7 @@ import ( "github.com/coder/coder/v2/enterprise/coderd/schedule" "github.com/coder/coder/v2/provisioner/echo" "github.com/coder/coder/v2/provisionersdk" + "github.com/coder/coder/v2/provisionersdk/proto" "github.com/coder/coder/v2/testutil" "github.com/coder/quartz" ) @@ -459,6 +464,79 @@ func TestCreateUserWorkspace(t *testing.T) { _, err = client1.CreateUserWorkspace(ctx, user1.ID.String(), req) require.Error(t, err) }) + + t.Run("ClaimPrebuild", func(t *testing.T) { + t.Parallel() + + if !dbtestutil.WillUsePostgres() { + t.Skip("dbmem cannot currently claim a workspace") + } + + client, db, user := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: coderdtest.DeploymentValues(t, func(dv *codersdk.DeploymentValues) { + err := dv.Experiments.Append(string(codersdk.ExperimentWorkspacePrebuilds)) + require.NoError(t, err) + }), + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureWorkspacePrebuilds: 1, + }, + }, + }) + + // GIVEN a template, template version, preset and a prebuilt workspace that uses them all + presetID := uuid.New() + tv := dbfake.TemplateVersion(t, db).Seed(database.TemplateVersion{ + OrganizationID: user.OrganizationID, + CreatedBy: user.UserID, + }).Preset(database.TemplateVersionPreset{ + ID: presetID, + }).Do() + + r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OwnerID: prebuilds.SystemUserID, + TemplateID: tv.Template.ID, + }).Seed(database.WorkspaceBuild{ + TemplateVersionID: tv.TemplateVersion.ID, + TemplateVersionPresetID: uuid.NullUUID{ + UUID: presetID, + Valid: true, + }, + }).WithAgent(func(a []*proto.Agent) []*proto.Agent { + return a + }).Do() + + // nolint:gocritic // this is a test + ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitLong)) + agent, err := db.GetWorkspaceAgentAndLatestBuildByAuthToken(ctx, uuid.MustParse(r.AgentToken)) + require.NoError(t, err) + + err = db.UpdateWorkspaceAgentLifecycleStateByID(ctx, database.UpdateWorkspaceAgentLifecycleStateByIDParams{ + ID: agent.WorkspaceAgent.ID, + LifecycleState: database.WorkspaceAgentLifecycleStateReady, + }) + require.NoError(t, err) + + // WHEN a workspace is created that matches the available prebuilt workspace + _, err = client.CreateUserWorkspace(ctx, user.UserID.String(), codersdk.CreateWorkspaceRequest{ + TemplateVersionID: tv.TemplateVersion.ID, + TemplateVersionPresetID: presetID, + Name: "claimed-workspace", + }) + require.NoError(t, err) + + // THEN a new build is scheduled with the build stage specified + build, err := db.GetLatestWorkspaceBuildByWorkspaceID(ctx, r.Workspace.ID) + require.NoError(t, err) + require.NotEqual(t, build.ID, r.Build.ID) + job, err := db.GetProvisionerJobByID(ctx, build.JobID) + require.NoError(t, err) + var metadata provisionerdserver.WorkspaceProvisionJob + require.NoError(t, json.Unmarshal(job.Input, &metadata)) + require.Equal(t, metadata.PrebuiltWorkspaceBuildStage, proto.PrebuiltWorkspaceBuildStage_CLAIM) + }) } func TestWorkspaceAutobuild(t *testing.T) { diff --git a/provisioner/terraform/executor.go b/provisioner/terraform/executor.go index ca353123cf3c8..4be0f0749c372 100644 --- a/provisioner/terraform/executor.go +++ b/provisioner/terraform/executor.go @@ -350,6 +350,68 @@ func onlyDataResources(sm tfjson.StateModule) tfjson.StateModule { return filtered } +func (e *executor) logResourceReplacements(ctx context.Context, plan *tfjson.Plan) { + if plan == nil { + return + } + + if len(plan.ResourceChanges) == 0 { + return + } + var ( + count int + replacements = make(map[string][]string, len(plan.ResourceChanges)) + ) + + for _, ch := range plan.ResourceChanges { + // No change, no problem! + if ch.Change == nil { + continue + } + + // No-op change, no problem! + if ch.Change.Actions.NoOp() { + continue + } + + // No replacements, no problem! + if len(ch.Change.ReplacePaths) == 0 { + continue + } + + // Replacing our resources, no problem! + if strings.Index(ch.Type, "coder_") == 0 { + continue + } + + for _, p := range ch.Change.ReplacePaths { + var path string + switch p := p.(type) { + case []interface{}: + segs := p + list := make([]string, 0, len(segs)) + for _, s := range segs { + list = append(list, fmt.Sprintf("%v", s)) + } + path = strings.Join(list, ".") + default: + path = fmt.Sprintf("%v", p) + } + + replacements[ch.Address] = append(replacements[ch.Address], path) + } + + count++ + } + + if count > 0 { + e.server.logger.Warn(ctx, "plan introduces resource changes", slog.F("count", count)) + for n, p := range replacements { + e.server.logger.Warn(ctx, "resource will be replaced", slog.F("name", n), slog.F("replacement_paths", strings.Join(p, ","))) + } + } +} + // planResources must only be called while the lock is held. func (e *executor) planResources(ctx, killCtx context.Context, planfilePath string) (*State, json.RawMessage, error) { ctx, span := e.server.startTrace(ctx, tracing.FuncName()) @@ -360,6 +422,8 @@ func (e *executor) planResources(ctx, killCtx context.Context, planfilePath stri return nil, nil, xerrors.Errorf("show terraform plan file: %w", err) } + e.logResourceReplacements(ctx, plan) + rawGraph, err := e.graph(ctx, killCtx) if err != nil { return nil, nil, xerrors.Errorf("graph: %w", err) diff --git a/provisioner/terraform/provision.go b/provisioner/terraform/provision.go index 9f2edb5262716..f2a92b5745a87 100644 --- a/provisioner/terraform/provision.go +++ b/provisioner/terraform/provision.go @@ -273,6 +273,17 @@ func provisionEnv( if metadata.GetPrebuiltWorkspaceBuildStage().IsPrebuild() { env = append(env, provider.IsPrebuildEnvironmentVariable()+"=true") } + tokens := metadata.GetRunningAgentAuthTokens() + if len(tokens) == 1 { + env = append(env, provider.RunningAgentTokenEnvironmentVariable("")+"="+tokens[0].Token) + } else { + // Not currently supported, but added for forward-compatibility + for _, t := range tokens { + // If there are multiple agents, provide all the tokens to terraform so that it can + // choose the correct one for each agent ID. + env = append(env, provider.RunningAgentTokenEnvironmentVariable(t.AgentId)+"="+t.Token) + } + } if metadata.GetPrebuiltWorkspaceBuildStage().IsPrebuiltWorkspaceClaim() { env = append(env, provider.IsPrebuildClaimEnvironmentVariable()+"=true") } diff --git a/provisionerd/proto/version.go b/provisionerd/proto/version.go index c899e288dffe5..5e26a5909c060 100644 --- a/provisionerd/proto/version.go +++ b/provisionerd/proto/version.go @@ -19,6 +19,7 @@ import "github.com/coder/coder/v2/apiversion" // - Add previous parameter values to 'WorkspaceBuild' jobs. Provisioner passes // the previous values for the `terraform apply` to enforce monotonicity // in the terraform provider. +// - Add new field named `running_agent_auth_tokens` to provisioner job metadata const ( CurrentMajor = 1 CurrentMinor = 5 diff --git a/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go index 25eaadc126375..c0bf8a533d023 100644 --- a/provisionersdk/proto/provisioner.pb.go +++ b/provisionersdk/proto/provisioner.pb.go @@ -2335,6 +2335,61 @@ func (x *Role) GetOrgId() string { return "" } +type RunningAgentAuthToken struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + AgentId string `protobuf:"bytes,1,opt,name=agent_id,json=agentId,proto3" json:"agent_id,omitempty"` + Token string `protobuf:"bytes,2,opt,name=token,proto3" json:"token,omitempty"` +} + +func (x *RunningAgentAuthToken) Reset() { + *x = RunningAgentAuthToken{} + if protoimpl.UnsafeEnabled { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RunningAgentAuthToken) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RunningAgentAuthToken) ProtoMessage() {} + +func (x *RunningAgentAuthToken) ProtoReflect() protoreflect.Message { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[26] + 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 RunningAgentAuthToken.ProtoReflect.Descriptor instead. +func (*RunningAgentAuthToken) Descriptor() ([]byte, []int) { + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{26} +} + +func (x *RunningAgentAuthToken) GetAgentId() string { + if x != nil { + return x.AgentId + } + return "" +} + +func (x *RunningAgentAuthToken) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + // Metadata is information about a workspace used in the execution of a build type Metadata struct { state protoimpl.MessageState @@ -2361,13 +2416,13 @@ type Metadata struct { WorkspaceOwnerLoginType string `protobuf:"bytes,18,opt,name=workspace_owner_login_type,json=workspaceOwnerLoginType,proto3" json:"workspace_owner_login_type,omitempty"` WorkspaceOwnerRbacRoles []*Role `protobuf:"bytes,19,rep,name=workspace_owner_rbac_roles,json=workspaceOwnerRbacRoles,proto3" json:"workspace_owner_rbac_roles,omitempty"` PrebuiltWorkspaceBuildStage PrebuiltWorkspaceBuildStage `protobuf:"varint,20,opt,name=prebuilt_workspace_build_stage,json=prebuiltWorkspaceBuildStage,proto3,enum=provisioner.PrebuiltWorkspaceBuildStage" json:"prebuilt_workspace_build_stage,omitempty"` // Indicates that a prebuilt workspace is being built. - RunningWorkspaceAgentToken string `protobuf:"bytes,21,opt,name=running_workspace_agent_token,json=runningWorkspaceAgentToken,proto3" json:"running_workspace_agent_token,omitempty"` // Preserves the running agent token of a prebuilt workspace so it can reinitialize. + RunningAgentAuthTokens []*RunningAgentAuthToken `protobuf:"bytes,21,rep,name=running_agent_auth_tokens,json=runningAgentAuthTokens,proto3" json:"running_agent_auth_tokens,omitempty"` } func (x *Metadata) Reset() { *x = Metadata{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[26] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2380,7 +2435,7 @@ func (x *Metadata) String() string { func (*Metadata) ProtoMessage() {} func (x *Metadata) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[26] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[27] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2393,7 +2448,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{26} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{27} } func (x *Metadata) GetCoderUrl() string { @@ -2536,11 +2591,11 @@ func (x *Metadata) GetPrebuiltWorkspaceBuildStage() PrebuiltWorkspaceBuildStage return PrebuiltWorkspaceBuildStage_NONE } -func (x *Metadata) GetRunningWorkspaceAgentToken() string { +func (x *Metadata) GetRunningAgentAuthTokens() []*RunningAgentAuthToken { if x != nil { - return x.RunningWorkspaceAgentToken + return x.RunningAgentAuthTokens } - return "" + return nil } // Config represents execution configuration shared by all subsequent requests in the Session @@ -2559,7 +2614,7 @@ type Config struct { func (x *Config) Reset() { *x = Config{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[27] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2572,7 +2627,7 @@ func (x *Config) String() string { func (*Config) ProtoMessage() {} func (x *Config) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[27] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[28] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2585,7 +2640,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{27} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{28} } func (x *Config) GetTemplateSourceArchive() []byte { @@ -2619,7 +2674,7 @@ type ParseRequest struct { func (x *ParseRequest) Reset() { *x = ParseRequest{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[28] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2632,7 +2687,7 @@ func (x *ParseRequest) String() string { func (*ParseRequest) ProtoMessage() {} func (x *ParseRequest) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[28] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[29] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2645,7 +2700,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{28} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{29} } // ParseComplete indicates a request to parse completed. @@ -2663,7 +2718,7 @@ type ParseComplete struct { func (x *ParseComplete) Reset() { *x = ParseComplete{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[29] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2676,7 +2731,7 @@ func (x *ParseComplete) String() string { func (*ParseComplete) ProtoMessage() {} func (x *ParseComplete) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[29] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[30] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2689,7 +2744,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{29} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{30} } func (x *ParseComplete) GetError() string { @@ -2736,7 +2791,7 @@ type PlanRequest struct { func (x *PlanRequest) Reset() { *x = PlanRequest{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[30] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2749,7 +2804,7 @@ func (x *PlanRequest) String() string { func (*PlanRequest) ProtoMessage() {} func (x *PlanRequest) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[30] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[31] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2762,7 +2817,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{30} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{31} } func (x *PlanRequest) GetMetadata() *Metadata { @@ -2820,7 +2875,7 @@ type PlanComplete struct { func (x *PlanComplete) Reset() { *x = PlanComplete{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[31] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2833,7 +2888,7 @@ func (x *PlanComplete) String() string { func (*PlanComplete) ProtoMessage() {} func (x *PlanComplete) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[31] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[32] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2846,7 +2901,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{31} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{32} } func (x *PlanComplete) GetError() string { @@ -2925,7 +2980,7 @@ type ApplyRequest struct { func (x *ApplyRequest) Reset() { *x = ApplyRequest{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[32] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2938,7 +2993,7 @@ func (x *ApplyRequest) String() string { func (*ApplyRequest) ProtoMessage() {} func (x *ApplyRequest) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[32] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[33] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2951,7 +3006,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{32} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{33} } func (x *ApplyRequest) GetMetadata() *Metadata { @@ -2978,7 +3033,7 @@ type ApplyComplete struct { func (x *ApplyComplete) Reset() { *x = ApplyComplete{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[33] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2991,7 +3046,7 @@ func (x *ApplyComplete) String() string { func (*ApplyComplete) ProtoMessage() {} func (x *ApplyComplete) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[33] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[34] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3004,7 +3059,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{33} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{34} } func (x *ApplyComplete) GetState() []byte { @@ -3066,7 +3121,7 @@ type Timing struct { func (x *Timing) Reset() { *x = Timing{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[34] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3079,7 +3134,7 @@ func (x *Timing) String() string { func (*Timing) ProtoMessage() {} func (x *Timing) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[34] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[35] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3092,7 +3147,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{34} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{35} } func (x *Timing) GetStart() *timestamppb.Timestamp { @@ -3154,7 +3209,7 @@ type CancelRequest struct { func (x *CancelRequest) Reset() { *x = CancelRequest{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[35] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3167,7 +3222,7 @@ func (x *CancelRequest) String() string { func (*CancelRequest) ProtoMessage() {} func (x *CancelRequest) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[35] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[36] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3180,7 +3235,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{35} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{36} } type Request struct { @@ -3201,7 +3256,7 @@ type Request struct { func (x *Request) Reset() { *x = Request{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[36] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3214,7 +3269,7 @@ func (x *Request) String() string { func (*Request) ProtoMessage() {} func (x *Request) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[36] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[37] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3227,7 +3282,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{36} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{37} } func (m *Request) GetType() isRequest_Type { @@ -3323,7 +3378,7 @@ type Response struct { func (x *Response) Reset() { *x = Response{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[37] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3336,7 +3391,7 @@ func (x *Response) String() string { func (*Response) ProtoMessage() {} func (x *Response) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[37] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[38] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3349,7 +3404,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{37} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{38} } func (m *Response) GetType() isResponse_Type { @@ -3431,7 +3486,7 @@ type Agent_Metadata struct { func (x *Agent_Metadata) Reset() { *x = Agent_Metadata{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[38] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3444,7 +3499,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[38] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[39] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3516,7 +3571,7 @@ type Resource_Metadata struct { func (x *Resource_Metadata) Reset() { *x = Resource_Metadata{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[40] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3529,7 +3584,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[40] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[41] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3878,265 +3933,272 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 0x52, 0x03, 0x64, 0x69, 0x72, 0x22, 0x31, 0x0a, 0x04, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x15, 0x0a, 0x06, 0x6f, 0x72, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x6f, 0x72, 0x67, 0x49, 0x64, 0x22, 0xae, 0x09, 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, + 0x09, 0x52, 0x05, 0x6f, 0x72, 0x67, 0x49, 0x64, 0x22, 0x48, 0x0a, 0x15, 0x52, 0x75, 0x6e, 0x6e, + 0x69, 0x6e, 0x67, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, + 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, + 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x22, 0xca, 0x09, 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, - 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, 0x12, 0x4e, 0x0a, 0x1a, 0x77, 0x6f, 0x72, 0x6b, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x72, 0x62, 0x61, 0x63, 0x5f, - 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x18, 0x13, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x6f, 0x6c, 0x65, 0x52, 0x17, - 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x52, 0x62, - 0x61, 0x63, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x12, 0x6d, 0x0a, 0x1e, 0x70, 0x72, 0x65, 0x62, 0x75, - 0x69, 0x6c, 0x74, 0x5f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x62, 0x75, - 0x69, 0x6c, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x67, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x28, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, - 0x65, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, - 0x75, 0x69, 0x6c, 0x64, 0x53, 0x74, 0x61, 0x67, 0x65, 0x52, 0x1b, 0x70, 0x72, 0x65, 0x62, 0x75, - 0x69, 0x6c, 0x74, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, - 0x64, 0x53, 0x74, 0x61, 0x67, 0x65, 0x12, 0x41, 0x0a, 0x1d, 0x72, 0x75, 0x6e, 0x6e, 0x69, 0x6e, - 0x67, 0x5f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x15, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1a, 0x72, - 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, - 0x67, 0x65, 0x6e, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 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, 0x92, 0x03, 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, 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, + 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, 0x12, 0x4e, 0x0a, 0x1a, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, + 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x72, 0x62, 0x61, 0x63, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x18, + 0x13, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x6f, 0x6c, 0x65, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x52, 0x62, 0x61, 0x63, 0x52, 0x6f, 0x6c, 0x65, + 0x73, 0x12, 0x6d, 0x0a, 0x1e, 0x70, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x5f, 0x77, 0x6f, + 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x73, 0x74, + 0x61, 0x67, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x28, 0x2e, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x74, + 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x53, 0x74, + 0x61, 0x67, 0x65, 0x52, 0x1b, 0x70, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x57, 0x6f, 0x72, + 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x53, 0x74, 0x61, 0x67, 0x65, + 0x12, 0x5d, 0x0a, 0x19, 0x72, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x61, 0x67, 0x65, 0x6e, + 0x74, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x15, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, + 0x72, 0x2e, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x41, 0x75, + 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x16, 0x72, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, + 0x41, 0x67, 0x65, 0x6e, 0x74, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 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, 0x92, 0x03, 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, + 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, 0x73, 0x12, 0x5b, 0x0a, 0x19, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, - 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, + 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, 0x12, 0x5b, 0x0a, 0x19, 0x70, 0x72, + 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, + 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x05, 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, 0x17, + 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, + 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0xbc, 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, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x17, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, - 0x75, 0x73, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x73, 0x22, 0xbc, 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, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6c, 0x61, - 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x12, 0x21, 0x0a, - 0x0c, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x0a, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x46, 0x69, 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, - 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, 0x3e, 0x0a, 0x1b, 0x50, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x57, - 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x53, 0x74, 0x61, - 0x67, 0x65, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, - 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x43, 0x4c, 0x41, 0x49, - 0x4d, 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, + 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, 0x12, + 0x12, 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x70, + 0x6c, 0x61, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x5f, 0x66, 0x69, + 0x6c, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x6d, 0x6f, 0x64, 0x75, 0x6c, + 0x65, 0x46, 0x69, 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, 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, 0x3e, 0x0a, 0x1b, 0x50, 0x72, 0x65, + 0x62, 0x75, 0x69, 0x6c, 0x74, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, + 0x69, 0x6c, 0x64, 0x53, 0x74, 0x61, 0x67, 0x65, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4e, 0x45, + 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x01, 0x12, 0x09, + 0x0a, 0x05, 0x43, 0x4c, 0x41, 0x49, 0x4d, 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 ( @@ -4152,7 +4214,7 @@ func file_provisionersdk_proto_provisioner_proto_rawDescGZIP() []byte { } var file_provisionersdk_proto_provisioner_proto_enumTypes = make([]protoimpl.EnumInfo, 6) -var file_provisionersdk_proto_provisioner_proto_msgTypes = make([]protoimpl.MessageInfo, 42) +var file_provisionersdk_proto_provisioner_proto_msgTypes = make([]protoimpl.MessageInfo, 43) var file_provisionersdk_proto_provisioner_proto_goTypes = []interface{}{ (LogLevel)(0), // 0: provisioner.LogLevel (AppSharingLevel)(0), // 1: provisioner.AppSharingLevel @@ -4186,32 +4248,33 @@ var file_provisionersdk_proto_provisioner_proto_goTypes = []interface{}{ (*Resource)(nil), // 29: provisioner.Resource (*Module)(nil), // 30: provisioner.Module (*Role)(nil), // 31: provisioner.Role - (*Metadata)(nil), // 32: provisioner.Metadata - (*Config)(nil), // 33: provisioner.Config - (*ParseRequest)(nil), // 34: provisioner.ParseRequest - (*ParseComplete)(nil), // 35: provisioner.ParseComplete - (*PlanRequest)(nil), // 36: provisioner.PlanRequest - (*PlanComplete)(nil), // 37: provisioner.PlanComplete - (*ApplyRequest)(nil), // 38: provisioner.ApplyRequest - (*ApplyComplete)(nil), // 39: provisioner.ApplyComplete - (*Timing)(nil), // 40: provisioner.Timing - (*CancelRequest)(nil), // 41: provisioner.CancelRequest - (*Request)(nil), // 42: provisioner.Request - (*Response)(nil), // 43: provisioner.Response - (*Agent_Metadata)(nil), // 44: provisioner.Agent.Metadata - nil, // 45: provisioner.Agent.EnvEntry - (*Resource_Metadata)(nil), // 46: provisioner.Resource.Metadata - nil, // 47: provisioner.ParseComplete.WorkspaceTagsEntry - (*timestamppb.Timestamp)(nil), // 48: google.protobuf.Timestamp + (*RunningAgentAuthToken)(nil), // 32: provisioner.RunningAgentAuthToken + (*Metadata)(nil), // 33: provisioner.Metadata + (*Config)(nil), // 34: provisioner.Config + (*ParseRequest)(nil), // 35: provisioner.ParseRequest + (*ParseComplete)(nil), // 36: provisioner.ParseComplete + (*PlanRequest)(nil), // 37: provisioner.PlanRequest + (*PlanComplete)(nil), // 38: provisioner.PlanComplete + (*ApplyRequest)(nil), // 39: provisioner.ApplyRequest + (*ApplyComplete)(nil), // 40: provisioner.ApplyComplete + (*Timing)(nil), // 41: provisioner.Timing + (*CancelRequest)(nil), // 42: provisioner.CancelRequest + (*Request)(nil), // 43: provisioner.Request + (*Response)(nil), // 44: provisioner.Response + (*Agent_Metadata)(nil), // 45: provisioner.Agent.Metadata + nil, // 46: provisioner.Agent.EnvEntry + (*Resource_Metadata)(nil), // 47: provisioner.Resource.Metadata + nil, // 48: provisioner.ParseComplete.WorkspaceTagsEntry + (*timestamppb.Timestamp)(nil), // 49: google.protobuf.Timestamp } var file_provisionersdk_proto_provisioner_proto_depIdxs = []int32{ 8, // 0: provisioner.RichParameter.options:type_name -> provisioner.RichParameterOption 13, // 1: provisioner.Preset.parameters:type_name -> provisioner.PresetParameter 11, // 2: provisioner.Preset.prebuild:type_name -> provisioner.Prebuild 0, // 3: provisioner.Log.level:type_name -> provisioner.LogLevel - 45, // 4: provisioner.Agent.env:type_name -> provisioner.Agent.EnvEntry + 46, // 4: provisioner.Agent.env:type_name -> provisioner.Agent.EnvEntry 27, // 5: provisioner.Agent.apps:type_name -> provisioner.App - 44, // 6: provisioner.Agent.metadata:type_name -> provisioner.Agent.Metadata + 45, // 6: provisioner.Agent.metadata:type_name -> provisioner.Agent.Metadata 23, // 7: provisioner.Agent.display_apps:type_name -> provisioner.DisplayApps 25, // 8: provisioner.Agent.scripts:type_name -> provisioner.Script 24, // 9: provisioner.Agent.extra_envs:type_name -> provisioner.Env @@ -4223,47 +4286,48 @@ var file_provisionersdk_proto_provisioner_proto_depIdxs = []int32{ 1, // 15: provisioner.App.sharing_level:type_name -> provisioner.AppSharingLevel 2, // 16: provisioner.App.open_in:type_name -> provisioner.AppOpenIn 19, // 17: provisioner.Resource.agents:type_name -> provisioner.Agent - 46, // 18: provisioner.Resource.metadata:type_name -> provisioner.Resource.Metadata + 47, // 18: provisioner.Resource.metadata:type_name -> provisioner.Resource.Metadata 3, // 19: provisioner.Metadata.workspace_transition:type_name -> provisioner.WorkspaceTransition 31, // 20: provisioner.Metadata.workspace_owner_rbac_roles:type_name -> provisioner.Role 4, // 21: provisioner.Metadata.prebuilt_workspace_build_stage:type_name -> provisioner.PrebuiltWorkspaceBuildStage - 7, // 22: provisioner.ParseComplete.template_variables:type_name -> provisioner.TemplateVariable - 47, // 23: provisioner.ParseComplete.workspace_tags:type_name -> provisioner.ParseComplete.WorkspaceTagsEntry - 32, // 24: provisioner.PlanRequest.metadata:type_name -> provisioner.Metadata - 10, // 25: provisioner.PlanRequest.rich_parameter_values:type_name -> provisioner.RichParameterValue - 14, // 26: provisioner.PlanRequest.variable_values:type_name -> provisioner.VariableValue - 18, // 27: provisioner.PlanRequest.external_auth_providers:type_name -> provisioner.ExternalAuthProvider - 10, // 28: provisioner.PlanRequest.previous_parameter_values:type_name -> provisioner.RichParameterValue - 29, // 29: provisioner.PlanComplete.resources:type_name -> provisioner.Resource - 9, // 30: provisioner.PlanComplete.parameters:type_name -> provisioner.RichParameter - 17, // 31: provisioner.PlanComplete.external_auth_providers:type_name -> provisioner.ExternalAuthProviderResource - 40, // 32: provisioner.PlanComplete.timings:type_name -> provisioner.Timing - 30, // 33: provisioner.PlanComplete.modules:type_name -> provisioner.Module - 12, // 34: provisioner.PlanComplete.presets:type_name -> provisioner.Preset - 32, // 35: provisioner.ApplyRequest.metadata:type_name -> provisioner.Metadata - 29, // 36: provisioner.ApplyComplete.resources:type_name -> provisioner.Resource - 9, // 37: provisioner.ApplyComplete.parameters:type_name -> provisioner.RichParameter - 17, // 38: provisioner.ApplyComplete.external_auth_providers:type_name -> provisioner.ExternalAuthProviderResource - 40, // 39: provisioner.ApplyComplete.timings:type_name -> provisioner.Timing - 48, // 40: provisioner.Timing.start:type_name -> google.protobuf.Timestamp - 48, // 41: provisioner.Timing.end:type_name -> google.protobuf.Timestamp - 5, // 42: provisioner.Timing.state:type_name -> provisioner.TimingState - 33, // 43: provisioner.Request.config:type_name -> provisioner.Config - 34, // 44: provisioner.Request.parse:type_name -> provisioner.ParseRequest - 36, // 45: provisioner.Request.plan:type_name -> provisioner.PlanRequest - 38, // 46: provisioner.Request.apply:type_name -> provisioner.ApplyRequest - 41, // 47: provisioner.Request.cancel:type_name -> provisioner.CancelRequest - 15, // 48: provisioner.Response.log:type_name -> provisioner.Log - 35, // 49: provisioner.Response.parse:type_name -> provisioner.ParseComplete - 37, // 50: provisioner.Response.plan:type_name -> provisioner.PlanComplete - 39, // 51: provisioner.Response.apply:type_name -> provisioner.ApplyComplete - 42, // 52: provisioner.Provisioner.Session:input_type -> provisioner.Request - 43, // 53: provisioner.Provisioner.Session:output_type -> provisioner.Response - 53, // [53:54] is the sub-list for method output_type - 52, // [52:53] is the sub-list for method input_type - 52, // [52:52] is the sub-list for extension type_name - 52, // [52:52] is the sub-list for extension extendee - 0, // [0:52] is the sub-list for field type_name + 32, // 22: provisioner.Metadata.running_agent_auth_tokens:type_name -> provisioner.RunningAgentAuthToken + 7, // 23: provisioner.ParseComplete.template_variables:type_name -> provisioner.TemplateVariable + 48, // 24: provisioner.ParseComplete.workspace_tags:type_name -> provisioner.ParseComplete.WorkspaceTagsEntry + 33, // 25: provisioner.PlanRequest.metadata:type_name -> provisioner.Metadata + 10, // 26: provisioner.PlanRequest.rich_parameter_values:type_name -> provisioner.RichParameterValue + 14, // 27: provisioner.PlanRequest.variable_values:type_name -> provisioner.VariableValue + 18, // 28: provisioner.PlanRequest.external_auth_providers:type_name -> provisioner.ExternalAuthProvider + 10, // 29: provisioner.PlanRequest.previous_parameter_values:type_name -> provisioner.RichParameterValue + 29, // 30: provisioner.PlanComplete.resources:type_name -> provisioner.Resource + 9, // 31: provisioner.PlanComplete.parameters:type_name -> provisioner.RichParameter + 17, // 32: provisioner.PlanComplete.external_auth_providers:type_name -> provisioner.ExternalAuthProviderResource + 41, // 33: provisioner.PlanComplete.timings:type_name -> provisioner.Timing + 30, // 34: provisioner.PlanComplete.modules:type_name -> provisioner.Module + 12, // 35: provisioner.PlanComplete.presets:type_name -> provisioner.Preset + 33, // 36: provisioner.ApplyRequest.metadata:type_name -> provisioner.Metadata + 29, // 37: provisioner.ApplyComplete.resources:type_name -> provisioner.Resource + 9, // 38: provisioner.ApplyComplete.parameters:type_name -> provisioner.RichParameter + 17, // 39: provisioner.ApplyComplete.external_auth_providers:type_name -> provisioner.ExternalAuthProviderResource + 41, // 40: provisioner.ApplyComplete.timings:type_name -> provisioner.Timing + 49, // 41: provisioner.Timing.start:type_name -> google.protobuf.Timestamp + 49, // 42: provisioner.Timing.end:type_name -> google.protobuf.Timestamp + 5, // 43: provisioner.Timing.state:type_name -> provisioner.TimingState + 34, // 44: provisioner.Request.config:type_name -> provisioner.Config + 35, // 45: provisioner.Request.parse:type_name -> provisioner.ParseRequest + 37, // 46: provisioner.Request.plan:type_name -> provisioner.PlanRequest + 39, // 47: provisioner.Request.apply:type_name -> provisioner.ApplyRequest + 42, // 48: provisioner.Request.cancel:type_name -> provisioner.CancelRequest + 15, // 49: provisioner.Response.log:type_name -> provisioner.Log + 36, // 50: provisioner.Response.parse:type_name -> provisioner.ParseComplete + 38, // 51: provisioner.Response.plan:type_name -> provisioner.PlanComplete + 40, // 52: provisioner.Response.apply:type_name -> provisioner.ApplyComplete + 43, // 53: provisioner.Provisioner.Session:input_type -> provisioner.Request + 44, // 54: provisioner.Provisioner.Session:output_type -> provisioner.Response + 54, // [54:55] is the sub-list for method output_type + 53, // [53:54] is the sub-list for method input_type + 53, // [53:53] is the sub-list for extension type_name + 53, // [53:53] is the sub-list for extension extendee + 0, // [0:53] is the sub-list for field type_name } func init() { file_provisionersdk_proto_provisioner_proto_init() } @@ -4585,7 +4649,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.(*Metadata); i { + switch v := v.(*RunningAgentAuthToken); i { case 0: return &v.state case 1: @@ -4597,7 +4661,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.(*Config); i { + switch v := v.(*Metadata); i { case 0: return &v.state case 1: @@ -4609,7 +4673,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.(*ParseRequest); i { + switch v := v.(*Config); i { case 0: return &v.state case 1: @@ -4621,7 +4685,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.(*ParseComplete); i { + switch v := v.(*ParseRequest); i { case 0: return &v.state case 1: @@ -4633,7 +4697,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PlanRequest); i { + switch v := v.(*ParseComplete); i { case 0: return &v.state case 1: @@ -4645,7 +4709,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PlanComplete); i { + switch v := v.(*PlanRequest); i { case 0: return &v.state case 1: @@ -4657,7 +4721,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ApplyRequest); i { + switch v := v.(*PlanComplete); i { case 0: return &v.state case 1: @@ -4669,7 +4733,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[33].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ApplyComplete); i { + switch v := v.(*ApplyRequest); i { case 0: return &v.state case 1: @@ -4681,7 +4745,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[34].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Timing); i { + switch v := v.(*ApplyComplete); i { case 0: return &v.state case 1: @@ -4693,7 +4757,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[35].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CancelRequest); i { + switch v := v.(*Timing); i { case 0: return &v.state case 1: @@ -4705,7 +4769,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[36].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Request); i { + switch v := v.(*CancelRequest); i { case 0: return &v.state case 1: @@ -4717,7 +4781,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[37].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Response); i { + switch v := v.(*Request); i { case 0: return &v.state case 1: @@ -4729,6 +4793,18 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[38].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[39].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Agent_Metadata); i { case 0: return &v.state @@ -4740,7 +4816,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[40].Exporter = func(v interface{}, i int) interface{} { + file_provisionersdk_proto_provisioner_proto_msgTypes[41].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Resource_Metadata); i { case 0: return &v.state @@ -4758,14 +4834,14 @@ func file_provisionersdk_proto_provisioner_proto_init() { (*Agent_Token)(nil), (*Agent_InstanceId)(nil), } - file_provisionersdk_proto_provisioner_proto_msgTypes[36].OneofWrappers = []interface{}{ + file_provisionersdk_proto_provisioner_proto_msgTypes[37].OneofWrappers = []interface{}{ (*Request_Config)(nil), (*Request_Parse)(nil), (*Request_Plan)(nil), (*Request_Apply)(nil), (*Request_Cancel)(nil), } - file_provisionersdk_proto_provisioner_proto_msgTypes[37].OneofWrappers = []interface{}{ + file_provisionersdk_proto_provisioner_proto_msgTypes[38].OneofWrappers = []interface{}{ (*Response_Log)(nil), (*Response_Parse)(nil), (*Response_Plan)(nil), @@ -4777,7 +4853,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_provisionersdk_proto_provisioner_proto_rawDesc, NumEnums: 6, - NumMessages: 42, + NumMessages: 43, NumExtensions: 0, NumServices: 1, }, diff --git a/provisionersdk/proto/provisioner.proto b/provisionersdk/proto/provisioner.proto index 0cf22efec03bb..9abcd9e900435 100644 --- a/provisionersdk/proto/provisioner.proto +++ b/provisionersdk/proto/provisioner.proto @@ -273,6 +273,10 @@ message Role { string org_id = 2; } +message RunningAgentAuthToken { + string agent_id = 1; + string token = 2; +} enum PrebuiltWorkspaceBuildStage { NONE = 0; // Default value for builds unrelated to prebuilds. CREATE = 1; // A prebuilt workspace is being provisioned. @@ -301,7 +305,7 @@ message Metadata { string workspace_owner_login_type = 18; repeated Role workspace_owner_rbac_roles = 19; PrebuiltWorkspaceBuildStage prebuilt_workspace_build_stage = 20; // Indicates that a prebuilt workspace is being built. - string running_workspace_agent_token = 21; // Preserves the running agent token of a prebuilt workspace so it can reinitialize. + repeated RunningAgentAuthToken running_agent_auth_tokens = 21; } // Config represents execution configuration shared by all subsequent requests in the Session diff --git a/site/e2e/provisionerGenerated.ts b/site/e2e/provisionerGenerated.ts index cee6d5876410b..68cd48576349d 100644 --- a/site/e2e/provisionerGenerated.ts +++ b/site/e2e/provisionerGenerated.ts @@ -297,6 +297,11 @@ export interface Role { orgId: string; } +export interface RunningAgentAuthToken { + agentId: string; + token: string; +} + /** Metadata is information about a workspace used in the execution of a build */ export interface Metadata { coderUrl: string; @@ -320,8 +325,7 @@ export interface Metadata { workspaceOwnerRbacRoles: Role[]; /** Indicates that a prebuilt workspace is being built. */ prebuiltWorkspaceBuildStage: PrebuiltWorkspaceBuildStage; - /** Preserves the running agent token of a prebuilt workspace so it can reinitialize. */ - runningWorkspaceAgentToken: string; + runningAgentAuthTokens: RunningAgentAuthToken[]; } /** Config represents execution configuration shared by all subsequent requests in the Session */ @@ -986,6 +990,18 @@ export const Role = { }, }; +export const RunningAgentAuthToken = { + encode(message: RunningAgentAuthToken, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.agentId !== "") { + writer.uint32(10).string(message.agentId); + } + if (message.token !== "") { + writer.uint32(18).string(message.token); + } + return writer; + }, +}; + export const Metadata = { encode(message: Metadata, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { if (message.coderUrl !== "") { @@ -1048,8 +1064,8 @@ export const Metadata = { if (message.prebuiltWorkspaceBuildStage !== 0) { writer.uint32(160).int32(message.prebuiltWorkspaceBuildStage); } - if (message.runningWorkspaceAgentToken !== "") { - writer.uint32(170).string(message.runningWorkspaceAgentToken); + for (const v of message.runningAgentAuthTokens) { + RunningAgentAuthToken.encode(v!, writer.uint32(170).fork()).ldelim(); } return writer; }, From c7bc4047ba0c8c27f1300887606021106cdbd208 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 14 May 2025 09:35:21 -0300 Subject: [PATCH 38/88] chore: replace MUI LoadingButton with Button + Spinner - 1 (#17816) --- .../LicensesSettingsPageView.tsx | 21 ++++++----- .../NotificationsPage/Troubleshooting.tsx | 15 ++++---- .../EditOAuth2AppPageView.tsx | 10 +++--- .../OAuth2AppsSettingsPage/OAuth2AppForm.tsx | 8 +++-- site/src/pages/GroupsPage/GroupPage.tsx | 35 ++++++++----------- 5 files changed, 44 insertions(+), 45 deletions(-) diff --git a/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicensesSettingsPageView.tsx b/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicensesSettingsPageView.tsx index a7d39d8536c62..bedf3f6de3b4d 100644 --- a/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicensesSettingsPageView.tsx +++ b/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicensesSettingsPageView.tsx @@ -1,17 +1,18 @@ import { type Interpolation, type Theme, useTheme } from "@emotion/react"; import AddIcon from "@mui/icons-material/AddOutlined"; -import LoadingButton from "@mui/lab/LoadingButton"; -import Button from "@mui/material/Button"; +import MuiButton from "@mui/material/Button"; import MuiLink from "@mui/material/Link"; import Skeleton from "@mui/material/Skeleton"; import Tooltip from "@mui/material/Tooltip"; import type { GetLicensesResponse } from "api/api"; import type { UserStatusChangeCount } from "api/typesGenerated"; +import { Button } from "components/Button/Button"; import { SettingsHeader, SettingsHeaderDescription, SettingsHeaderTitle, } from "components/SettingsHeader/SettingsHeader"; +import { Spinner } from "components/Spinner/Spinner"; import { Stack } from "components/Stack/Stack"; import { useWindowSize } from "hooks/useWindowSize"; import { RotateCwIcon } from "lucide-react"; @@ -72,22 +73,24 @@ const LicensesSettingsPageView: FC = ({ - + - } + variant="outline" > + + + Refresh - + diff --git a/site/src/pages/DeploymentSettingsPage/NotificationsPage/Troubleshooting.tsx b/site/src/pages/DeploymentSettingsPage/NotificationsPage/Troubleshooting.tsx index c9a4362427cf7..19c4892b49e8a 100644 --- a/site/src/pages/DeploymentSettingsPage/NotificationsPage/Troubleshooting.tsx +++ b/site/src/pages/DeploymentSettingsPage/NotificationsPage/Troubleshooting.tsx @@ -1,7 +1,8 @@ import { useTheme } from "@emotion/react"; -import LoadingButton from "@mui/lab/LoadingButton"; import { API } from "api/api"; +import { Button } from "components/Button/Button"; import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"; +import { Spinner } from "components/Spinner/Spinner"; import type { FC } from "react"; import { useMutation } from "react-query"; @@ -29,17 +30,17 @@ export const Troubleshooting: FC = () => {
- { sendTestNotificationApi(); }} > + Send notification - +
diff --git a/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/EditOAuth2AppPageView.tsx b/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/EditOAuth2AppPageView.tsx index 1656c1cd2ae19..0b837673409bb 100644 --- a/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/EditOAuth2AppPageView.tsx +++ b/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/EditOAuth2AppPageView.tsx @@ -1,6 +1,5 @@ import { type Interpolation, type Theme, useTheme } from "@emotion/react"; import CopyIcon from "@mui/icons-material/FileCopyOutlined"; -import LoadingButton from "@mui/lab/LoadingButton"; import Divider from "@mui/material/Divider"; import Table from "@mui/material/Table"; import TableBody from "@mui/material/TableBody"; @@ -22,6 +21,7 @@ import { SettingsHeaderDescription, SettingsHeaderTitle, } from "components/SettingsHeader/SettingsHeader"; +import { Spinner } from "components/Spinner/Spinner"; import { Stack } from "components/Stack/Stack"; import { TableLoader } from "components/TableLoader/TableLoader"; import { ChevronLeftIcon } from "lucide-react"; @@ -224,14 +224,14 @@ const OAuth2AppSecretsTable: FC = ({ justifyContent="space-between" >

Client secrets

- + Generate secret - + diff --git a/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/OAuth2AppForm.tsx b/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/OAuth2AppForm.tsx index 6f31a3f94988e..0bb08327f5fa3 100644 --- a/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/OAuth2AppForm.tsx +++ b/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/OAuth2AppForm.tsx @@ -1,7 +1,8 @@ -import LoadingButton from "@mui/lab/LoadingButton"; import TextField from "@mui/material/TextField"; import { isApiValidationError, mapApiErrorToFieldErrors } from "api/errors"; import type * as TypesGen from "api/typesGenerated"; +import { Button } from "components/Button/Button"; +import { Spinner } from "components/Spinner/Spinner"; import { Stack } from "components/Stack/Stack"; import type { FC, ReactNode } from "react"; @@ -76,9 +77,10 @@ export const OAuth2AppForm: FC = ({ /> - + {actions} diff --git a/site/src/pages/GroupsPage/GroupPage.tsx b/site/src/pages/GroupsPage/GroupPage.tsx index 60161a5ee7ec0..27c63fa96cc88 100644 --- a/site/src/pages/GroupsPage/GroupPage.tsx +++ b/site/src/pages/GroupsPage/GroupPage.tsx @@ -1,7 +1,6 @@ import type { Interpolation, Theme } from "@emotion/react"; import PersonAdd from "@mui/icons-material/PersonAdd"; -import LoadingButton from "@mui/lab/LoadingButton"; -import Button from "@mui/material/Button"; +import MuiButton from "@mui/material/Button"; import { getErrorMessage } from "api/errors"; import { addMember, @@ -18,7 +17,7 @@ import type { import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Avatar } from "components/Avatar/Avatar"; import { AvatarData } from "components/Avatar/AvatarData"; -import { Button as ShadcnButton } from "components/Button/Button"; +import { Button } from "components/Button/Button"; import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog"; import { DropdownMenu, @@ -35,6 +34,7 @@ import { SettingsHeaderDescription, SettingsHeaderTitle, } from "components/SettingsHeader/SettingsHeader"; +import { Spinner } from "components/Spinner/Spinner"; import { Stack } from "components/Stack/Stack"; import { Table, @@ -121,14 +121,14 @@ const GroupPage: FC = () => { {canUpdateGroup && ( - - + )} @@ -279,15 +279,12 @@ const AddGroupMember: FC = ({ }} /> - } - loading={isLoading} - > + ); @@ -332,14 +329,10 @@ const GroupMemberRow: FC = ({ {canUpdate && ( - + Date: Wed, 14 May 2025 09:37:01 -0300 Subject: [PATCH 39/88] chore: replace MUI LoadingButton with Button + Spinner - 2 (#17817) --- .../modules/resources/PortForwardButton.tsx | 31 +++++++++---------- .../pages/HealthPage/DismissWarningButton.tsx | 29 +++++++++-------- .../pages/LoginPage/PasswordSignInForm.tsx | 14 +++++---- .../OrganizationMembersPageView.tsx | 15 ++++----- .../ResetPasswordPage/ChangePasswordPage.tsx | 21 +++++++------ 5 files changed, 57 insertions(+), 53 deletions(-) diff --git a/site/src/modules/resources/PortForwardButton.tsx b/site/src/modules/resources/PortForwardButton.tsx index 437adf881e745..a4b8ee8277ebc 100644 --- a/site/src/modules/resources/PortForwardButton.tsx +++ b/site/src/modules/resources/PortForwardButton.tsx @@ -2,8 +2,7 @@ import { type Interpolation, type Theme, useTheme } from "@emotion/react"; import LockIcon from "@mui/icons-material/Lock"; import LockOpenIcon from "@mui/icons-material/LockOpen"; import SensorsIcon from "@mui/icons-material/Sensors"; -import LoadingButton from "@mui/lab/LoadingButton"; -import Button from "@mui/material/Button"; +import MUIButton from "@mui/material/Button"; import CircularProgress from "@mui/material/CircularProgress"; import FormControl from "@mui/material/FormControl"; import Link from "@mui/material/Link"; @@ -27,11 +26,13 @@ import { type WorkspaceAgentPortShareProtocol, WorkspaceAppSharingLevels, } from "api/typesGenerated"; +import { Button } from "components/Button/Button"; import { HelpTooltipLink, HelpTooltipText, HelpTooltipTitle, } from "components/HelpTooltip/HelpTooltip"; +import { Spinner } from "components/Spinner/Spinner"; import { Popover, PopoverContent, @@ -76,7 +77,7 @@ export const PortForwardButton: FC = (props) => { return ( - + = ({ required css={styles.newPortInput} /> - + @@ -368,7 +369,7 @@ export const PortForwardPopoverView: FC = ({ alignItems="center" > {canSharePorts && ( - + )} @@ -482,7 +483,7 @@ export const PortForwardPopoverView: FC = ({ )} - + ); @@ -550,14 +551,10 @@ export const PortForwardPopoverView: FC = ({ disabledPublicMenuItem )} - +
diff --git a/site/src/pages/HealthPage/DismissWarningButton.tsx b/site/src/pages/HealthPage/DismissWarningButton.tsx index b61aea85095f1..27184d427edb0 100644 --- a/site/src/pages/HealthPage/DismissWarningButton.tsx +++ b/site/src/pages/HealthPage/DismissWarningButton.tsx @@ -1,10 +1,11 @@ import NotificationsOffOutlined from "@mui/icons-material/NotificationsOffOutlined"; import NotificationOutlined from "@mui/icons-material/NotificationsOutlined"; -import LoadingButton from "@mui/lab/LoadingButton"; import Skeleton from "@mui/material/Skeleton"; import { healthSettings, updateHealthSettings } from "api/queries/debug"; import type { HealthSection } from "api/typesGenerated"; +import { Button } from "components/Button/Button"; import { displaySuccess } from "components/GlobalSnackbar/utils"; +import { Spinner } from "components/Spinner/Spinner"; import { useMutation, useQuery, useQueryClient } from "react-query"; export const DismissWarningButton = (props: { healthcheck: HealthSection }) => { @@ -33,11 +34,9 @@ export const DismissWarningButton = (props: { healthcheck: HealthSection }) => { if (isDismissed) { return ( - } + ); } return ( - } + ); }; diff --git a/site/src/pages/LoginPage/PasswordSignInForm.tsx b/site/src/pages/LoginPage/PasswordSignInForm.tsx index de61c3de6982a..34c753e67bb18 100644 --- a/site/src/pages/LoginPage/PasswordSignInForm.tsx +++ b/site/src/pages/LoginPage/PasswordSignInForm.tsx @@ -1,6 +1,7 @@ -import LoadingButton from "@mui/lab/LoadingButton"; import Link from "@mui/material/Link"; import TextField from "@mui/material/TextField"; +import { Button } from "components/Button/Button"; +import { Spinner } from "components/Spinner/Spinner"; import { Stack } from "components/Stack/Stack"; import { useFormik } from "formik"; import type { FC } from "react"; @@ -59,14 +60,15 @@ export const PasswordSignInForm: FC = ({ label={Language.passwordLabel} type="password" /> - + {Language.passwordSignIn} - + = ({ }} /> - } - loading={isLoading} + variant="outline" > + + + Add user - + ); diff --git a/site/src/pages/ResetPasswordPage/ChangePasswordPage.tsx b/site/src/pages/ResetPasswordPage/ChangePasswordPage.tsx index a05fea8cc7761..e2a8c8206e713 100644 --- a/site/src/pages/ResetPasswordPage/ChangePasswordPage.tsx +++ b/site/src/pages/ResetPasswordPage/ChangePasswordPage.tsx @@ -1,12 +1,13 @@ import type { Interpolation, Theme } from "@emotion/react"; -import LoadingButton from "@mui/lab/LoadingButton"; -import Button from "@mui/material/Button"; +import MUIButton from "@mui/material/Button"; import TextField from "@mui/material/TextField"; import { isApiValidationError } from "api/errors"; import { changePasswordWithOTP } from "api/queries/users"; import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { Button } from "components/Button/Button"; import { CustomLogo } from "components/CustomLogo/CustomLogo"; import { displaySuccess } from "components/GlobalSnackbar/utils"; +import { Spinner } from "components/Spinner/Spinner"; import { Stack } from "components/Stack/Stack"; import { useFormik } from "formik"; import type { FC } from "react"; @@ -115,16 +116,16 @@ const ChangePasswordPage: FC = ({ redirect }) => { /> - + Reset password - - + From 6e967780c960d71e1eb3e701af7b71c99e82f8d8 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 14 May 2025 14:52:22 +0200 Subject: [PATCH 40/88] feat: track resource replacements when claiming a prebuilt workspace (#17571) Closes https://github.com/coder/internal/issues/369 We can't know whether a replacement (i.e. drift of terraform state leading to a resource needing to be deleted/recreated) will take place apriori; we can only detect it at `plan` time, because the provider decides whether a resource must be replaced and it cannot be inferred through static analysis of the template. **This is likely to be the most common gotcha with using prebuilds, since it requires a slight template modification to use prebuilds effectively**, so let's head this off before it's an issue for customers. Drift details will now be logged in the workspace build logs: ![image](https://github.com/user-attachments/assets/da1988b6-2cbe-4a79-a3c5-ea29891f3d6f) Plus a notification will be sent to template admins when this situation arises: ![image](https://github.com/user-attachments/assets/39d555b1-a262-4a3e-b529-03b9f23bf66a) A new metric - `coderd_prebuilt_workspaces_resource_replacements_total` - will also increment each time a workspace encounters replacements. We only track _that_ a resource replacement occurred, not how many. Just one is enough to ruin a prebuild, but we can't know apriori which replacement would cause this. For example, say we have 2 replacements: a `docker_container` and a `null_resource`; we don't know which one might cause an issue (or indeed if either would), so we just track the replacement. --------- Signed-off-by: Danny Kopping --- cli/server.go | 62 +- coderd/coderd.go | 4 +- ...esource_replacements_notification.down.sql | 1 + ..._resource_replacements_notification.up.sql | 34 + coderd/notifications/events.go | 1 + coderd/notifications/notifications_test.go | 28 +- .../notificationstest/fake_enqueuer.go | 7 + ...plateWorkspaceResourceReplaced.html.golden | 131 ++ ...plateWorkspaceResourceReplaced.json.golden | 42 + coderd/prebuilds/api.go | 6 + coderd/prebuilds/noop.go | 7 +- .../provisionerdserver/provisionerdserver.go | 16 +- .../provisionerdserver_test.go | 120 +- .../prebuilt-workspaces.md | 6 + enterprise/coderd/coderd.go | 2 +- enterprise/coderd/prebuilds/claim_test.go | 2 +- .../coderd/prebuilds/metricscollector.go | 76 +- .../coderd/prebuilds/metricscollector_test.go | 84 +- enterprise/coderd/prebuilds/reconcile.go | 128 ++ enterprise/coderd/prebuilds/reconcile_test.go | 137 +- enterprise/coderd/provisionerdaemons.go | 4 +- provisioner/terraform/executor.go | 175 ++- provisioner/terraform/provision.go | 5 +- .../terraform/resource_replacements.go | 86 ++ .../resource_replacements_internal_test.go | 176 +++ provisionerd/proto/provisionerd.pb.go | 361 ++--- provisionerd/proto/provisionerd.proto | 1 + provisionerd/proto/version.go | 1 + provisionerd/runner/runner.go | 2 + provisionersdk/proto/provisioner.pb.go | 1283 +++++++++-------- provisionersdk/proto/provisioner.proto | 6 + site/e2e/helpers.ts | 2 + site/e2e/provisionerGenerated.ts | 21 + 33 files changed, 2048 insertions(+), 969 deletions(-) create mode 100644 coderd/database/migrations/000324_resource_replacements_notification.down.sql create mode 100644 coderd/database/migrations/000324_resource_replacements_notification.up.sql create mode 100644 coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceResourceReplaced.html.golden create mode 100644 coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceResourceReplaced.json.golden create mode 100644 provisioner/terraform/resource_replacements.go create mode 100644 provisioner/terraform/resource_replacements_internal_test.go diff --git a/cli/server.go b/cli/server.go index d32ed51c06007..c5532e07e7a81 100644 --- a/cli/server.go +++ b/cli/server.go @@ -928,6 +928,37 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. options.StatsBatcher = batcher defer closeBatcher() + // Manage notifications. + var ( + notificationsCfg = options.DeploymentValues.Notifications + notificationsManager *notifications.Manager + ) + + metrics := notifications.NewMetrics(options.PrometheusRegistry) + helpers := templateHelpers(options) + + // The enqueuer is responsible for enqueueing notifications to the given store. + enqueuer, err := notifications.NewStoreEnqueuer(notificationsCfg, options.Database, helpers, logger.Named("notifications.enqueuer"), quartz.NewReal()) + if err != nil { + return xerrors.Errorf("failed to instantiate notification store enqueuer: %w", err) + } + options.NotificationsEnqueuer = enqueuer + + // The notification manager is responsible for: + // - creating notifiers and managing their lifecycles (notifiers are responsible for dequeueing/sending notifications) + // - keeping the store updated with status updates + notificationsManager, err = notifications.NewManager(notificationsCfg, options.Database, options.Pubsub, helpers, metrics, logger.Named("notifications.manager")) + if err != nil { + return xerrors.Errorf("failed to instantiate notification manager: %w", err) + } + + // nolint:gocritic // We need to run the manager in a notifier context. + notificationsManager.Run(dbauthz.AsNotifier(ctx)) + + // Run report generator to distribute periodic reports. + notificationReportGenerator := reports.NewReportGenerator(ctx, logger.Named("notifications.report_generator"), options.Database, options.NotificationsEnqueuer, quartz.NewReal()) + defer notificationReportGenerator.Close() + // We use a separate coderAPICloser so the Enterprise API // can have its own close functions. This is cleaner // than abstracting the Coder API itself. @@ -975,37 +1006,6 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. return xerrors.Errorf("write config url: %w", err) } - // Manage notifications. - var ( - notificationsCfg = options.DeploymentValues.Notifications - notificationsManager *notifications.Manager - ) - - metrics := notifications.NewMetrics(options.PrometheusRegistry) - helpers := templateHelpers(options) - - // The enqueuer is responsible for enqueueing notifications to the given store. - enqueuer, err := notifications.NewStoreEnqueuer(notificationsCfg, options.Database, helpers, logger.Named("notifications.enqueuer"), quartz.NewReal()) - if err != nil { - return xerrors.Errorf("failed to instantiate notification store enqueuer: %w", err) - } - options.NotificationsEnqueuer = enqueuer - - // The notification manager is responsible for: - // - creating notifiers and managing their lifecycles (notifiers are responsible for dequeueing/sending notifications) - // - keeping the store updated with status updates - notificationsManager, err = notifications.NewManager(notificationsCfg, options.Database, options.Pubsub, helpers, metrics, logger.Named("notifications.manager")) - if err != nil { - return xerrors.Errorf("failed to instantiate notification manager: %w", err) - } - - // nolint:gocritic // We need to run the manager in a notifier context. - notificationsManager.Run(dbauthz.AsNotifier(ctx)) - - // Run report generator to distribute periodic reports. - notificationReportGenerator := reports.NewReportGenerator(ctx, logger.Named("notifications.report_generator"), options.Database, options.NotificationsEnqueuer, quartz.NewReal()) - defer notificationReportGenerator.Close() - // Since errCh only has one buffered slot, all routines // sending on it must be wrapped in a select/default to // avoid leaving dangling goroutines waiting for the diff --git a/coderd/coderd.go b/coderd/coderd.go index b41e0070f6ecc..98ae7a8ede413 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -40,10 +40,11 @@ import ( "tailscale.com/util/singleflight" "cdr.dev/slog" - "github.com/coder/coder/v2/codersdk/drpcsdk" "github.com/coder/quartz" "github.com/coder/serpent" + "github.com/coder/coder/v2/codersdk/drpcsdk" + "github.com/coder/coder/v2/coderd/ai" "github.com/coder/coder/v2/coderd/cryptokeys" "github.com/coder/coder/v2/coderd/entitlements" @@ -1795,6 +1796,7 @@ func (api *API) CreateInMemoryTaggedProvisionerDaemon(dialCtx context.Context, n Clock: api.Clock, }, api.NotificationsEnqueuer, + &api.PrebuildsReconciler, ) if err != nil { return nil, err diff --git a/coderd/database/migrations/000324_resource_replacements_notification.down.sql b/coderd/database/migrations/000324_resource_replacements_notification.down.sql new file mode 100644 index 0000000000000..8da13f718b635 --- /dev/null +++ b/coderd/database/migrations/000324_resource_replacements_notification.down.sql @@ -0,0 +1 @@ +DELETE FROM notification_templates WHERE id = '89d9745a-816e-4695-a17f-3d0a229e2b8d'; diff --git a/coderd/database/migrations/000324_resource_replacements_notification.up.sql b/coderd/database/migrations/000324_resource_replacements_notification.up.sql new file mode 100644 index 0000000000000..395332adaee20 --- /dev/null +++ b/coderd/database/migrations/000324_resource_replacements_notification.up.sql @@ -0,0 +1,34 @@ +INSERT INTO notification_templates + (id, name, title_template, body_template, "group", actions) +VALUES ('89d9745a-816e-4695-a17f-3d0a229e2b8d', + 'Prebuilt Workspace Resource Replaced', + E'There might be a problem with a recently claimed prebuilt workspace', + $$ +Workspace **{{.Labels.workspace}}** was claimed from a prebuilt workspace by **{{.Labels.claimant}}**. + +During the claim, Terraform destroyed and recreated the following resources +because one or more immutable attributes changed: + +{{range $resource, $paths := .Data.replacements -}} +- _{{ $resource }}_ was replaced due to changes to _{{ $paths }}_ +{{end}} + +When Terraform must change an immutable attribute, it replaces the entire resource. +If you’re using prebuilds to speed up provisioning, unexpected replacements will slow down +workspace startup—even when claiming a prebuilt environment. + +For tips on preventing replacements and improving claim performance, see [this guide](https://coder.com/docs/admin/templates/extending-templates/prebuilt-workspaces#preventing-resource-replacement). + +NOTE: this prebuilt workspace used the **{{.Labels.preset}}** preset. +$$, + 'Template Events', + '[ + { + "label": "View workspace build", + "url": "{{base_url}}/@{{.Labels.claimant}}/{{.Labels.workspace}}/builds/{{.Labels.workspace_build_num}}" + }, + { + "label": "View template version", + "url": "{{base_url}}/templates/{{.Labels.org}}/{{.Labels.template}}/versions/{{.Labels.template_version}}" + } + ]'::jsonb); diff --git a/coderd/notifications/events.go b/coderd/notifications/events.go index 2f45205bf33ec..35d9925055da5 100644 --- a/coderd/notifications/events.go +++ b/coderd/notifications/events.go @@ -39,6 +39,7 @@ var ( TemplateTemplateDeprecated = uuid.MustParse("f40fae84-55a2-42cd-99fa-b41c1ca64894") TemplateWorkspaceBuildsFailedReport = uuid.MustParse("34a20db2-e9cc-4a93-b0e4-8569699d7a00") + TemplateWorkspaceResourceReplaced = uuid.MustParse("89d9745a-816e-4695-a17f-3d0a229e2b8d") ) // Notification-related events. diff --git a/coderd/notifications/notifications_test.go b/coderd/notifications/notifications_test.go index 12372b74a14c3..8f8a3c82441e0 100644 --- a/coderd/notifications/notifications_test.go +++ b/coderd/notifications/notifications_test.go @@ -35,6 +35,9 @@ import ( "golang.org/x/xerrors" "cdr.dev/slog" + "github.com/coder/quartz" + "github.com/coder/serpent" + "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbauthz" @@ -48,8 +51,6 @@ import ( "github.com/coder/coder/v2/coderd/util/syncmap" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/testutil" - "github.com/coder/quartz" - "github.com/coder/serpent" ) // updateGoldenFiles is a flag that can be set to update golden files. @@ -1226,6 +1227,29 @@ func TestNotificationTemplates_Golden(t *testing.T) { Labels: map[string]string{}, }, }, + { + name: "TemplateWorkspaceResourceReplaced", + id: notifications.TemplateWorkspaceResourceReplaced, + payload: types.MessagePayload{ + UserName: "Bobby", + UserEmail: "bobby@coder.com", + UserUsername: "bobby", + Labels: map[string]string{ + "org": "cern", + "workspace": "my-workspace", + "workspace_build_num": "2", + "template": "docker", + "template_version": "angry_torvalds", + "preset": "particle-accelerator", + "claimant": "prebuilds-claimer", + }, + Data: map[string]any{ + "replacements": map[string]string{ + "docker_container[0]": "env, hostname", + }, + }, + }, + }, } // We must have a test case for every notification_template. This is enforced below: diff --git a/coderd/notifications/notificationstest/fake_enqueuer.go b/coderd/notifications/notificationstest/fake_enqueuer.go index 8fbc2cee25806..568091818295c 100644 --- a/coderd/notifications/notificationstest/fake_enqueuer.go +++ b/coderd/notifications/notificationstest/fake_enqueuer.go @@ -9,6 +9,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/coder/coder/v2/coderd/database/dbauthz" + "github.com/coder/coder/v2/coderd/notifications" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" ) @@ -19,6 +20,12 @@ type FakeEnqueuer struct { sent []*FakeNotification } +var _ notifications.Enqueuer = &FakeEnqueuer{} + +func NewFakeEnqueuer() *FakeEnqueuer { + return &FakeEnqueuer{} +} + type FakeNotification struct { UserID, TemplateID uuid.UUID Labels map[string]string diff --git a/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceResourceReplaced.html.golden b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceResourceReplaced.html.golden new file mode 100644 index 0000000000000..6d64eed0249a7 --- /dev/null +++ b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceResourceReplaced.html.golden @@ -0,0 +1,131 @@ +From: system@coder.com +To: bobby@coder.com +Subject: There might be a problem with a recently claimed prebuilt workspace +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, + +Workspace my-workspace was claimed from a prebuilt workspace by prebuilds-c= +laimer. + +During the claim, Terraform destroyed and recreated the following resources +because one or more immutable attributes changed: + +docker_container[0] was replaced due to changes to env, hostname + +When Terraform must change an immutable attribute, it replaces the entire r= +esource. +If you=E2=80=99re using prebuilds to speed up provisioning, unexpected repl= +acements will slow down +workspace startup=E2=80=94even when claiming a prebuilt environment. + +For tips on preventing replacements and improving claim performance, see th= +is guide (https://coder.com/docs/admin/templates/extending-templates/prebui= +lt-workspaces#preventing-resource-replacement). + +NOTE: this prebuilt workspace used the particle-accelerator preset. + + +View workspace build: http://test.com/@prebuilds-claimer/my-workspace/build= +s/2 + +View template version: http://test.com/templates/cern/docker/versions/angry= +_torvalds + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/html; charset=UTF-8 + + + + + + + There might be a problem with a recently claimed prebuilt worksp= +ace + + +
+
+ 3D"Cod= +
+

+ There might be a problem with a recently claimed prebuilt workspace +

+
+

Hi Bobby,

+

Workspace my-workspace was claimed from a prebu= +ilt workspace by prebuilds-claimer.

+ +

During the claim, Terraform destroyed and recreated the following resour= +ces
+because one or more immutable attributes changed:

+ +
    +
  • _dockercontainer[0] was replaced due to changes to env, h= +ostname
    +
  • +
+ +

When Terraform must change an immutable attribute, it replaces the entir= +e resource.
+If you=E2=80=99re using prebuilds to speed up provisioning, unexpected repl= +acements will slow down
+workspace startup=E2=80=94even when claiming a prebuilt environment.

+ +

For tips on preventing replacements and improving claim performance, see= + this guide.

+ +

NOTE: this prebuilt workspace used the particle-accelerator preset.

+
+ + +
+ + + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4-- diff --git a/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceResourceReplaced.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceResourceReplaced.json.golden new file mode 100644 index 0000000000000..09bf9431cdeed --- /dev/null +++ b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceResourceReplaced.json.golden @@ -0,0 +1,42 @@ +{ + "_version": "1.1", + "msg_id": "00000000-0000-0000-0000-000000000000", + "payload": { + "_version": "1.2", + "notification_name": "Prebuilt Workspace Resource Replaced", + "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 build", + "url": "http://test.com/@prebuilds-claimer/my-workspace/builds/2" + }, + { + "label": "View template version", + "url": "http://test.com/templates/cern/docker/versions/angry_torvalds" + } + ], + "labels": { + "claimant": "prebuilds-claimer", + "org": "cern", + "preset": "particle-accelerator", + "template": "docker", + "template_version": "angry_torvalds", + "workspace": "my-workspace", + "workspace_build_num": "2" + }, + "data": { + "replacements": { + "docker_container[0]": "env, hostname" + } + }, + "targets": null + }, + "title": "There might be a problem with a recently claimed prebuilt workspace", + "title_markdown": "There might be a problem with a recently claimed prebuilt workspace", + "body": "Workspace my-workspace was claimed from a prebuilt workspace by prebuilds-claimer.\n\nDuring the claim, Terraform destroyed and recreated the following resources\nbecause one or more immutable attributes changed:\n\ndocker_container[0] was replaced due to changes to env, hostname\n\nWhen Terraform must change an immutable attribute, it replaces the entire resource.\nIf you’re using prebuilds to speed up provisioning, unexpected replacements will slow down\nworkspace startup—even when claiming a prebuilt environment.\n\nFor tips on preventing replacements and improving claim performance, see this guide (https://coder.com/docs/admin/templates/extending-templates/prebuilt-workspaces#preventing-resource-replacement).\n\nNOTE: this prebuilt workspace used the particle-accelerator preset.", + "body_markdown": "\nWorkspace **my-workspace** was claimed from a prebuilt workspace by **prebuilds-claimer**.\n\nDuring the claim, Terraform destroyed and recreated the following resources\nbecause one or more immutable attributes changed:\n\n- _docker_container[0]_ was replaced due to changes to _env, hostname_\n\n\nWhen Terraform must change an immutable attribute, it replaces the entire resource.\nIf you’re using prebuilds to speed up provisioning, unexpected replacements will slow down\nworkspace startup—even when claiming a prebuilt environment.\n\nFor tips on preventing replacements and improving claim performance, see [this guide](https://coder.com/docs/admin/templates/extending-templates/prebuilt-workspaces#preventing-resource-replacement).\n\nNOTE: this prebuilt workspace used the **particle-accelerator** preset.\n" +} \ No newline at end of file diff --git a/coderd/prebuilds/api.go b/coderd/prebuilds/api.go index 00129eae37491..3092d27421d26 100644 --- a/coderd/prebuilds/api.go +++ b/coderd/prebuilds/api.go @@ -7,6 +7,7 @@ import ( "golang.org/x/xerrors" "github.com/coder/coder/v2/coderd/database" + sdkproto "github.com/coder/coder/v2/provisionersdk/proto" ) var ( @@ -27,6 +28,11 @@ type ReconciliationOrchestrator interface { // Stop gracefully shuts down the orchestrator with the given cause. // The cause is used for logging and error reporting. Stop(ctx context.Context, cause error) + + // TrackResourceReplacement handles a pathological situation whereby a terraform resource is replaced due to drift, + // which can obviate the whole point of pre-provisioning a prebuilt workspace. + // See more detail at https://coder.com/docs/admin/templates/extending-templates/prebuilt-workspaces#preventing-resource-replacement. + TrackResourceReplacement(ctx context.Context, workspaceID, buildID uuid.UUID, replacements []*sdkproto.ResourceReplacement) } type Reconciler interface { diff --git a/coderd/prebuilds/noop.go b/coderd/prebuilds/noop.go index 6fb3f7c6a5f1f..3c2dd78a804db 100644 --- a/coderd/prebuilds/noop.go +++ b/coderd/prebuilds/noop.go @@ -6,12 +6,15 @@ import ( "github.com/google/uuid" "github.com/coder/coder/v2/coderd/database" + sdkproto "github.com/coder/coder/v2/provisionersdk/proto" ) type NoopReconciler struct{} -func (NoopReconciler) Run(context.Context) {} -func (NoopReconciler) Stop(context.Context, error) {} +func (NoopReconciler) Run(context.Context) {} +func (NoopReconciler) Stop(context.Context, error) {} +func (NoopReconciler) TrackResourceReplacement(context.Context, uuid.UUID, uuid.UUID, []*sdkproto.ResourceReplacement) { +} func (NoopReconciler) ReconcileAll(context.Context) error { return nil } func (NoopReconciler) SnapshotState(context.Context, database.Store) (*GlobalSnapshot, error) { return &GlobalSnapshot{}, nil diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index f8a33d3a55188..075f927650284 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -28,6 +28,7 @@ import ( protobuf "google.golang.org/protobuf/proto" "cdr.dev/slog" + "github.com/coder/coder/v2/codersdk/drpcsdk" "github.com/coder/quartz" @@ -116,6 +117,7 @@ type server struct { UserQuietHoursScheduleStore *atomic.Pointer[schedule.UserQuietHoursScheduleStore] DeploymentValues *codersdk.DeploymentValues NotificationsEnqueuer notifications.Enqueuer + PrebuildsOrchestrator *atomic.Pointer[prebuilds.ReconciliationOrchestrator] OIDCConfig promoauth.OAuth2Config @@ -151,8 +153,7 @@ func (t Tags) Valid() error { return nil } -func NewServer( - lifecycleCtx context.Context, +func NewServer(lifecycleCtx context.Context, accessURL *url.URL, id uuid.UUID, organizationID uuid.UUID, @@ -171,6 +172,7 @@ func NewServer( deploymentValues *codersdk.DeploymentValues, options Options, enqueuer notifications.Enqueuer, + prebuildsOrchestrator *atomic.Pointer[prebuilds.ReconciliationOrchestrator], ) (proto.DRPCProvisionerDaemonServer, error) { // Fail-fast if pointers are nil if lifecycleCtx == nil { @@ -235,6 +237,7 @@ func NewServer( acquireJobLongPollDur: options.AcquireJobLongPollDur, heartbeatInterval: options.HeartbeatInterval, heartbeatFn: options.HeartbeatFn, + PrebuildsOrchestrator: prebuildsOrchestrator, } if s.heartbeatFn == nil { @@ -1828,6 +1831,15 @@ func (s *server) CompleteJob(ctx context.Context, completed *proto.CompletedJob) }) } + if s.PrebuildsOrchestrator != nil { + // Track resource replacements, if there are any. + orchestrator := s.PrebuildsOrchestrator.Load() + if resourceReplacements := completed.GetWorkspaceBuild().GetResourceReplacements(); orchestrator != nil && len(resourceReplacements) > 0 { + // Fire and forget. Bind to the lifecycle of the server so shutdowns are handled gracefully. + go (*orchestrator).TrackResourceReplacement(s.lifecycleCtx, workspace.ID, workspaceBuild.ID, resourceReplacements) + } + } + msg, err := json.Marshal(wspubsub.WorkspaceEvent{ Kind: wspubsub.WorkspaceEventKindStateChange, WorkspaceID: workspace.ID, diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index 876cc5fc2d755..b6c60781dac35 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -26,11 +26,6 @@ import ( "github.com/coder/quartz" "github.com/coder/serpent" - "github.com/coder/coder/v2/coderd/prebuilds" - "github.com/coder/coder/v2/coderd/provisionerdserver" - "github.com/coder/coder/v2/coderd/rbac" - "github.com/coder/coder/v2/codersdk/agentsdk" - "github.com/coder/coder/v2/buildinfo" "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/database" @@ -42,11 +37,15 @@ import ( "github.com/coder/coder/v2/coderd/externalauth" "github.com/coder/coder/v2/coderd/notifications" "github.com/coder/coder/v2/coderd/notifications/notificationstest" + agplprebuilds "github.com/coder/coder/v2/coderd/prebuilds" + "github.com/coder/coder/v2/coderd/provisionerdserver" + "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/schedule" "github.com/coder/coder/v2/coderd/schedule/cron" "github.com/coder/coder/v2/coderd/telemetry" "github.com/coder/coder/v2/coderd/wspubsub" "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/coder/v2/provisionerd/proto" "github.com/coder/coder/v2/provisionersdk" sdkproto "github.com/coder/coder/v2/provisionersdk/proto" @@ -1889,7 +1888,7 @@ func TestCompleteJob(t *testing.T) { // GIVEN something is listening to process workspace reinitialization: reinitChan := make(chan agentsdk.ReinitializationEvent, 1) // Buffered to simplify test structure - cancel, err := prebuilds.NewPubsubWorkspaceClaimListener(ps, testutil.Logger(t)).ListenForWorkspaceClaims(ctx, workspace.ID, reinitChan) + cancel, err := agplprebuilds.NewPubsubWorkspaceClaimListener(ps, testutil.Logger(t)).ListenForWorkspaceClaims(ctx, workspace.ID, reinitChan) require.NoError(t, err) defer cancel() @@ -1917,6 +1916,106 @@ func TestCompleteJob(t *testing.T) { }) } }) + + t.Run("PrebuiltWorkspaceClaimWithResourceReplacements", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitLong) + + // Given: a mock prebuild orchestrator which stores calls to TrackResourceReplacement. + done := make(chan struct{}) + orchestrator := &mockPrebuildsOrchestrator{ + ReconciliationOrchestrator: agplprebuilds.DefaultReconciler, + done: done, + } + srv, db, ps, pd := setup(t, false, &overrides{ + prebuildsOrchestrator: orchestrator, + }) + + // Given: a workspace build which simulates claiming a prebuild. + user := dbgen.User(t, db, database.User{}) + template := dbgen.Template(t, db, database.Template{ + Name: "template", + Provisioner: database.ProvisionerTypeEcho, + OrganizationID: pd.OrganizationID, + }) + file := dbgen.File(t, db, database.File{CreatedBy: user.ID}) + workspaceTable := dbgen.Workspace(t, db, database.WorkspaceTable{ + TemplateID: template.ID, + OwnerID: user.ID, + OrganizationID: pd.OrganizationID, + }) + version := dbgen.TemplateVersion(t, db, database.TemplateVersion{ + OrganizationID: pd.OrganizationID, + TemplateID: uuid.NullUUID{ + UUID: template.ID, + Valid: true, + }, + JobID: uuid.New(), + }) + build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ + WorkspaceID: workspaceTable.ID, + InitiatorID: user.ID, + TemplateVersionID: version.ID, + Transition: database.WorkspaceTransitionStart, + Reason: database.BuildReasonInitiator, + }) + job := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{ + FileID: file.ID, + InitiatorID: user.ID, + Type: database.ProvisionerJobTypeWorkspaceBuild, + Input: must(json.Marshal(provisionerdserver.WorkspaceProvisionJob{ + WorkspaceBuildID: build.ID, + PrebuiltWorkspaceBuildStage: sdkproto.PrebuiltWorkspaceBuildStage_CLAIM, + })), + OrganizationID: pd.OrganizationID, + }) + _, err := db.AcquireProvisionerJob(ctx, database.AcquireProvisionerJobParams{ + OrganizationID: pd.OrganizationID, + WorkerID: uuid.NullUUID{ + UUID: pd.ID, + Valid: true, + }, + Types: []database.ProvisionerType{database.ProvisionerTypeEcho}, + }) + require.NoError(t, err) + + // When: a replacement is encountered. + replacements := []*sdkproto.ResourceReplacement{ + { + Resource: "docker_container[0]", + Paths: []string{"env"}, + }, + } + + // Then: CompleteJob makes a call to TrackResourceReplacement. + _, err = srv.CompleteJob(ctx, &proto.CompletedJob{ + JobId: job.ID.String(), + Type: &proto.CompletedJob_WorkspaceBuild_{ + WorkspaceBuild: &proto.CompletedJob_WorkspaceBuild{ + State: []byte{}, + ResourceReplacements: replacements, + }, + }, + }) + require.NoError(t, err) + + // Then: the replacements are as we expected. + testutil.RequireReceive(ctx, t, done) + require.Equal(t, replacements, orchestrator.replacements) + }) +} + +type mockPrebuildsOrchestrator struct { + agplprebuilds.ReconciliationOrchestrator + + replacements []*sdkproto.ResourceReplacement + done chan struct{} +} + +func (m *mockPrebuildsOrchestrator) TrackResourceReplacement(_ context.Context, _, _ uuid.UUID, replacements []*sdkproto.ResourceReplacement) { + m.replacements = replacements + m.done <- struct{}{} } func TestInsertWorkspacePresetsAndParameters(t *testing.T) { @@ -2803,6 +2902,7 @@ type overrides struct { heartbeatInterval time.Duration auditor audit.Auditor notificationEnqueuer notifications.Enqueuer + prebuildsOrchestrator agplprebuilds.ReconciliationOrchestrator } func setup(t *testing.T, ignoreLogErrors bool, ov *overrides) (proto.DRPCProvisionerDaemonServer, database.Store, pubsub.Pubsub, database.ProvisionerDaemon) { @@ -2884,6 +2984,13 @@ func setup(t *testing.T, ignoreLogErrors bool, ov *overrides) (proto.DRPCProvisi }) require.NoError(t, err) + prebuildsOrchestrator := ov.prebuildsOrchestrator + if prebuildsOrchestrator == nil { + prebuildsOrchestrator = agplprebuilds.DefaultReconciler + } + var op atomic.Pointer[agplprebuilds.ReconciliationOrchestrator] + op.Store(&prebuildsOrchestrator) + srv, err := provisionerdserver.NewServer( ov.ctx, &url.URL{}, @@ -2911,6 +3018,7 @@ func setup(t *testing.T, ignoreLogErrors bool, ov *overrides) (proto.DRPCProvisi HeartbeatFn: ov.heartbeatFn, }, notifEnq, + &op, ) require.NoError(t, err) return srv, db, ps, daemon diff --git a/docs/admin/templates/extending-templates/prebuilt-workspaces.md b/docs/admin/templates/extending-templates/prebuilt-workspaces.md index bbff3b7f15747..894a2a219a9cf 100644 --- a/docs/admin/templates/extending-templates/prebuilt-workspaces.md +++ b/docs/admin/templates/extending-templates/prebuilt-workspaces.md @@ -163,6 +163,12 @@ resource "docker_container" "workspace" { Learn more about `ignore_changes` in the [Terraform documentation](https://developer.hashicorp.com/terraform/language/meta-arguments/lifecycle#ignore_changes). +_A note on "immutable" attributes: Terraform providers may specify `ForceNew` on their resources' attributes. Any change +to these attributes require the replacement (destruction and recreation) of the managed resource instance, rather than an in-place update. +For example, the [`ami`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance#ami-1) attribute on the `aws_instance` resource +has [`ForceNew`](https://github.com/hashicorp/terraform-provider-aws/blob/main/internal/service/ec2/ec2_instance.go#L75-L81) set, +since the AMI cannot be changed in-place._ + ### Current limitations The prebuilt workspaces feature has these current limitations: diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index 8b473e8168ffa..f46848812a69e 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -1165,6 +1165,6 @@ func (api *API) setupPrebuilds(featureEnabled bool) (agplprebuilds.Reconciliatio } reconciler := prebuilds.NewStoreReconciler(api.Database, api.Pubsub, api.DeploymentValues.Prebuilds, - api.Logger.Named("prebuilds"), quartz.NewReal(), api.PrometheusRegistry) + api.Logger.Named("prebuilds"), quartz.NewReal(), api.PrometheusRegistry, api.NotificationsEnqueuer) return reconciler, prebuilds.NewEnterpriseClaimer(api.Database) } diff --git a/enterprise/coderd/prebuilds/claim_test.go b/enterprise/coderd/prebuilds/claim_test.go index ad31d2b4eff1b..5a18600a84602 100644 --- a/enterprise/coderd/prebuilds/claim_test.go +++ b/enterprise/coderd/prebuilds/claim_test.go @@ -147,7 +147,7 @@ func TestClaimPrebuild(t *testing.T) { EntitlementsUpdateInterval: time.Second, }) - reconciler := prebuilds.NewStoreReconciler(spy, pubsub, codersdk.PrebuildsConfig{}, logger, quartz.NewMock(t), prometheus.NewRegistry()) + reconciler := prebuilds.NewStoreReconciler(spy, pubsub, codersdk.PrebuildsConfig{}, logger, quartz.NewMock(t), prometheus.NewRegistry(), newNoopEnqueuer()) var claimer agplprebuilds.Claimer = prebuilds.NewEnterpriseClaimer(spy) api.AGPL.PrebuildsClaimer.Store(&claimer) diff --git a/enterprise/coderd/prebuilds/metricscollector.go b/enterprise/coderd/prebuilds/metricscollector.go index 76089c025243d..9f1cc837d0474 100644 --- a/enterprise/coderd/prebuilds/metricscollector.go +++ b/enterprise/coderd/prebuilds/metricscollector.go @@ -3,6 +3,7 @@ package prebuilds import ( "context" "fmt" + "sync" "sync/atomic" "time" @@ -16,50 +17,73 @@ import ( "github.com/coder/coder/v2/coderd/prebuilds" ) +const ( + namespace = "coderd_prebuilt_workspaces_" + + MetricCreatedCount = namespace + "created_total" + MetricFailedCount = namespace + "failed_total" + MetricClaimedCount = namespace + "claimed_total" + MetricResourceReplacementsCount = namespace + "resource_replacements_total" + MetricDesiredGauge = namespace + "desired" + MetricRunningGauge = namespace + "running" + MetricEligibleGauge = namespace + "eligible" + MetricLastUpdatedGauge = namespace + "metrics_last_updated" +) + var ( labels = []string{"template_name", "preset_name", "organization_name"} createdPrebuildsDesc = prometheus.NewDesc( - "coderd_prebuilt_workspaces_created_total", + MetricCreatedCount, "Total number of prebuilt workspaces that have been created to meet the desired instance count of each "+ "template preset.", labels, nil, ) failedPrebuildsDesc = prometheus.NewDesc( - "coderd_prebuilt_workspaces_failed_total", + MetricFailedCount, "Total number of prebuilt workspaces that failed to build.", labels, nil, ) claimedPrebuildsDesc = prometheus.NewDesc( - "coderd_prebuilt_workspaces_claimed_total", + MetricClaimedCount, "Total number of prebuilt workspaces which were claimed by users. Claiming refers to creating a workspace "+ "with a preset selected for which eligible prebuilt workspaces are available and one is reassigned to a user.", labels, nil, ) + resourceReplacementsDesc = prometheus.NewDesc( + MetricResourceReplacementsCount, + "Total number of prebuilt workspaces whose resource(s) got replaced upon being claimed. "+ + "In Terraform, drift on immutable attributes results in resource replacement. "+ + "This represents a worst-case scenario for prebuilt workspaces because the pre-provisioned resource "+ + "would have been recreated when claiming, thus obviating the point of pre-provisioning. "+ + "See https://coder.com/docs/admin/templates/extending-templates/prebuilt-workspaces#preventing-resource-replacement", + labels, + nil, + ) desiredPrebuildsDesc = prometheus.NewDesc( - "coderd_prebuilt_workspaces_desired", + MetricDesiredGauge, "Target number of prebuilt workspaces that should be available for each template preset.", labels, nil, ) runningPrebuildsDesc = prometheus.NewDesc( - "coderd_prebuilt_workspaces_running", + MetricRunningGauge, "Current number of prebuilt workspaces that are in a running state. These workspaces have started "+ "successfully but may not yet be claimable by users (see coderd_prebuilt_workspaces_eligible).", labels, nil, ) eligiblePrebuildsDesc = prometheus.NewDesc( - "coderd_prebuilt_workspaces_eligible", + MetricEligibleGauge, "Current number of prebuilt workspaces that are eligible to be claimed by users. These are workspaces that "+ "have completed their build process with their agent reporting 'ready' status.", labels, nil, ) lastUpdateDesc = prometheus.NewDesc( - "coderd_prebuilt_workspaces_metrics_last_updated", + MetricLastUpdatedGauge, "The unix timestamp when the metrics related to prebuilt workspaces were last updated; these metrics are cached.", []string{}, nil, @@ -77,6 +101,9 @@ type MetricsCollector struct { snapshotter prebuilds.StateSnapshotter latestState atomic.Pointer[metricsState] + + replacementsCounter map[replacementKey]float64 + replacementsCounterMu sync.Mutex } var _ prometheus.Collector = new(MetricsCollector) @@ -84,9 +111,10 @@ var _ prometheus.Collector = new(MetricsCollector) func NewMetricsCollector(db database.Store, logger slog.Logger, snapshotter prebuilds.StateSnapshotter) *MetricsCollector { log := logger.Named("prebuilds_metrics_collector") return &MetricsCollector{ - database: db, - logger: log, - snapshotter: snapshotter, + database: db, + logger: log, + snapshotter: snapshotter, + replacementsCounter: make(map[replacementKey]float64), } } @@ -94,6 +122,7 @@ func (*MetricsCollector) Describe(descCh chan<- *prometheus.Desc) { descCh <- createdPrebuildsDesc descCh <- failedPrebuildsDesc descCh <- claimedPrebuildsDesc + descCh <- resourceReplacementsDesc descCh <- desiredPrebuildsDesc descCh <- runningPrebuildsDesc descCh <- eligiblePrebuildsDesc @@ -117,6 +146,12 @@ func (mc *MetricsCollector) Collect(metricsCh chan<- prometheus.Metric) { metricsCh <- prometheus.MustNewConstMetric(claimedPrebuildsDesc, prometheus.CounterValue, float64(metric.ClaimedCount), metric.TemplateName, metric.PresetName, metric.OrganizationName) } + mc.replacementsCounterMu.Lock() + for key, val := range mc.replacementsCounter { + metricsCh <- prometheus.MustNewConstMetric(resourceReplacementsDesc, prometheus.CounterValue, val, key.templateName, key.presetName, key.orgName) + } + mc.replacementsCounterMu.Unlock() + for _, preset := range currentState.snapshot.Presets { if !preset.UsingActiveVersion { continue @@ -187,3 +222,24 @@ func (mc *MetricsCollector) UpdateState(ctx context.Context, timeout time.Durati }) return nil } + +type replacementKey struct { + orgName, templateName, presetName string +} + +func (k replacementKey) String() string { + return fmt.Sprintf("%s:%s:%s", k.orgName, k.templateName, k.presetName) +} + +func (mc *MetricsCollector) trackResourceReplacement(orgName, templateName, presetName string) { + mc.replacementsCounterMu.Lock() + defer mc.replacementsCounterMu.Unlock() + + key := replacementKey{orgName: orgName, templateName: templateName, presetName: presetName} + + // We only track _that_ a resource replacement occurred, not how many. + // Just one is enough to ruin a prebuild, but we can't know apriori which replacement would cause this. + // For example, say we have 2 replacements: a docker_container and a null_resource; we don't know which one might + // cause an issue (or indeed if either would), so we just track the replacement. + mc.replacementsCounter[key]++ +} diff --git a/enterprise/coderd/prebuilds/metricscollector_test.go b/enterprise/coderd/prebuilds/metricscollector_test.go index de3f5d017f715..07c3c3c6996bb 100644 --- a/enterprise/coderd/prebuilds/metricscollector_test.go +++ b/enterprise/coderd/prebuilds/metricscollector_test.go @@ -57,12 +57,12 @@ func TestMetricsCollector(t *testing.T) { initiatorIDs: []uuid.UUID{agplprebuilds.SystemUserID}, ownerIDs: []uuid.UUID{agplprebuilds.SystemUserID}, metrics: []metricCheck{ - {"coderd_prebuilt_workspaces_created_total", ptr.To(1.0), true}, - {"coderd_prebuilt_workspaces_claimed_total", ptr.To(0.0), true}, - {"coderd_prebuilt_workspaces_failed_total", ptr.To(0.0), true}, - {"coderd_prebuilt_workspaces_desired", ptr.To(1.0), false}, - {"coderd_prebuilt_workspaces_running", ptr.To(0.0), false}, - {"coderd_prebuilt_workspaces_eligible", ptr.To(0.0), false}, + {prebuilds.MetricCreatedCount, ptr.To(1.0), true}, + {prebuilds.MetricClaimedCount, ptr.To(0.0), true}, + {prebuilds.MetricFailedCount, ptr.To(0.0), true}, + {prebuilds.MetricDesiredGauge, ptr.To(1.0), false}, + {prebuilds.MetricRunningGauge, ptr.To(0.0), false}, + {prebuilds.MetricEligibleGauge, ptr.To(0.0), false}, }, templateDeleted: []bool{false}, eligible: []bool{false}, @@ -74,12 +74,12 @@ func TestMetricsCollector(t *testing.T) { initiatorIDs: []uuid.UUID{agplprebuilds.SystemUserID}, ownerIDs: []uuid.UUID{agplprebuilds.SystemUserID}, metrics: []metricCheck{ - {"coderd_prebuilt_workspaces_created_total", ptr.To(1.0), true}, - {"coderd_prebuilt_workspaces_claimed_total", ptr.To(0.0), true}, - {"coderd_prebuilt_workspaces_failed_total", ptr.To(0.0), true}, - {"coderd_prebuilt_workspaces_desired", ptr.To(1.0), false}, - {"coderd_prebuilt_workspaces_running", ptr.To(1.0), false}, - {"coderd_prebuilt_workspaces_eligible", ptr.To(0.0), false}, + {prebuilds.MetricCreatedCount, ptr.To(1.0), true}, + {prebuilds.MetricClaimedCount, ptr.To(0.0), true}, + {prebuilds.MetricFailedCount, ptr.To(0.0), true}, + {prebuilds.MetricDesiredGauge, ptr.To(1.0), false}, + {prebuilds.MetricRunningGauge, ptr.To(1.0), false}, + {prebuilds.MetricEligibleGauge, ptr.To(0.0), false}, }, templateDeleted: []bool{false}, eligible: []bool{false}, @@ -91,11 +91,11 @@ func TestMetricsCollector(t *testing.T) { initiatorIDs: []uuid.UUID{agplprebuilds.SystemUserID}, ownerIDs: []uuid.UUID{agplprebuilds.SystemUserID, uuid.New()}, metrics: []metricCheck{ - {"coderd_prebuilt_workspaces_created_total", ptr.To(1.0), true}, - {"coderd_prebuilt_workspaces_failed_total", ptr.To(1.0), true}, - {"coderd_prebuilt_workspaces_desired", ptr.To(1.0), false}, - {"coderd_prebuilt_workspaces_running", ptr.To(0.0), false}, - {"coderd_prebuilt_workspaces_eligible", ptr.To(0.0), false}, + {prebuilds.MetricCreatedCount, ptr.To(1.0), true}, + {prebuilds.MetricFailedCount, ptr.To(1.0), true}, + {prebuilds.MetricDesiredGauge, ptr.To(1.0), false}, + {prebuilds.MetricRunningGauge, ptr.To(0.0), false}, + {prebuilds.MetricEligibleGauge, ptr.To(0.0), false}, }, templateDeleted: []bool{false}, eligible: []bool{false}, @@ -107,12 +107,12 @@ func TestMetricsCollector(t *testing.T) { initiatorIDs: []uuid.UUID{agplprebuilds.SystemUserID}, ownerIDs: []uuid.UUID{agplprebuilds.SystemUserID}, metrics: []metricCheck{ - {"coderd_prebuilt_workspaces_created_total", ptr.To(1.0), true}, - {"coderd_prebuilt_workspaces_claimed_total", ptr.To(0.0), true}, - {"coderd_prebuilt_workspaces_failed_total", ptr.To(0.0), true}, - {"coderd_prebuilt_workspaces_desired", ptr.To(1.0), false}, - {"coderd_prebuilt_workspaces_running", ptr.To(1.0), false}, - {"coderd_prebuilt_workspaces_eligible", ptr.To(1.0), false}, + {prebuilds.MetricCreatedCount, ptr.To(1.0), true}, + {prebuilds.MetricClaimedCount, ptr.To(0.0), true}, + {prebuilds.MetricFailedCount, ptr.To(0.0), true}, + {prebuilds.MetricDesiredGauge, ptr.To(1.0), false}, + {prebuilds.MetricRunningGauge, ptr.To(1.0), false}, + {prebuilds.MetricEligibleGauge, ptr.To(1.0), false}, }, templateDeleted: []bool{false}, eligible: []bool{true}, @@ -124,12 +124,12 @@ func TestMetricsCollector(t *testing.T) { initiatorIDs: []uuid.UUID{agplprebuilds.SystemUserID}, ownerIDs: []uuid.UUID{agplprebuilds.SystemUserID}, metrics: []metricCheck{ - {"coderd_prebuilt_workspaces_created_total", ptr.To(1.0), true}, - {"coderd_prebuilt_workspaces_claimed_total", ptr.To(0.0), true}, - {"coderd_prebuilt_workspaces_failed_total", ptr.To(0.0), true}, - {"coderd_prebuilt_workspaces_desired", ptr.To(1.0), false}, - {"coderd_prebuilt_workspaces_running", ptr.To(1.0), false}, - {"coderd_prebuilt_workspaces_eligible", ptr.To(0.0), false}, + {prebuilds.MetricCreatedCount, ptr.To(1.0), true}, + {prebuilds.MetricClaimedCount, ptr.To(0.0), true}, + {prebuilds.MetricFailedCount, ptr.To(0.0), true}, + {prebuilds.MetricDesiredGauge, ptr.To(1.0), false}, + {prebuilds.MetricRunningGauge, ptr.To(1.0), false}, + {prebuilds.MetricEligibleGauge, ptr.To(0.0), false}, }, templateDeleted: []bool{false}, eligible: []bool{false}, @@ -141,11 +141,11 @@ func TestMetricsCollector(t *testing.T) { initiatorIDs: []uuid.UUID{agplprebuilds.SystemUserID}, ownerIDs: []uuid.UUID{uuid.New()}, metrics: []metricCheck{ - {"coderd_prebuilt_workspaces_created_total", ptr.To(1.0), true}, - {"coderd_prebuilt_workspaces_claimed_total", ptr.To(1.0), true}, - {"coderd_prebuilt_workspaces_desired", ptr.To(1.0), false}, - {"coderd_prebuilt_workspaces_running", ptr.To(0.0), false}, - {"coderd_prebuilt_workspaces_eligible", ptr.To(0.0), false}, + {prebuilds.MetricCreatedCount, ptr.To(1.0), true}, + {prebuilds.MetricClaimedCount, ptr.To(1.0), true}, + {prebuilds.MetricDesiredGauge, ptr.To(1.0), false}, + {prebuilds.MetricRunningGauge, ptr.To(0.0), false}, + {prebuilds.MetricEligibleGauge, ptr.To(0.0), false}, }, templateDeleted: []bool{false}, eligible: []bool{false}, @@ -157,9 +157,9 @@ func TestMetricsCollector(t *testing.T) { initiatorIDs: []uuid.UUID{uuid.New()}, ownerIDs: []uuid.UUID{uuid.New()}, metrics: []metricCheck{ - {"coderd_prebuilt_workspaces_desired", ptr.To(1.0), false}, - {"coderd_prebuilt_workspaces_running", ptr.To(0.0), false}, - {"coderd_prebuilt_workspaces_eligible", ptr.To(0.0), false}, + {prebuilds.MetricDesiredGauge, ptr.To(1.0), false}, + {prebuilds.MetricRunningGauge, ptr.To(0.0), false}, + {prebuilds.MetricEligibleGauge, ptr.To(0.0), false}, }, templateDeleted: []bool{false}, eligible: []bool{false}, @@ -171,7 +171,7 @@ func TestMetricsCollector(t *testing.T) { initiatorIDs: []uuid.UUID{agplprebuilds.SystemUserID}, ownerIDs: []uuid.UUID{agplprebuilds.SystemUserID, uuid.New()}, metrics: []metricCheck{ - {"coderd_prebuilt_workspaces_desired", ptr.To(0.0), false}, + {prebuilds.MetricDesiredGauge, ptr.To(0.0), false}, }, templateDeleted: []bool{true}, eligible: []bool{false}, @@ -183,8 +183,8 @@ func TestMetricsCollector(t *testing.T) { initiatorIDs: []uuid.UUID{agplprebuilds.SystemUserID}, ownerIDs: []uuid.UUID{agplprebuilds.SystemUserID}, metrics: []metricCheck{ - {"coderd_prebuilt_workspaces_running", ptr.To(1.0), false}, - {"coderd_prebuilt_workspaces_eligible", ptr.To(0.0), false}, + {prebuilds.MetricRunningGauge, ptr.To(1.0), false}, + {prebuilds.MetricEligibleGauge, ptr.To(0.0), false}, }, templateDeleted: []bool{true}, eligible: []bool{false}, @@ -220,7 +220,7 @@ func TestMetricsCollector(t *testing.T) { }) clock := quartz.NewMock(t) db, pubsub := dbtestutil.NewDB(t) - reconciler := prebuilds.NewStoreReconciler(db, pubsub, codersdk.PrebuildsConfig{}, logger, quartz.NewMock(t), prometheus.NewRegistry()) + reconciler := prebuilds.NewStoreReconciler(db, pubsub, codersdk.PrebuildsConfig{}, logger, quartz.NewMock(t), prometheus.NewRegistry(), newNoopEnqueuer()) ctx := testutil.Context(t, testutil.WaitLong) createdUsers := []uuid.UUID{agplprebuilds.SystemUserID} @@ -242,7 +242,7 @@ func TestMetricsCollector(t *testing.T) { org, template := setupTestDBTemplate(t, db, ownerID, templateDeleted) templateVersionID := setupTestDBTemplateVersion(ctx, t, clock, db, pubsub, org.ID, ownerID, template.ID) preset := setupTestDBPreset(t, db, templateVersionID, 1, uuid.New().String()) - workspace := setupTestDBWorkspace( + workspace, _ := setupTestDBWorkspace( t, clock, db, pubsub, transition, jobStatus, org.ID, preset, template.ID, templateVersionID, initiatorID, ownerID, ) diff --git a/enterprise/coderd/prebuilds/reconcile.go b/enterprise/coderd/prebuilds/reconcile.go index 79a8baa337e72..f9588a5d7cacb 100644 --- a/enterprise/coderd/prebuilds/reconcile.go +++ b/enterprise/coderd/prebuilds/reconcile.go @@ -3,8 +3,10 @@ package prebuilds import ( "context" "database/sql" + "errors" "fmt" "math" + "strings" "sync" "sync/atomic" "time" @@ -19,11 +21,13 @@ import ( "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/provisionerjobs" "github.com/coder/coder/v2/coderd/database/pubsub" + "github.com/coder/coder/v2/coderd/notifications" "github.com/coder/coder/v2/coderd/prebuilds" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" "github.com/coder/coder/v2/coderd/wsbuilder" "github.com/coder/coder/v2/codersdk" + sdkproto "github.com/coder/coder/v2/provisionersdk/proto" "cdr.dev/slog" @@ -40,6 +44,7 @@ type StoreReconciler struct { clock quartz.Clock registerer prometheus.Registerer metrics *MetricsCollector + notifEnq notifications.Enqueuer cancelFn context.CancelCauseFunc running atomic.Bool @@ -56,6 +61,7 @@ func NewStoreReconciler(store database.Store, logger slog.Logger, clock quartz.Clock, registerer prometheus.Registerer, + notifEnq notifications.Enqueuer, ) *StoreReconciler { reconciler := &StoreReconciler{ store: store, @@ -64,6 +70,7 @@ func NewStoreReconciler(store database.Store, cfg: cfg, clock: clock, registerer: registerer, + notifEnq: notifEnq, done: make(chan struct{}, 1), provisionNotifyCh: make(chan database.ProvisionerJob, 10), } @@ -633,3 +640,124 @@ func (c *StoreReconciler) provision( return nil } + +// ForceMetricsUpdate forces the metrics collector, if defined, to update its state (we cache the metrics state to +// reduce load on the database). +func (c *StoreReconciler) ForceMetricsUpdate(ctx context.Context) error { + if c.metrics == nil { + return nil + } + + return c.metrics.UpdateState(ctx, time.Second*10) +} + +func (c *StoreReconciler) TrackResourceReplacement(ctx context.Context, workspaceID, buildID uuid.UUID, replacements []*sdkproto.ResourceReplacement) { + // nolint:gocritic // Necessary to query all the required data. + ctx = dbauthz.AsSystemRestricted(ctx) + // Since this may be called in a fire-and-forget fashion, we need to give up at some point. + trackCtx, trackCancel := context.WithTimeout(ctx, time.Minute) + defer trackCancel() + + if err := c.trackResourceReplacement(trackCtx, workspaceID, buildID, replacements); err != nil { + c.logger.Error(ctx, "failed to track resource replacement", slog.Error(err)) + } +} + +// nolint:revive // Shut up it's fine. +func (c *StoreReconciler) trackResourceReplacement(ctx context.Context, workspaceID, buildID uuid.UUID, replacements []*sdkproto.ResourceReplacement) error { + if err := ctx.Err(); err != nil { + return err + } + + workspace, err := c.store.GetWorkspaceByID(ctx, workspaceID) + if err != nil { + return xerrors.Errorf("fetch workspace %q: %w", workspaceID.String(), err) + } + + build, err := c.store.GetWorkspaceBuildByID(ctx, buildID) + if err != nil { + return xerrors.Errorf("fetch workspace build %q: %w", buildID.String(), err) + } + + // The first build will always be the prebuild. + prebuild, err := c.store.GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx, database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams{ + WorkspaceID: workspaceID, BuildNumber: 1, + }) + if err != nil { + return xerrors.Errorf("fetch prebuild: %w", err) + } + + // This should not be possible, but defend against it. + if !prebuild.TemplateVersionPresetID.Valid || prebuild.TemplateVersionPresetID.UUID == uuid.Nil { + return xerrors.Errorf("no preset used in prebuild for workspace %q", workspaceID.String()) + } + + prebuildPreset, err := c.store.GetPresetByID(ctx, prebuild.TemplateVersionPresetID.UUID) + if err != nil { + return xerrors.Errorf("fetch template preset for template version ID %q: %w", prebuild.TemplateVersionID.String(), err) + } + + claimant, err := c.store.GetUserByID(ctx, workspace.OwnerID) // At this point, the workspace is owned by the new owner. + if err != nil { + return xerrors.Errorf("fetch claimant %q: %w", workspace.OwnerID.String(), err) + } + + // Use the claiming build here (not prebuild) because both should be equivalent, and we might as well spot inconsistencies now. + templateVersion, err := c.store.GetTemplateVersionByID(ctx, build.TemplateVersionID) + if err != nil { + return xerrors.Errorf("fetch template version %q: %w", build.TemplateVersionID.String(), err) + } + + org, err := c.store.GetOrganizationByID(ctx, workspace.OrganizationID) + if err != nil { + return xerrors.Errorf("fetch org %q: %w", workspace.OrganizationID.String(), err) + } + + // Track resource replacement in Prometheus metric. + if c.metrics != nil { + c.metrics.trackResourceReplacement(org.Name, workspace.TemplateName, prebuildPreset.Name) + } + + // Send notification to template admins. + if c.notifEnq == nil { + c.logger.Warn(ctx, "notification enqueuer not set, cannot send resource replacement notification(s)") + return nil + } + + repls := make(map[string]string, len(replacements)) + for _, repl := range replacements { + repls[repl.GetResource()] = strings.Join(repl.GetPaths(), ", ") + } + + templateAdmins, err := c.store.GetUsers(ctx, database.GetUsersParams{ + RbacRole: []string{codersdk.RoleTemplateAdmin}, + }) + if err != nil { + return xerrors.Errorf("fetch template admins: %w", err) + } + + var notifErr error + for _, templateAdmin := range templateAdmins { + if _, err := c.notifEnq.EnqueueWithData(ctx, templateAdmin.ID, notifications.TemplateWorkspaceResourceReplaced, + map[string]string{ + "org": org.Name, + "workspace": workspace.Name, + "template": workspace.TemplateName, + "template_version": templateVersion.Name, + "preset": prebuildPreset.Name, + "workspace_build_num": fmt.Sprintf("%d", build.BuildNumber), + "claimant": claimant.Username, + }, + map[string]any{ + "replacements": repls, + }, "prebuilds_reconciler", + // Associate this notification with all the related entities. + workspace.ID, workspace.OwnerID, workspace.TemplateID, templateVersion.ID, prebuildPreset.ID, workspace.OrganizationID, + ); err != nil { + notifErr = errors.Join(xerrors.Errorf("send notification to %q: %w", templateAdmin.ID.String(), err)) + continue + } + } + + return notifErr +} diff --git a/enterprise/coderd/prebuilds/reconcile_test.go b/enterprise/coderd/prebuilds/reconcile_test.go index a1666134a7965..bdf447dcfae22 100644 --- a/enterprise/coderd/prebuilds/reconcile_test.go +++ b/enterprise/coderd/prebuilds/reconcile_test.go @@ -9,10 +9,14 @@ import ( "time" "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/assert" "golang.org/x/xerrors" "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/coder/v2/coderd/notifications" + "github.com/coder/coder/v2/coderd/notifications/notificationstest" "github.com/coder/coder/v2/coderd/util/slice" + sdkproto "github.com/coder/coder/v2/provisionersdk/proto" "github.com/google/uuid" "github.com/stretchr/testify/require" @@ -49,7 +53,7 @@ func TestNoReconciliationActionsIfNoPresets(t *testing.T) { ReconciliationInterval: serpent.Duration(testutil.WaitLong), } logger := testutil.Logger(t) - controller := prebuilds.NewStoreReconciler(db, ps, cfg, logger, quartz.NewMock(t), prometheus.NewRegistry()) + controller := prebuilds.NewStoreReconciler(db, ps, cfg, logger, quartz.NewMock(t), prometheus.NewRegistry(), newNoopEnqueuer()) // given a template version with no presets org := dbgen.Organization(t, db, database.Organization{}) @@ -94,7 +98,7 @@ func TestNoReconciliationActionsIfNoPrebuilds(t *testing.T) { ReconciliationInterval: serpent.Duration(testutil.WaitLong), } logger := testutil.Logger(t) - controller := prebuilds.NewStoreReconciler(db, ps, cfg, logger, quartz.NewMock(t), prometheus.NewRegistry()) + controller := prebuilds.NewStoreReconciler(db, ps, cfg, logger, quartz.NewMock(t), prometheus.NewRegistry(), newNoopEnqueuer()) // given there are presets, but no prebuilds org := dbgen.Organization(t, db, database.Organization{}) @@ -345,7 +349,7 @@ func TestPrebuildReconciliation(t *testing.T) { 1, uuid.New().String(), ) - prebuild := setupTestDBPrebuild( + prebuild, _ := setupTestDBPrebuild( t, clock, db, @@ -367,7 +371,7 @@ func TestPrebuildReconciliation(t *testing.T) { if useBrokenPubsub { pubSub = &brokenPublisher{Pubsub: pubSub} } - controller := prebuilds.NewStoreReconciler(db, pubSub, cfg, logger, quartz.NewMock(t), prometheus.NewRegistry()) + controller := prebuilds.NewStoreReconciler(db, pubSub, cfg, logger, quartz.NewMock(t), prometheus.NewRegistry(), newNoopEnqueuer()) // Run the reconciliation multiple times to ensure idempotency // 8 was arbitrary, but large enough to reasonably trust the result @@ -444,7 +448,7 @@ func TestMultiplePresetsPerTemplateVersion(t *testing.T) { t, &slogtest.Options{IgnoreErrors: true}, ).Leveled(slog.LevelDebug) db, pubSub := dbtestutil.NewDB(t) - controller := prebuilds.NewStoreReconciler(db, pubSub, cfg, logger, quartz.NewMock(t), prometheus.NewRegistry()) + controller := prebuilds.NewStoreReconciler(db, pubSub, cfg, logger, quartz.NewMock(t), prometheus.NewRegistry(), newNoopEnqueuer()) ownerID := uuid.New() dbgen.User(t, db, database.User{ @@ -477,7 +481,7 @@ func TestMultiplePresetsPerTemplateVersion(t *testing.T) { ) prebuildIDs := make([]uuid.UUID, 0) for i := 0; i < int(preset.DesiredInstances.Int32); i++ { - prebuild := setupTestDBPrebuild( + prebuild, _ := setupTestDBPrebuild( t, clock, db, @@ -528,7 +532,7 @@ func TestInvalidPreset(t *testing.T) { t, &slogtest.Options{IgnoreErrors: true}, ).Leveled(slog.LevelDebug) db, pubSub := dbtestutil.NewDB(t) - controller := prebuilds.NewStoreReconciler(db, pubSub, cfg, logger, quartz.NewMock(t), prometheus.NewRegistry()) + controller := prebuilds.NewStoreReconciler(db, pubSub, cfg, logger, quartz.NewMock(t), prometheus.NewRegistry(), newNoopEnqueuer()) ownerID := uuid.New() dbgen.User(t, db, database.User{ @@ -592,7 +596,7 @@ func TestDeletionOfPrebuiltWorkspaceWithInvalidPreset(t *testing.T) { t, &slogtest.Options{IgnoreErrors: true}, ).Leveled(slog.LevelDebug) db, pubSub := dbtestutil.NewDB(t) - controller := prebuilds.NewStoreReconciler(db, pubSub, cfg, logger, quartz.NewMock(t), prometheus.NewRegistry()) + controller := prebuilds.NewStoreReconciler(db, pubSub, cfg, logger, quartz.NewMock(t), prometheus.NewRegistry(), newNoopEnqueuer()) ownerID := uuid.New() dbgen.User(t, db, database.User{ @@ -601,7 +605,7 @@ func TestDeletionOfPrebuiltWorkspaceWithInvalidPreset(t *testing.T) { org, template := setupTestDBTemplate(t, db, ownerID, templateDeleted) templateVersionID := setupTestDBTemplateVersion(ctx, t, clock, db, pubSub, org.ID, ownerID, template.ID) preset := setupTestDBPreset(t, db, templateVersionID, 1, uuid.New().String()) - prebuiltWorkspace := setupTestDBPrebuild( + prebuiltWorkspace, _ := setupTestDBPrebuild( t, clock, db, @@ -669,7 +673,7 @@ func TestRunLoop(t *testing.T) { t, &slogtest.Options{IgnoreErrors: true}, ).Leveled(slog.LevelDebug) db, pubSub := dbtestutil.NewDB(t) - reconciler := prebuilds.NewStoreReconciler(db, pubSub, cfg, logger, clock, prometheus.NewRegistry()) + reconciler := prebuilds.NewStoreReconciler(db, pubSub, cfg, logger, clock, prometheus.NewRegistry(), newNoopEnqueuer()) ownerID := uuid.New() dbgen.User(t, db, database.User{ @@ -702,7 +706,7 @@ func TestRunLoop(t *testing.T) { ) prebuildIDs := make([]uuid.UUID, 0) for i := 0; i < int(preset.DesiredInstances.Int32); i++ { - prebuild := setupTestDBPrebuild( + prebuild, _ := setupTestDBPrebuild( t, clock, db, @@ -799,7 +803,7 @@ func TestFailedBuildBackoff(t *testing.T) { t, &slogtest.Options{IgnoreErrors: true}, ).Leveled(slog.LevelDebug) db, ps := dbtestutil.NewDB(t) - reconciler := prebuilds.NewStoreReconciler(db, ps, cfg, logger, clock, prometheus.NewRegistry()) + reconciler := prebuilds.NewStoreReconciler(db, ps, cfg, logger, clock, prometheus.NewRegistry(), newNoopEnqueuer()) // Given: an active template version with presets and prebuilds configured. const desiredInstances = 2 @@ -812,7 +816,7 @@ func TestFailedBuildBackoff(t *testing.T) { preset := setupTestDBPreset(t, db, templateVersionID, desiredInstances, "test") for range desiredInstances { - _ = setupTestDBPrebuild(t, clock, db, ps, database.WorkspaceTransitionStart, database.ProvisionerJobStatusFailed, org.ID, preset, template.ID, templateVersionID) + _, _ = setupTestDBPrebuild(t, clock, db, ps, database.WorkspaceTransitionStart, database.ProvisionerJobStatusFailed, org.ID, preset, template.ID, templateVersionID) } // When: determining what actions to take next, backoff is calculated because the prebuild is in a failed state. @@ -873,7 +877,7 @@ func TestFailedBuildBackoff(t *testing.T) { if i == 1 { status = database.ProvisionerJobStatusSucceeded } - _ = setupTestDBPrebuild(t, clock, db, ps, database.WorkspaceTransitionStart, status, org.ID, preset, template.ID, templateVersionID) + _, _ = setupTestDBPrebuild(t, clock, db, ps, database.WorkspaceTransitionStart, status, org.ID, preset, template.ID, templateVersionID) } // Then: the backoff time is roughly equal to two backoff intervals, since another build has failed. @@ -914,7 +918,8 @@ func TestReconciliationLock(t *testing.T) { codersdk.PrebuildsConfig{}, slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug), quartz.NewMock(t), - prometheus.NewRegistry()) + prometheus.NewRegistry(), + newNoopEnqueuer()) reconciler.WithReconciliationLock(ctx, logger, func(_ context.Context, _ database.Store) error { lockObtained := mutex.TryLock() // As long as the postgres lock is held, this mutex should always be unlocked when we get here. @@ -931,6 +936,102 @@ func TestReconciliationLock(t *testing.T) { wg.Wait() } +func TestTrackResourceReplacement(t *testing.T) { + t.Parallel() + + if !dbtestutil.WillUsePostgres() { + t.Skip("This test requires postgres") + } + + ctx := testutil.Context(t, testutil.WaitSuperLong) + + // Setup. + clock := quartz.NewMock(t) + logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: false}).Leveled(slog.LevelDebug) + db, ps := dbtestutil.NewDB(t) + + fakeEnqueuer := newFakeEnqueuer() + registry := prometheus.NewRegistry() + reconciler := prebuilds.NewStoreReconciler(db, ps, codersdk.PrebuildsConfig{}, logger, clock, registry, fakeEnqueuer) + + // Given: a template admin to receive a notification. + templateAdmin := dbgen.User(t, db, database.User{ + RBACRoles: []string{codersdk.RoleTemplateAdmin}, + }) + + // Given: a prebuilt workspace. + userID := uuid.New() + dbgen.User(t, db, database.User{ID: userID}) + org, template := setupTestDBTemplate(t, db, userID, false) + templateVersionID := setupTestDBTemplateVersion(ctx, t, clock, db, ps, org.ID, userID, template.ID) + preset := setupTestDBPreset(t, db, templateVersionID, 1, "b0rked") + prebuiltWorkspace, prebuild := setupTestDBPrebuild(t, clock, db, ps, database.WorkspaceTransitionStart, database.ProvisionerJobStatusSucceeded, org.ID, preset, template.ID, templateVersionID) + + // Given: no replacement has been tracked yet, we should not see a metric for it yet. + require.NoError(t, reconciler.ForceMetricsUpdate(ctx)) + mf, err := registry.Gather() + require.NoError(t, err) + require.Nil(t, findMetric(mf, prebuilds.MetricResourceReplacementsCount, map[string]string{ + "template_name": template.Name, + "preset_name": preset.Name, + "org_name": org.Name, + })) + + // When: a claim occurred and resource replacements are detected (_how_ is out of scope of this test). + reconciler.TrackResourceReplacement(ctx, prebuiltWorkspace.ID, prebuild.ID, []*sdkproto.ResourceReplacement{ + { + Resource: "docker_container[0]", + Paths: []string{"env", "image"}, + }, + { + Resource: "docker_volume[0]", + Paths: []string{"name"}, + }, + }) + + // Then: a notification will be sent detailing the replacement(s). + matching := fakeEnqueuer.Sent(func(notification *notificationstest.FakeNotification) bool { + // This is not an exhaustive check of the expected labels/data in the notification. This would tie the implementations + // too tightly together. + // All we need to validate is that a template of the right kind was sent, to the expected user, with some replacements. + + if !assert.Equal(t, notification.TemplateID, notifications.TemplateWorkspaceResourceReplaced, "unexpected template") { + return false + } + + if !assert.Equal(t, templateAdmin.ID, notification.UserID, "unexpected receiver") { + return false + } + + if !assert.Len(t, notification.Data["replacements"], 2, "unexpected replacements count") { + return false + } + + return true + }) + require.Len(t, matching, 1) + + // Then: the metric will be incremented. + mf, err = registry.Gather() + require.NoError(t, err) + metric := findMetric(mf, prebuilds.MetricResourceReplacementsCount, map[string]string{ + "template_name": template.Name, + "preset_name": preset.Name, + "org_name": org.Name, + }) + require.NotNil(t, metric) + require.NotNil(t, metric.GetCounter()) + require.EqualValues(t, 1, metric.GetCounter().GetValue()) +} + +func newNoopEnqueuer() *notifications.NoopEnqueuer { + return notifications.NewNoopEnqueuer() +} + +func newFakeEnqueuer() *notificationstest.FakeEnqueuer { + return notificationstest.NewFakeEnqueuer() +} + // nolint:revive // It's a control flag, but this is a test. func setupTestDBTemplate( t *testing.T, @@ -1040,7 +1141,7 @@ func setupTestDBPrebuild( preset database.TemplateVersionPreset, templateID uuid.UUID, templateVersionID uuid.UUID, -) database.WorkspaceTable { +) (database.WorkspaceTable, database.WorkspaceBuild) { t.Helper() return setupTestDBWorkspace(t, clock, db, ps, transition, prebuildStatus, orgID, preset, templateID, templateVersionID, agplprebuilds.SystemUserID, agplprebuilds.SystemUserID) } @@ -1058,7 +1159,7 @@ func setupTestDBWorkspace( templateVersionID uuid.UUID, initiatorID uuid.UUID, ownerID uuid.UUID, -) database.WorkspaceTable { +) (database.WorkspaceTable, database.WorkspaceBuild) { t.Helper() cancelledAt := sql.NullTime{} completedAt := sql.NullTime{} @@ -1117,7 +1218,7 @@ func setupTestDBWorkspace( }, }) - return workspace + return workspace, workspaceBuild } // nolint:revive // It's a control flag, but this is a test. diff --git a/enterprise/coderd/provisionerdaemons.go b/enterprise/coderd/provisionerdaemons.go index b9a93144f4931..0315209e29543 100644 --- a/enterprise/coderd/provisionerdaemons.go +++ b/enterprise/coderd/provisionerdaemons.go @@ -19,6 +19,8 @@ import ( "storj.io/drpc/drpcserver" "cdr.dev/slog" + "github.com/coder/websocket" + "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbtime" @@ -34,7 +36,6 @@ import ( "github.com/coder/coder/v2/codersdk/drpcsdk" "github.com/coder/coder/v2/provisionerd/proto" "github.com/coder/coder/v2/provisionersdk" - "github.com/coder/websocket" ) func (api *API) provisionerDaemonsEnabledMW(next http.Handler) http.Handler { @@ -357,6 +358,7 @@ func (api *API) provisionerDaemonServe(rw http.ResponseWriter, r *http.Request) Clock: api.Clock, }, api.NotificationsEnqueuer, + &api.AGPL.PrebuildsReconciler, ) if err != nil { if !xerrors.Is(err, context.Canceled) { diff --git a/provisioner/terraform/executor.go b/provisioner/terraform/executor.go index 4be0f0749c372..6d3c6de5e902d 100644 --- a/provisioner/terraform/executor.go +++ b/provisioner/terraform/executor.go @@ -260,7 +260,7 @@ func getStateFilePath(workdir string) string { } // revive:disable-next-line:flag-parameter -func (e *executor) plan(ctx, killCtx context.Context, env, vars []string, logr logSink, destroy bool) (*proto.PlanComplete, error) { +func (e *executor) plan(ctx, killCtx context.Context, env, vars []string, logr logSink, metadata *proto.Metadata) (*proto.PlanComplete, error) { ctx, span := e.server.startTrace(ctx, tracing.FuncName()) defer span.End() @@ -276,6 +276,7 @@ func (e *executor) plan(ctx, killCtx context.Context, env, vars []string, logr l "-refresh=true", "-out=" + planfilePath, } + destroy := metadata.GetWorkspaceTransition() == proto.WorkspaceTransition_DESTROY if destroy { args = append(args, "-destroy") } @@ -304,7 +305,11 @@ func (e *executor) plan(ctx, killCtx context.Context, env, vars []string, logr l state, plan, err := e.planResources(ctx, killCtx, planfilePath) if err != nil { graphTimings.ingest(createGraphTimingsEvent(timingGraphErrored)) - return nil, err + return nil, xerrors.Errorf("plan resources: %w", err) + } + planJSON, err := json.Marshal(plan) + if err != nil { + return nil, xerrors.Errorf("marshal plan: %w", err) } graphTimings.ingest(createGraphTimingsEvent(timingGraphComplete)) @@ -315,13 +320,40 @@ func (e *executor) plan(ctx, killCtx context.Context, env, vars []string, logr l e.logger.Warn(ctx, "failed to archive terraform modules", slog.Error(err)) } + // When a prebuild claim attempt is made, log a warning if a resource is due to be replaced, since this will obviate + // the point of prebuilding if the expensive resource is replaced once claimed! + var ( + isPrebuildClaimAttempt = !destroy && metadata.GetPrebuiltWorkspaceBuildStage().IsPrebuiltWorkspaceClaim() + resReps []*proto.ResourceReplacement + ) + if repsFromPlan := findResourceReplacements(plan); len(repsFromPlan) > 0 { + if isPrebuildClaimAttempt { + // TODO(dannyk): we should log drift always (not just during prebuild claim attempts); we're validating that this output + // will not be overwhelming for end-users, but it'll certainly be super valuable for template admins + // to diagnose this resource replacement issue, at least. + // Once prebuilds moves out of beta, consider deleting this condition. + + // Lock held before calling (see top of method). + e.logDrift(ctx, killCtx, planfilePath, logr) + } + + resReps = make([]*proto.ResourceReplacement, 0, len(repsFromPlan)) + for n, p := range repsFromPlan { + resReps = append(resReps, &proto.ResourceReplacement{ + Resource: n, + Paths: p, + }) + } + } + msg := &proto.PlanComplete{ Parameters: state.Parameters, Resources: state.Resources, ExternalAuthProviders: state.ExternalAuthProviders, Timings: append(e.timings.aggregate(), graphTimings.aggregate()...), Presets: state.Presets, - Plan: plan, + Plan: planJSON, + ResourceReplacements: resReps, ModuleFiles: moduleFiles, } @@ -350,80 +382,16 @@ func onlyDataResources(sm tfjson.StateModule) tfjson.StateModule { return filtered } -func (e *executor) logResourceReplacements(ctx context.Context, plan *tfjson.Plan) { - if plan == nil { - return - } - - if len(plan.ResourceChanges) == 0 { - return - } - var ( - count int - replacements = make(map[string][]string, len(plan.ResourceChanges)) - ) - - for _, ch := range plan.ResourceChanges { - // No change, no problem! - if ch.Change == nil { - continue - } - - // No-op change, no problem! - if ch.Change.Actions.NoOp() { - continue - } - - // No replacements, no problem! - if len(ch.Change.ReplacePaths) == 0 { - continue - } - - // Replacing our resources, no problem! - if strings.Index(ch.Type, "coder_") == 0 { - continue - } - - for _, p := range ch.Change.ReplacePaths { - var path string - switch p := p.(type) { - case []interface{}: - segs := p - list := make([]string, 0, len(segs)) - for _, s := range segs { - list = append(list, fmt.Sprintf("%v", s)) - } - path = strings.Join(list, ".") - default: - path = fmt.Sprintf("%v", p) - } - - replacements[ch.Address] = append(replacements[ch.Address], path) - } - - count++ - } - - if count > 0 { - e.server.logger.Warn(ctx, "plan introduces resource changes", slog.F("count", count)) - for n, p := range replacements { - e.server.logger.Warn(ctx, "resource will be replaced", slog.F("name", n), slog.F("replacement_paths", strings.Join(p, ","))) - } - } -} - // planResources must only be called while the lock is held. -func (e *executor) planResources(ctx, killCtx context.Context, planfilePath string) (*State, json.RawMessage, error) { +func (e *executor) planResources(ctx, killCtx context.Context, planfilePath string) (*State, *tfjson.Plan, error) { ctx, span := e.server.startTrace(ctx, tracing.FuncName()) defer span.End() - plan, err := e.showPlan(ctx, killCtx, planfilePath) + plan, err := e.parsePlan(ctx, killCtx, planfilePath) if err != nil { return nil, nil, xerrors.Errorf("show terraform plan file: %w", err) } - e.logResourceReplacements(ctx, plan) - rawGraph, err := e.graph(ctx, killCtx) if err != nil { return nil, nil, xerrors.Errorf("graph: %w", err) @@ -447,16 +415,11 @@ func (e *executor) planResources(ctx, killCtx context.Context, planfilePath stri return nil, nil, err } - planJSON, err := json.Marshal(plan) - if err != nil { - return nil, nil, err - } - - return state, planJSON, nil + return state, plan, nil } -// showPlan must only be called while the lock is held. -func (e *executor) showPlan(ctx, killCtx context.Context, planfilePath string) (*tfjson.Plan, error) { +// parsePlan must only be called while the lock is held. +func (e *executor) parsePlan(ctx, killCtx context.Context, planfilePath string) (*tfjson.Plan, error) { ctx, span := e.server.startTrace(ctx, tracing.FuncName()) defer span.End() @@ -466,6 +429,64 @@ func (e *executor) showPlan(ctx, killCtx context.Context, planfilePath string) ( return p, err } +// logDrift must only be called while the lock is held. +// It will log the output of `terraform show`, which will show which resources have drifted from the known state. +func (e *executor) logDrift(ctx, killCtx context.Context, planfilePath string, logr logSink) { + stdout, stdoutDone := resourceReplaceLogWriter(logr, e.logger) + stderr, stderrDone := logWriter(logr, proto.LogLevel_ERROR) + defer func() { + _ = stdout.Close() + _ = stderr.Close() + <-stdoutDone + <-stderrDone + }() + + err := e.showPlan(ctx, killCtx, stdout, stderr, planfilePath) + if err != nil { + e.server.logger.Debug(ctx, "failed to log state drift", slog.Error(err)) + } +} + +// resourceReplaceLogWriter highlights log lines relating to resource replacement by elevating their log level. +// This will help template admins to visually find problematic resources easier. +// +// The WriteCloser must be closed by the caller to end logging, after which the returned channel will be closed to +// indicate that logging of the written data has finished. Failure to close the WriteCloser will leak a goroutine. +func resourceReplaceLogWriter(sink logSink, logger slog.Logger) (io.WriteCloser, <-chan struct{}) { + r, w := io.Pipe() + done := make(chan struct{}) + + go func() { + defer close(done) + + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := scanner.Bytes() + level := proto.LogLevel_INFO + + // Terraform indicates that a resource will be deleted and recreated by showing the change along with this substring. + if bytes.Contains(line, []byte("# forces replacement")) { + level = proto.LogLevel_WARN + } + + sink.ProvisionLog(level, string(line)) + } + if err := scanner.Err(); err != nil { + logger.Error(context.Background(), "failed to read terraform log", slog.Error(err)) + } + }() + return w, done +} + +// showPlan must only be called while the lock is held. +func (e *executor) showPlan(ctx, killCtx context.Context, stdoutWriter, stderrWriter io.WriteCloser, planfilePath string) error { + ctx, span := e.server.startTrace(ctx, tracing.FuncName()) + defer span.End() + + args := []string{"show", "-no-color", planfilePath} + return e.execWriteOutput(ctx, killCtx, args, e.basicEnv(), stdoutWriter, stderrWriter) +} + // graph must only be called while the lock is held. func (e *executor) graph(ctx, killCtx context.Context) (string, error) { ctx, span := e.server.startTrace(ctx, tracing.FuncName()) diff --git a/provisioner/terraform/provision.go b/provisioner/terraform/provision.go index f2a92b5745a87..84c630eec48fe 100644 --- a/provisioner/terraform/provision.go +++ b/provisioner/terraform/provision.go @@ -163,10 +163,7 @@ func (s *server) Plan( return provisionersdk.PlanErrorf("plan vars: %s", err) } - resp, err := e.plan( - ctx, killCtx, env, vars, sess, - request.Metadata.GetWorkspaceTransition() == proto.WorkspaceTransition_DESTROY, - ) + resp, err := e.plan(ctx, killCtx, env, vars, sess, request.Metadata) if err != nil { return provisionersdk.PlanErrorf("%s", err.Error()) } diff --git a/provisioner/terraform/resource_replacements.go b/provisioner/terraform/resource_replacements.go new file mode 100644 index 0000000000000..a2bbbb1802883 --- /dev/null +++ b/provisioner/terraform/resource_replacements.go @@ -0,0 +1,86 @@ +package terraform + +import ( + "fmt" + "strings" + + tfjson "github.com/hashicorp/terraform-json" +) + +type resourceReplacements map[string][]string + +// resourceReplacements finds all resources which would be replaced by the current plan, and the attribute paths which +// caused the replacement. +// +// NOTE: "replacement" in terraform terms means that a resource will have to be destroyed and replaced with a new resource +// since one of its immutable attributes was modified, which cannot be updated in-place. +func findResourceReplacements(plan *tfjson.Plan) resourceReplacements { + if plan == nil { + return nil + } + + // No changes, no problem! + if len(plan.ResourceChanges) == 0 { + return nil + } + + replacements := make(resourceReplacements, len(plan.ResourceChanges)) + + for _, ch := range plan.ResourceChanges { + // No change, no problem! + if ch.Change == nil { + continue + } + + // No-op change, no problem! + if ch.Change.Actions.NoOp() { + continue + } + + // No replacements, no problem! + if len(ch.Change.ReplacePaths) == 0 { + continue + } + + // Replacing our resources: could be a problem - but we ignore since they're "virtual" resources. If any of these + // resources' attributes are referenced by non-coder resources, those will show up as transitive changes there. + // i.e. if the coder_agent.id attribute is used in docker_container.env + // + // Replacing our resources is not strictly a problem in and of itself. + // + // NOTE: + // We may need to special-case coder_agent in the future. Currently, coder_agent is replaced on every build + // because it only supports Create but not Update: https://github.com/coder/terraform-provider-coder/blob/5648efb/provider/agent.go#L28 + // When we can modify an agent's attributes, some of which may be immutable (like "arch") and some may not (like "env"), + // then we'll have to handle this specifically. + // This will only become relevant once we support multiple agents: https://github.com/coder/coder/issues/17388 + if strings.Index(ch.Type, "coder_") == 0 { + continue + } + + // Replacements found, problem! + for _, val := range ch.Change.ReplacePaths { + var pathStr string + // Each path needs to be coerced into a string. All types except []interface{} can be coerced using fmt.Sprintf. + switch path := val.(type) { + case []interface{}: + // Found a slice of paths; coerce to string and join by ".". + segments := make([]string, 0, len(path)) + for _, seg := range path { + segments = append(segments, fmt.Sprintf("%v", seg)) + } + pathStr = strings.Join(segments, ".") + default: + pathStr = fmt.Sprintf("%v", path) + } + + replacements[ch.Address] = append(replacements[ch.Address], pathStr) + } + } + + if len(replacements) == 0 { + return nil + } + + return replacements +} diff --git a/provisioner/terraform/resource_replacements_internal_test.go b/provisioner/terraform/resource_replacements_internal_test.go new file mode 100644 index 0000000000000..4cca4ed396a43 --- /dev/null +++ b/provisioner/terraform/resource_replacements_internal_test.go @@ -0,0 +1,176 @@ +package terraform + +import ( + "testing" + + tfjson "github.com/hashicorp/terraform-json" + "github.com/stretchr/testify/require" +) + +func TestFindResourceReplacements(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + plan *tfjson.Plan + expected resourceReplacements + }{ + { + name: "nil plan", + }, + { + name: "no resource changes", + plan: &tfjson.Plan{}, + }, + { + name: "resource change with nil change", + plan: &tfjson.Plan{ + ResourceChanges: []*tfjson.ResourceChange{ + { + Address: "resource1", + }, + }, + }, + }, + { + name: "no-op action", + plan: &tfjson.Plan{ + ResourceChanges: []*tfjson.ResourceChange{ + { + Address: "resource1", + Change: &tfjson.Change{ + Actions: tfjson.Actions{tfjson.ActionNoop}, + }, + }, + }, + }, + }, + { + name: "empty replace paths", + plan: &tfjson.Plan{ + ResourceChanges: []*tfjson.ResourceChange{ + { + Address: "resource1", + Change: &tfjson.Change{ + Actions: tfjson.Actions{tfjson.ActionDelete, tfjson.ActionCreate}, + }, + }, + }, + }, + }, + { + name: "coder_* types are ignored", + plan: &tfjson.Plan{ + ResourceChanges: []*tfjson.ResourceChange{ + { + Address: "resource1", + Type: "coder_resource", + Change: &tfjson.Change{ + Actions: tfjson.Actions{tfjson.ActionDelete, tfjson.ActionCreate}, + ReplacePaths: []interface{}{"path1"}, + }, + }, + }, + }, + }, + { + name: "valid replacements - single path", + plan: &tfjson.Plan{ + ResourceChanges: []*tfjson.ResourceChange{ + { + Address: "resource1", + Type: "example_resource", + Change: &tfjson.Change{ + Actions: tfjson.Actions{tfjson.ActionDelete, tfjson.ActionCreate}, + ReplacePaths: []interface{}{"path1"}, + }, + }, + }, + }, + expected: resourceReplacements{ + "resource1": {"path1"}, + }, + }, + { + name: "valid replacements - multiple paths", + plan: &tfjson.Plan{ + ResourceChanges: []*tfjson.ResourceChange{ + { + Address: "resource1", + Type: "example_resource", + Change: &tfjson.Change{ + Actions: tfjson.Actions{tfjson.ActionDelete, tfjson.ActionCreate}, + ReplacePaths: []interface{}{"path1", "path2"}, + }, + }, + }, + }, + expected: resourceReplacements{ + "resource1": {"path1", "path2"}, + }, + }, + { + name: "complex replace path", + plan: &tfjson.Plan{ + ResourceChanges: []*tfjson.ResourceChange{ + { + Address: "resource1", + Type: "example_resource", + Change: &tfjson.Change{ + Actions: tfjson.Actions{tfjson.ActionDelete, tfjson.ActionCreate}, + ReplacePaths: []interface{}{ + []interface{}{"path", "to", "key"}, + }, + }, + }, + }, + }, + expected: resourceReplacements{ + "resource1": {"path.to.key"}, + }, + }, + { + name: "multiple changes", + plan: &tfjson.Plan{ + ResourceChanges: []*tfjson.ResourceChange{ + { + Address: "resource1", + Type: "example_resource", + Change: &tfjson.Change{ + Actions: tfjson.Actions{tfjson.ActionDelete, tfjson.ActionCreate}, + ReplacePaths: []interface{}{"path1"}, + }, + }, + { + Address: "resource2", + Type: "example_resource", + Change: &tfjson.Change{ + Actions: tfjson.Actions{tfjson.ActionDelete, tfjson.ActionCreate}, + ReplacePaths: []interface{}{"path2", "path3"}, + }, + }, + { + Address: "resource3", + Type: "coder_example", + Change: &tfjson.Change{ + Actions: tfjson.Actions{tfjson.ActionDelete, tfjson.ActionCreate}, + ReplacePaths: []interface{}{"ignored_path"}, + }, + }, + }, + }, + expected: resourceReplacements{ + "resource1": {"path1"}, + "resource2": {"path2", "path3"}, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + require.EqualValues(t, tc.expected, findResourceReplacements(tc.plan)) + }) + } +} diff --git a/provisionerd/proto/provisionerd.pb.go b/provisionerd/proto/provisionerd.pb.go index 9267431f928be..41bc91591e017 100644 --- a/provisionerd/proto/provisionerd.pb.go +++ b/provisionerd/proto/provisionerd.pb.go @@ -1223,10 +1223,11 @@ type CompletedJob_WorkspaceBuild struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - State []byte `protobuf:"bytes,1,opt,name=state,proto3" json:"state,omitempty"` - Resources []*proto.Resource `protobuf:"bytes,2,rep,name=resources,proto3" json:"resources,omitempty"` - Timings []*proto.Timing `protobuf:"bytes,3,rep,name=timings,proto3" json:"timings,omitempty"` - Modules []*proto.Module `protobuf:"bytes,4,rep,name=modules,proto3" json:"modules,omitempty"` + State []byte `protobuf:"bytes,1,opt,name=state,proto3" json:"state,omitempty"` + Resources []*proto.Resource `protobuf:"bytes,2,rep,name=resources,proto3" json:"resources,omitempty"` + Timings []*proto.Timing `protobuf:"bytes,3,rep,name=timings,proto3" json:"timings,omitempty"` + Modules []*proto.Module `protobuf:"bytes,4,rep,name=modules,proto3" json:"modules,omitempty"` + ResourceReplacements []*proto.ResourceReplacement `protobuf:"bytes,5,rep,name=resource_replacements,json=resourceReplacements,proto3" json:"resource_replacements,omitempty"` } func (x *CompletedJob_WorkspaceBuild) Reset() { @@ -1289,6 +1290,13 @@ func (x *CompletedJob_WorkspaceBuild) GetModules() []*proto.Module { return nil } +func (x *CompletedJob_WorkspaceBuild) GetResourceReplacements() []*proto.ResourceReplacement { + if x != nil { + return x.ResourceReplacements + } + return nil +} + type CompletedJob_TemplateImport struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1597,7 +1605,7 @@ var file_provisionerd_proto_provisionerd_proto_rawDesc = []byte{ 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, 0xb6, 0x09, 0x0a, 0x0c, 0x43, 0x6f, + 0x6e, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0x8d, 0x0a, 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, 0x65, 0x5f, 0x62, @@ -1616,7 +1624,7 @@ var file_provisionerd_proto_provisionerd_proto_rawDesc = []byte{ 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x48, 0x00, 0x52, 0x0e, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x44, 0x72, - 0x79, 0x52, 0x75, 0x6e, 0x1a, 0xb9, 0x01, 0x0a, 0x0e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, + 0x79, 0x52, 0x75, 0x6e, 0x1a, 0x90, 0x02, 0x0a, 0x0e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, @@ -1628,145 +1636,150 @@ var file_provisionerd_proto_provisionerd_proto_rawDesc = []byte{ 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, 0xd1, 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, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x52, 0x0e, 0x73, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x73, 0x12, 0x3c, 0x0a, 0x0e, 0x73, 0x74, 0x6f, 0x70, 0x5f, 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, 0x0d, 0x73, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x73, 0x12, 0x43, 0x0a, 0x0f, 0x72, 0x69, 0x63, 0x68, 0x5f, 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, 0x0e, 0x72, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, - 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x41, 0x0a, 0x1d, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, - 0x61, 0x6c, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, - 0x73, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x1a, 0x65, - 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, - 0x64, 0x65, 0x72, 0x73, 0x4e, 0x61, 0x6d, 0x65, 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, 0x38, 0x0a, 0x0d, - 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x06, 0x20, + 0x12, 0x55, 0x0a, 0x15, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x72, 0x65, 0x70, + 0x6c, 0x61, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x05, 0x20, 0x03, 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, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x52, 0x14, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x61, + 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x1a, 0xd1, 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, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x0e, 0x73, 0x74, 0x61, 0x72, + 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x3c, 0x0a, 0x0e, 0x73, 0x74, + 0x6f, 0x70, 0x5f, 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, 0x0d, 0x73, 0x74, 0x6f, 0x70, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x43, 0x0a, 0x0f, 0x72, 0x69, 0x63, 0x68, + 0x5f, 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, 0x0e, 0x72, + 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x41, 0x0a, + 0x1d, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x04, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x1a, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, + 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x4e, 0x61, 0x6d, 0x65, 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, 0x38, 0x0a, 0x0d, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x6d, 0x6f, 0x64, + 0x75, 0x6c, 0x65, 0x73, 0x18, 0x06, 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, + 0x0c, 0x73, 0x74, 0x61, 0x72, 0x74, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x36, 0x0a, + 0x0c, 0x73, 0x74, 0x6f, 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, 0x0c, 0x73, 0x74, 0x61, 0x72, 0x74, 0x4d, - 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x36, 0x0a, 0x0c, 0x73, 0x74, 0x6f, 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, 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, 0x12, 0x12, 0x0a, - 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x70, 0x6c, 0x61, - 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x5f, 0x66, 0x69, 0x6c, 0x65, - 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x46, - 0x69, 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, 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, + 0x72, 0x2e, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x52, 0x0b, 0x73, 0x74, 0x6f, 0x70, 0x4d, 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, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x09, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x6f, 0x64, 0x75, + 0x6c, 0x65, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, + 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x46, 0x69, 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, 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, } var ( @@ -1815,9 +1828,10 @@ var file_provisionerd_proto_provisionerd_proto_goTypes = []interface{}{ (*proto.Timing)(nil), // 28: provisioner.Timing (*proto.Resource)(nil), // 29: provisioner.Resource (*proto.Module)(nil), // 30: provisioner.Module - (*proto.RichParameter)(nil), // 31: provisioner.RichParameter - (*proto.ExternalAuthProviderResource)(nil), // 32: provisioner.ExternalAuthProviderResource - (*proto.Preset)(nil), // 33: provisioner.Preset + (*proto.ResourceReplacement)(nil), // 31: provisioner.ResourceReplacement + (*proto.RichParameter)(nil), // 32: provisioner.RichParameter + (*proto.ExternalAuthProviderResource)(nil), // 33: provisioner.ExternalAuthProviderResource + (*proto.Preset)(nil), // 34: provisioner.Preset } var file_provisionerd_proto_provisionerd_proto_depIdxs = []int32{ 11, // 0: provisionerd.AcquiredJob.workspace_build:type_name -> provisionerd.AcquiredJob.WorkspaceBuild @@ -1851,32 +1865,33 @@ var file_provisionerd_proto_provisionerd_proto_depIdxs = []int32{ 29, // 28: provisionerd.CompletedJob.WorkspaceBuild.resources:type_name -> provisioner.Resource 28, // 29: provisionerd.CompletedJob.WorkspaceBuild.timings:type_name -> provisioner.Timing 30, // 30: provisionerd.CompletedJob.WorkspaceBuild.modules:type_name -> provisioner.Module - 29, // 31: provisionerd.CompletedJob.TemplateImport.start_resources:type_name -> provisioner.Resource - 29, // 32: provisionerd.CompletedJob.TemplateImport.stop_resources:type_name -> provisioner.Resource - 31, // 33: provisionerd.CompletedJob.TemplateImport.rich_parameters:type_name -> provisioner.RichParameter - 32, // 34: provisionerd.CompletedJob.TemplateImport.external_auth_providers:type_name -> provisioner.ExternalAuthProviderResource - 30, // 35: provisionerd.CompletedJob.TemplateImport.start_modules:type_name -> provisioner.Module - 30, // 36: provisionerd.CompletedJob.TemplateImport.stop_modules:type_name -> provisioner.Module - 33, // 37: provisionerd.CompletedJob.TemplateImport.presets:type_name -> provisioner.Preset - 29, // 38: provisionerd.CompletedJob.TemplateDryRun.resources:type_name -> provisioner.Resource - 30, // 39: provisionerd.CompletedJob.TemplateDryRun.modules:type_name -> provisioner.Module - 1, // 40: provisionerd.ProvisionerDaemon.AcquireJob:input_type -> provisionerd.Empty - 10, // 41: provisionerd.ProvisionerDaemon.AcquireJobWithCancel:input_type -> provisionerd.CancelAcquire - 8, // 42: provisionerd.ProvisionerDaemon.CommitQuota:input_type -> provisionerd.CommitQuotaRequest - 6, // 43: provisionerd.ProvisionerDaemon.UpdateJob:input_type -> provisionerd.UpdateJobRequest - 3, // 44: provisionerd.ProvisionerDaemon.FailJob:input_type -> provisionerd.FailedJob - 4, // 45: provisionerd.ProvisionerDaemon.CompleteJob:input_type -> provisionerd.CompletedJob - 2, // 46: provisionerd.ProvisionerDaemon.AcquireJob:output_type -> provisionerd.AcquiredJob - 2, // 47: provisionerd.ProvisionerDaemon.AcquireJobWithCancel:output_type -> provisionerd.AcquiredJob - 9, // 48: provisionerd.ProvisionerDaemon.CommitQuota:output_type -> provisionerd.CommitQuotaResponse - 7, // 49: provisionerd.ProvisionerDaemon.UpdateJob:output_type -> provisionerd.UpdateJobResponse - 1, // 50: provisionerd.ProvisionerDaemon.FailJob:output_type -> provisionerd.Empty - 1, // 51: provisionerd.ProvisionerDaemon.CompleteJob:output_type -> provisionerd.Empty - 46, // [46:52] is the sub-list for method output_type - 40, // [40:46] is the sub-list for method input_type - 40, // [40:40] is the sub-list for extension type_name - 40, // [40:40] is the sub-list for extension extendee - 0, // [0:40] is the sub-list for field type_name + 31, // 31: provisionerd.CompletedJob.WorkspaceBuild.resource_replacements:type_name -> provisioner.ResourceReplacement + 29, // 32: provisionerd.CompletedJob.TemplateImport.start_resources:type_name -> provisioner.Resource + 29, // 33: provisionerd.CompletedJob.TemplateImport.stop_resources:type_name -> provisioner.Resource + 32, // 34: provisionerd.CompletedJob.TemplateImport.rich_parameters:type_name -> provisioner.RichParameter + 33, // 35: provisionerd.CompletedJob.TemplateImport.external_auth_providers:type_name -> provisioner.ExternalAuthProviderResource + 30, // 36: provisionerd.CompletedJob.TemplateImport.start_modules:type_name -> provisioner.Module + 30, // 37: provisionerd.CompletedJob.TemplateImport.stop_modules:type_name -> provisioner.Module + 34, // 38: provisionerd.CompletedJob.TemplateImport.presets:type_name -> provisioner.Preset + 29, // 39: provisionerd.CompletedJob.TemplateDryRun.resources:type_name -> provisioner.Resource + 30, // 40: provisionerd.CompletedJob.TemplateDryRun.modules:type_name -> provisioner.Module + 1, // 41: provisionerd.ProvisionerDaemon.AcquireJob:input_type -> provisionerd.Empty + 10, // 42: provisionerd.ProvisionerDaemon.AcquireJobWithCancel:input_type -> provisionerd.CancelAcquire + 8, // 43: provisionerd.ProvisionerDaemon.CommitQuota:input_type -> provisionerd.CommitQuotaRequest + 6, // 44: provisionerd.ProvisionerDaemon.UpdateJob:input_type -> provisionerd.UpdateJobRequest + 3, // 45: provisionerd.ProvisionerDaemon.FailJob:input_type -> provisionerd.FailedJob + 4, // 46: provisionerd.ProvisionerDaemon.CompleteJob:input_type -> provisionerd.CompletedJob + 2, // 47: provisionerd.ProvisionerDaemon.AcquireJob:output_type -> provisionerd.AcquiredJob + 2, // 48: provisionerd.ProvisionerDaemon.AcquireJobWithCancel:output_type -> provisionerd.AcquiredJob + 9, // 49: provisionerd.ProvisionerDaemon.CommitQuota:output_type -> provisionerd.CommitQuotaResponse + 7, // 50: provisionerd.ProvisionerDaemon.UpdateJob:output_type -> provisionerd.UpdateJobResponse + 1, // 51: provisionerd.ProvisionerDaemon.FailJob:output_type -> provisionerd.Empty + 1, // 52: provisionerd.ProvisionerDaemon.CompleteJob:output_type -> provisionerd.Empty + 47, // [47:53] is the sub-list for method output_type + 41, // [41:47] is the sub-list for method input_type + 41, // [41:41] is the sub-list for extension type_name + 41, // [41:41] is the sub-list for extension extendee + 0, // [0:41] 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 25bd1aed4f307..0accc48f00a58 100644 --- a/provisionerd/proto/provisionerd.proto +++ b/provisionerd/proto/provisionerd.proto @@ -79,6 +79,7 @@ message CompletedJob { repeated provisioner.Resource resources = 2; repeated provisioner.Timing timings = 3; repeated provisioner.Module modules = 4; + repeated provisioner.ResourceReplacement resource_replacements = 5; } message TemplateImport { repeated provisioner.Resource start_resources = 1; diff --git a/provisionerd/proto/version.go b/provisionerd/proto/version.go index 5e26a5909c060..58f4548404e7a 100644 --- a/provisionerd/proto/version.go +++ b/provisionerd/proto/version.go @@ -20,6 +20,7 @@ import "github.com/coder/coder/v2/apiversion" // the previous values for the `terraform apply` to enforce monotonicity // in the terraform provider. // - Add new field named `running_agent_auth_tokens` to provisioner job metadata +// - Add new field named `resource_replacements` in PlanComplete & CompletedJob.WorkspaceBuild. const ( CurrentMajor = 1 CurrentMinor = 5 diff --git a/provisionerd/runner/runner.go b/provisionerd/runner/runner.go index 12a28bbdee6ec..ed1f134556fba 100644 --- a/provisionerd/runner/runner.go +++ b/provisionerd/runner/runner.go @@ -1065,6 +1065,8 @@ func (r *Runner) runWorkspaceBuild(ctx context.Context) (*proto.CompletedJob, *p // called by `plan`. `apply` does not modify them, so we can use the // modules from the plan response. Modules: planComplete.Modules, + // Resource replacements are discovered at plan time, only. + ResourceReplacements: planComplete.ResourceReplacements, }, }, }, nil diff --git a/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go index c0bf8a533d023..8f5260e902bc8 100644 --- a/provisionersdk/proto/provisioner.pb.go +++ b/provisionersdk/proto/provisioner.pb.go @@ -914,6 +914,61 @@ func (x *PresetParameter) GetValue() string { return "" } +type ResourceReplacement struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Resource string `protobuf:"bytes,1,opt,name=resource,proto3" json:"resource,omitempty"` + Paths []string `protobuf:"bytes,2,rep,name=paths,proto3" json:"paths,omitempty"` +} + +func (x *ResourceReplacement) Reset() { + *x = ResourceReplacement{} + if protoimpl.UnsafeEnabled { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ResourceReplacement) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResourceReplacement) ProtoMessage() {} + +func (x *ResourceReplacement) ProtoReflect() protoreflect.Message { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[8] + 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 ResourceReplacement.ProtoReflect.Descriptor instead. +func (*ResourceReplacement) Descriptor() ([]byte, []int) { + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{8} +} + +func (x *ResourceReplacement) GetResource() string { + if x != nil { + return x.Resource + } + return "" +} + +func (x *ResourceReplacement) GetPaths() []string { + if x != nil { + return x.Paths + } + return nil +} + // VariableValue holds the key/value mapping of a Terraform variable. type VariableValue struct { state protoimpl.MessageState @@ -928,7 +983,7 @@ type VariableValue struct { func (x *VariableValue) Reset() { *x = VariableValue{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[8] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -941,7 +996,7 @@ func (x *VariableValue) String() string { func (*VariableValue) ProtoMessage() {} func (x *VariableValue) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[8] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -954,7 +1009,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{8} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{9} } func (x *VariableValue) GetName() string { @@ -991,7 +1046,7 @@ type Log struct { func (x *Log) Reset() { *x = Log{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[9] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1004,7 +1059,7 @@ func (x *Log) String() string { func (*Log) ProtoMessage() {} func (x *Log) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[9] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1017,7 +1072,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{9} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{10} } func (x *Log) GetLevel() LogLevel { @@ -1045,7 +1100,7 @@ type InstanceIdentityAuth struct { func (x *InstanceIdentityAuth) Reset() { *x = InstanceIdentityAuth{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[10] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1058,7 +1113,7 @@ func (x *InstanceIdentityAuth) String() string { func (*InstanceIdentityAuth) ProtoMessage() {} func (x *InstanceIdentityAuth) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[10] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1071,7 +1126,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{10} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{11} } func (x *InstanceIdentityAuth) GetInstanceId() string { @@ -1093,7 +1148,7 @@ type ExternalAuthProviderResource struct { func (x *ExternalAuthProviderResource) Reset() { *x = ExternalAuthProviderResource{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[11] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1106,7 +1161,7 @@ func (x *ExternalAuthProviderResource) String() string { func (*ExternalAuthProviderResource) ProtoMessage() {} func (x *ExternalAuthProviderResource) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[11] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[12] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1119,7 +1174,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{11} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{12} } func (x *ExternalAuthProviderResource) GetId() string { @@ -1148,7 +1203,7 @@ type ExternalAuthProvider struct { func (x *ExternalAuthProvider) Reset() { *x = ExternalAuthProvider{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[12] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1161,7 +1216,7 @@ func (x *ExternalAuthProvider) String() string { func (*ExternalAuthProvider) ProtoMessage() {} func (x *ExternalAuthProvider) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[12] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1174,7 +1229,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{12} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{13} } func (x *ExternalAuthProvider) GetId() string { @@ -1228,7 +1283,7 @@ type Agent struct { func (x *Agent) Reset() { *x = Agent{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[13] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1241,7 +1296,7 @@ func (x *Agent) String() string { func (*Agent) ProtoMessage() {} func (x *Agent) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[13] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1254,7 +1309,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{13} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{14} } func (x *Agent) GetId() string { @@ -1425,7 +1480,7 @@ type ResourcesMonitoring struct { func (x *ResourcesMonitoring) Reset() { *x = ResourcesMonitoring{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[14] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1438,7 +1493,7 @@ func (x *ResourcesMonitoring) String() string { func (*ResourcesMonitoring) ProtoMessage() {} func (x *ResourcesMonitoring) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[14] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1451,7 +1506,7 @@ func (x *ResourcesMonitoring) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourcesMonitoring.ProtoReflect.Descriptor instead. func (*ResourcesMonitoring) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{14} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{15} } func (x *ResourcesMonitoring) GetMemory() *MemoryResourceMonitor { @@ -1480,7 +1535,7 @@ type MemoryResourceMonitor struct { func (x *MemoryResourceMonitor) Reset() { *x = MemoryResourceMonitor{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[15] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1493,7 +1548,7 @@ func (x *MemoryResourceMonitor) String() string { func (*MemoryResourceMonitor) ProtoMessage() {} func (x *MemoryResourceMonitor) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[15] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1506,7 +1561,7 @@ func (x *MemoryResourceMonitor) ProtoReflect() protoreflect.Message { // Deprecated: Use MemoryResourceMonitor.ProtoReflect.Descriptor instead. func (*MemoryResourceMonitor) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{15} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{16} } func (x *MemoryResourceMonitor) GetEnabled() bool { @@ -1536,7 +1591,7 @@ type VolumeResourceMonitor struct { func (x *VolumeResourceMonitor) Reset() { *x = VolumeResourceMonitor{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[16] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1549,7 +1604,7 @@ func (x *VolumeResourceMonitor) String() string { func (*VolumeResourceMonitor) ProtoMessage() {} func (x *VolumeResourceMonitor) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[16] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1562,7 +1617,7 @@ func (x *VolumeResourceMonitor) ProtoReflect() protoreflect.Message { // Deprecated: Use VolumeResourceMonitor.ProtoReflect.Descriptor instead. func (*VolumeResourceMonitor) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{16} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{17} } func (x *VolumeResourceMonitor) GetPath() string { @@ -1601,7 +1656,7 @@ type DisplayApps struct { func (x *DisplayApps) Reset() { *x = DisplayApps{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[17] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1614,7 +1669,7 @@ func (x *DisplayApps) String() string { func (*DisplayApps) ProtoMessage() {} func (x *DisplayApps) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[17] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1627,7 +1682,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{17} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{18} } func (x *DisplayApps) GetVscode() bool { @@ -1677,7 +1732,7 @@ type Env struct { func (x *Env) Reset() { *x = Env{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[18] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1690,7 +1745,7 @@ func (x *Env) String() string { func (*Env) ProtoMessage() {} func (x *Env) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[18] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1703,7 +1758,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{18} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{19} } func (x *Env) GetName() string { @@ -1740,7 +1795,7 @@ type Script struct { func (x *Script) Reset() { *x = Script{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[19] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1753,7 +1808,7 @@ func (x *Script) String() string { func (*Script) ProtoMessage() {} func (x *Script) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[19] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1766,7 +1821,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{19} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{20} } func (x *Script) GetDisplayName() string { @@ -1845,7 +1900,7 @@ type Devcontainer struct { func (x *Devcontainer) Reset() { *x = Devcontainer{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[20] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1858,7 +1913,7 @@ func (x *Devcontainer) String() string { func (*Devcontainer) ProtoMessage() {} func (x *Devcontainer) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[20] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1871,7 +1926,7 @@ func (x *Devcontainer) ProtoReflect() protoreflect.Message { // Deprecated: Use Devcontainer.ProtoReflect.Descriptor instead. func (*Devcontainer) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{20} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{21} } func (x *Devcontainer) GetWorkspaceFolder() string { @@ -1920,7 +1975,7 @@ type App struct { func (x *App) Reset() { *x = App{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[21] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1933,7 +1988,7 @@ func (x *App) String() string { func (*App) ProtoMessage() {} func (x *App) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[21] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[22] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1946,7 +2001,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{21} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{22} } func (x *App) GetSlug() string { @@ -2047,7 +2102,7 @@ type Healthcheck struct { func (x *Healthcheck) Reset() { *x = Healthcheck{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[22] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2060,7 +2115,7 @@ func (x *Healthcheck) String() string { func (*Healthcheck) ProtoMessage() {} func (x *Healthcheck) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[22] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[23] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2073,7 +2128,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{22} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{23} } func (x *Healthcheck) GetUrl() string { @@ -2117,7 +2172,7 @@ type Resource struct { func (x *Resource) Reset() { *x = Resource{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[23] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2130,7 +2185,7 @@ func (x *Resource) String() string { func (*Resource) ProtoMessage() {} func (x *Resource) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[23] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[24] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2143,7 +2198,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{23} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{24} } func (x *Resource) GetName() string { @@ -2223,7 +2278,7 @@ type Module struct { func (x *Module) Reset() { *x = Module{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[24] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2236,7 +2291,7 @@ func (x *Module) String() string { func (*Module) ProtoMessage() {} func (x *Module) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[24] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[25] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2249,7 +2304,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{24} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{25} } func (x *Module) GetSource() string { @@ -2292,7 +2347,7 @@ type Role struct { func (x *Role) Reset() { *x = Role{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[25] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2305,7 +2360,7 @@ func (x *Role) String() string { func (*Role) ProtoMessage() {} func (x *Role) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[25] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[26] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2318,7 +2373,7 @@ func (x *Role) ProtoReflect() protoreflect.Message { // Deprecated: Use Role.ProtoReflect.Descriptor instead. func (*Role) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{25} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{26} } func (x *Role) GetName() string { @@ -2347,7 +2402,7 @@ type RunningAgentAuthToken struct { func (x *RunningAgentAuthToken) Reset() { *x = RunningAgentAuthToken{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[26] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2360,7 +2415,7 @@ func (x *RunningAgentAuthToken) String() string { func (*RunningAgentAuthToken) ProtoMessage() {} func (x *RunningAgentAuthToken) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[26] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[27] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2373,7 +2428,7 @@ func (x *RunningAgentAuthToken) ProtoReflect() protoreflect.Message { // Deprecated: Use RunningAgentAuthToken.ProtoReflect.Descriptor instead. func (*RunningAgentAuthToken) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{26} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{27} } func (x *RunningAgentAuthToken) GetAgentId() string { @@ -2422,7 +2477,7 @@ type Metadata struct { func (x *Metadata) Reset() { *x = Metadata{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[27] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2435,7 +2490,7 @@ func (x *Metadata) String() string { func (*Metadata) ProtoMessage() {} func (x *Metadata) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[27] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[28] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2448,7 +2503,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{27} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{28} } func (x *Metadata) GetCoderUrl() string { @@ -2614,7 +2669,7 @@ type Config struct { func (x *Config) Reset() { *x = Config{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[28] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2627,7 +2682,7 @@ func (x *Config) String() string { func (*Config) ProtoMessage() {} func (x *Config) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[28] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[29] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2640,7 +2695,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{28} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{29} } func (x *Config) GetTemplateSourceArchive() []byte { @@ -2674,7 +2729,7 @@ type ParseRequest struct { func (x *ParseRequest) Reset() { *x = ParseRequest{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[29] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2687,7 +2742,7 @@ func (x *ParseRequest) String() string { func (*ParseRequest) ProtoMessage() {} func (x *ParseRequest) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[29] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[30] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2700,7 +2755,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{29} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{30} } // ParseComplete indicates a request to parse completed. @@ -2718,7 +2773,7 @@ type ParseComplete struct { func (x *ParseComplete) Reset() { *x = ParseComplete{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[30] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2731,7 +2786,7 @@ func (x *ParseComplete) String() string { func (*ParseComplete) ProtoMessage() {} func (x *ParseComplete) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[30] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[31] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2744,7 +2799,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{30} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{31} } func (x *ParseComplete) GetError() string { @@ -2791,7 +2846,7 @@ type PlanRequest struct { func (x *PlanRequest) Reset() { *x = PlanRequest{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[31] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2804,7 +2859,7 @@ func (x *PlanRequest) String() string { func (*PlanRequest) ProtoMessage() {} func (x *PlanRequest) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[31] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[32] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2817,7 +2872,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{31} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{32} } func (x *PlanRequest) GetMetadata() *Metadata { @@ -2870,12 +2925,13 @@ type PlanComplete struct { Presets []*Preset `protobuf:"bytes,8,rep,name=presets,proto3" json:"presets,omitempty"` Plan []byte `protobuf:"bytes,9,opt,name=plan,proto3" json:"plan,omitempty"` ModuleFiles []byte `protobuf:"bytes,10,opt,name=module_files,json=moduleFiles,proto3" json:"module_files,omitempty"` + ResourceReplacements []*ResourceReplacement `protobuf:"bytes,11,rep,name=resource_replacements,json=resourceReplacements,proto3" json:"resource_replacements,omitempty"` } func (x *PlanComplete) Reset() { *x = PlanComplete{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[32] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2888,7 +2944,7 @@ func (x *PlanComplete) String() string { func (*PlanComplete) ProtoMessage() {} func (x *PlanComplete) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[32] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[33] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2901,7 +2957,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{32} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{33} } func (x *PlanComplete) GetError() string { @@ -2967,6 +3023,13 @@ func (x *PlanComplete) GetModuleFiles() []byte { return nil } +func (x *PlanComplete) GetResourceReplacements() []*ResourceReplacement { + if x != nil { + return x.ResourceReplacements + } + 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 { @@ -2980,7 +3043,7 @@ type ApplyRequest struct { func (x *ApplyRequest) Reset() { *x = ApplyRequest{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[33] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2993,7 +3056,7 @@ func (x *ApplyRequest) String() string { func (*ApplyRequest) ProtoMessage() {} func (x *ApplyRequest) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[33] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[34] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3006,7 +3069,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{33} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{34} } func (x *ApplyRequest) GetMetadata() *Metadata { @@ -3033,7 +3096,7 @@ type ApplyComplete struct { func (x *ApplyComplete) Reset() { *x = ApplyComplete{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[34] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3046,7 +3109,7 @@ func (x *ApplyComplete) String() string { func (*ApplyComplete) ProtoMessage() {} func (x *ApplyComplete) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[34] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[35] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3059,7 +3122,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{34} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{35} } func (x *ApplyComplete) GetState() []byte { @@ -3121,7 +3184,7 @@ type Timing struct { func (x *Timing) Reset() { *x = Timing{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[35] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3134,7 +3197,7 @@ func (x *Timing) String() string { func (*Timing) ProtoMessage() {} func (x *Timing) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[35] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[36] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3147,7 +3210,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{35} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{36} } func (x *Timing) GetStart() *timestamppb.Timestamp { @@ -3209,7 +3272,7 @@ type CancelRequest struct { func (x *CancelRequest) Reset() { *x = CancelRequest{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[36] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3222,7 +3285,7 @@ func (x *CancelRequest) String() string { func (*CancelRequest) ProtoMessage() {} func (x *CancelRequest) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[36] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[37] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3235,7 +3298,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{36} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{37} } type Request struct { @@ -3256,7 +3319,7 @@ type Request struct { func (x *Request) Reset() { *x = Request{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[37] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3269,7 +3332,7 @@ func (x *Request) String() string { func (*Request) ProtoMessage() {} func (x *Request) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[37] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[38] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3282,7 +3345,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{37} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{38} } func (m *Request) GetType() isRequest_Type { @@ -3378,7 +3441,7 @@ type Response struct { func (x *Response) Reset() { *x = Response{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[38] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3391,7 +3454,7 @@ func (x *Response) String() string { func (*Response) ProtoMessage() {} func (x *Response) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[38] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[39] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3404,7 +3467,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{38} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{39} } func (m *Response) GetType() isResponse_Type { @@ -3486,7 +3549,7 @@ type Agent_Metadata struct { func (x *Agent_Metadata) Reset() { *x = Agent_Metadata{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[39] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3499,7 +3562,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[39] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[40] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3512,7 +3575,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{13, 0} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{14, 0} } func (x *Agent_Metadata) GetKey() string { @@ -3571,7 +3634,7 @@ type Resource_Metadata struct { func (x *Resource_Metadata) Reset() { *x = Resource_Metadata{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[41] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3584,7 +3647,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[41] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[42] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3597,7 +3660,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{23, 0} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{24, 0} } func (x *Resource_Metadata) GetKey() string { @@ -3715,388 +3778,398 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 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, 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, 0xb6, 0x08, 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, 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, 0x12, 0x3f, 0x0a, 0x0d, 0x64, 0x65, 0x76, - 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x19, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x44, - 0x65, 0x76, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0d, 0x64, 0x65, 0x76, - 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 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, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x47, 0x0a, 0x13, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, + 0x14, 0x0a, 0x05, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, + 0x70, 0x61, 0x74, 0x68, 0x73, 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, 0xb6, 0x08, 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, 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, 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, 0x6e, - 0x0a, 0x0c, 0x44, 0x65, 0x76, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x29, - 0x0a, 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x66, 0x6f, 0x6c, 0x64, - 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, - 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x50, 0x61, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 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, + 0x6e, 0x67, 0x12, 0x3f, 0x0a, 0x0d, 0x64, 0x65, 0x76, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, + 0x65, 0x72, 0x73, 0x18, 0x19, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x44, 0x65, 0x76, 0x63, 0x6f, 0x6e, 0x74, 0x61, + 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0d, 0x64, 0x65, 0x76, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, + 0x65, 0x72, 0x73, 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, 0x6e, 0x0a, 0x0c, 0x44, 0x65, 0x76, 0x63, 0x6f, + 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x29, 0x0a, 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x5f, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x46, 0x6f, 0x6c, 0x64, + 0x65, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x70, 0x61, 0x74, + 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x50, + 0x61, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 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, - 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, 0x5e, 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, 0x12, 0x10, 0x0a, 0x03, 0x64, 0x69, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x03, 0x64, 0x69, 0x72, 0x22, 0x31, 0x0a, 0x04, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x12, 0x0a, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x12, 0x15, 0x0a, 0x06, 0x6f, 0x72, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x6f, 0x72, 0x67, 0x49, 0x64, 0x22, 0x48, 0x0a, 0x15, 0x52, 0x75, 0x6e, 0x6e, - 0x69, 0x6e, 0x67, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, - 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, - 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, - 0x65, 0x6e, 0x22, 0xca, 0x09, 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, 0x12, 0x4e, 0x0a, 0x1a, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, - 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x72, 0x62, 0x61, 0x63, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x18, - 0x13, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x6f, 0x6c, 0x65, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x52, 0x62, 0x61, 0x63, 0x52, 0x6f, 0x6c, 0x65, - 0x73, 0x12, 0x6d, 0x0a, 0x1e, 0x70, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x5f, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x73, 0x74, - 0x61, 0x67, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x28, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x74, - 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x53, 0x74, - 0x61, 0x67, 0x65, 0x52, 0x1b, 0x70, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x57, 0x6f, 0x72, - 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x53, 0x74, 0x61, 0x67, 0x65, - 0x12, 0x5d, 0x0a, 0x19, 0x72, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x15, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, - 0x72, 0x2e, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x41, 0x75, - 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x16, 0x72, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, - 0x41, 0x67, 0x65, 0x6e, 0x74, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 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, 0x92, 0x03, 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, - 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, 0x12, 0x5b, 0x0a, 0x19, 0x70, 0x72, - 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, - 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, + 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, 0x5e, + 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, 0x12, 0x10, 0x0a, 0x03, + 0x64, 0x69, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x64, 0x69, 0x72, 0x22, 0x31, + 0x0a, 0x04, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x15, 0x0a, 0x06, 0x6f, 0x72, + 0x67, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6f, 0x72, 0x67, 0x49, + 0x64, 0x22, 0x48, 0x0a, 0x15, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x41, 0x67, 0x65, 0x6e, + 0x74, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x67, + 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x67, + 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xca, 0x09, 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, 0x12, 0x4e, 0x0a, 0x1a, 0x77, 0x6f, + 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x72, 0x62, + 0x61, 0x63, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x18, 0x13, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, + 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x6f, 0x6c, + 0x65, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, + 0x72, 0x52, 0x62, 0x61, 0x63, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x12, 0x6d, 0x0a, 0x1e, 0x70, 0x72, + 0x65, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x5f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x67, 0x65, 0x18, 0x14, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x28, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, + 0x2e, 0x50, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x53, 0x74, 0x61, 0x67, 0x65, 0x52, 0x1b, 0x70, 0x72, + 0x65, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, + 0x75, 0x69, 0x6c, 0x64, 0x53, 0x74, 0x61, 0x67, 0x65, 0x12, 0x5d, 0x0a, 0x19, 0x72, 0x75, 0x6e, + 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, + 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x15, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x75, 0x6e, 0x6e, 0x69, + 0x6e, 0x67, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, + 0x52, 0x16, 0x72, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x41, 0x75, + 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 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, 0x92, 0x03, 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, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x17, - 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, - 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0xbc, 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, 0x12, - 0x12, 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x70, - 0x6c, 0x61, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x5f, 0x66, 0x69, - 0x6c, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x6d, 0x6f, 0x64, 0x75, 0x6c, - 0x65, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x22, 0x41, 0x0a, 0x0c, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, + 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, 0x12, 0x5b, 0x0a, 0x19, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x5f, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, + 0x18, 0x05, 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, 0x17, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, + 0x73, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, + 0x22, 0x93, 0x04, 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, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, + 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x12, 0x21, 0x0a, 0x0c, + 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x0b, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, + 0x55, 0x0a, 0x15, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x72, 0x65, 0x70, 0x6c, + 0x61, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x0b, 0x20, 0x03, 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, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x52, 0x14, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, + 0x65, 0x6d, 0x65, 0x6e, 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, @@ -4214,7 +4287,7 @@ func file_provisionersdk_proto_provisioner_proto_rawDescGZIP() []byte { } var file_provisionersdk_proto_provisioner_proto_enumTypes = make([]protoimpl.EnumInfo, 6) -var file_provisionersdk_proto_provisioner_proto_msgTypes = make([]protoimpl.MessageInfo, 43) +var file_provisionersdk_proto_provisioner_proto_msgTypes = make([]protoimpl.MessageInfo, 44) var file_provisionersdk_proto_provisioner_proto_goTypes = []interface{}{ (LogLevel)(0), // 0: provisioner.LogLevel (AppSharingLevel)(0), // 1: provisioner.AppSharingLevel @@ -4230,104 +4303,106 @@ var file_provisionersdk_proto_provisioner_proto_goTypes = []interface{}{ (*Prebuild)(nil), // 11: provisioner.Prebuild (*Preset)(nil), // 12: provisioner.Preset (*PresetParameter)(nil), // 13: provisioner.PresetParameter - (*VariableValue)(nil), // 14: provisioner.VariableValue - (*Log)(nil), // 15: provisioner.Log - (*InstanceIdentityAuth)(nil), // 16: provisioner.InstanceIdentityAuth - (*ExternalAuthProviderResource)(nil), // 17: provisioner.ExternalAuthProviderResource - (*ExternalAuthProvider)(nil), // 18: provisioner.ExternalAuthProvider - (*Agent)(nil), // 19: provisioner.Agent - (*ResourcesMonitoring)(nil), // 20: provisioner.ResourcesMonitoring - (*MemoryResourceMonitor)(nil), // 21: provisioner.MemoryResourceMonitor - (*VolumeResourceMonitor)(nil), // 22: provisioner.VolumeResourceMonitor - (*DisplayApps)(nil), // 23: provisioner.DisplayApps - (*Env)(nil), // 24: provisioner.Env - (*Script)(nil), // 25: provisioner.Script - (*Devcontainer)(nil), // 26: provisioner.Devcontainer - (*App)(nil), // 27: provisioner.App - (*Healthcheck)(nil), // 28: provisioner.Healthcheck - (*Resource)(nil), // 29: provisioner.Resource - (*Module)(nil), // 30: provisioner.Module - (*Role)(nil), // 31: provisioner.Role - (*RunningAgentAuthToken)(nil), // 32: provisioner.RunningAgentAuthToken - (*Metadata)(nil), // 33: provisioner.Metadata - (*Config)(nil), // 34: provisioner.Config - (*ParseRequest)(nil), // 35: provisioner.ParseRequest - (*ParseComplete)(nil), // 36: provisioner.ParseComplete - (*PlanRequest)(nil), // 37: provisioner.PlanRequest - (*PlanComplete)(nil), // 38: provisioner.PlanComplete - (*ApplyRequest)(nil), // 39: provisioner.ApplyRequest - (*ApplyComplete)(nil), // 40: provisioner.ApplyComplete - (*Timing)(nil), // 41: provisioner.Timing - (*CancelRequest)(nil), // 42: provisioner.CancelRequest - (*Request)(nil), // 43: provisioner.Request - (*Response)(nil), // 44: provisioner.Response - (*Agent_Metadata)(nil), // 45: provisioner.Agent.Metadata - nil, // 46: provisioner.Agent.EnvEntry - (*Resource_Metadata)(nil), // 47: provisioner.Resource.Metadata - nil, // 48: provisioner.ParseComplete.WorkspaceTagsEntry - (*timestamppb.Timestamp)(nil), // 49: google.protobuf.Timestamp + (*ResourceReplacement)(nil), // 14: provisioner.ResourceReplacement + (*VariableValue)(nil), // 15: provisioner.VariableValue + (*Log)(nil), // 16: provisioner.Log + (*InstanceIdentityAuth)(nil), // 17: provisioner.InstanceIdentityAuth + (*ExternalAuthProviderResource)(nil), // 18: provisioner.ExternalAuthProviderResource + (*ExternalAuthProvider)(nil), // 19: provisioner.ExternalAuthProvider + (*Agent)(nil), // 20: provisioner.Agent + (*ResourcesMonitoring)(nil), // 21: provisioner.ResourcesMonitoring + (*MemoryResourceMonitor)(nil), // 22: provisioner.MemoryResourceMonitor + (*VolumeResourceMonitor)(nil), // 23: provisioner.VolumeResourceMonitor + (*DisplayApps)(nil), // 24: provisioner.DisplayApps + (*Env)(nil), // 25: provisioner.Env + (*Script)(nil), // 26: provisioner.Script + (*Devcontainer)(nil), // 27: provisioner.Devcontainer + (*App)(nil), // 28: provisioner.App + (*Healthcheck)(nil), // 29: provisioner.Healthcheck + (*Resource)(nil), // 30: provisioner.Resource + (*Module)(nil), // 31: provisioner.Module + (*Role)(nil), // 32: provisioner.Role + (*RunningAgentAuthToken)(nil), // 33: provisioner.RunningAgentAuthToken + (*Metadata)(nil), // 34: provisioner.Metadata + (*Config)(nil), // 35: provisioner.Config + (*ParseRequest)(nil), // 36: provisioner.ParseRequest + (*ParseComplete)(nil), // 37: provisioner.ParseComplete + (*PlanRequest)(nil), // 38: provisioner.PlanRequest + (*PlanComplete)(nil), // 39: provisioner.PlanComplete + (*ApplyRequest)(nil), // 40: provisioner.ApplyRequest + (*ApplyComplete)(nil), // 41: provisioner.ApplyComplete + (*Timing)(nil), // 42: provisioner.Timing + (*CancelRequest)(nil), // 43: provisioner.CancelRequest + (*Request)(nil), // 44: provisioner.Request + (*Response)(nil), // 45: provisioner.Response + (*Agent_Metadata)(nil), // 46: provisioner.Agent.Metadata + nil, // 47: provisioner.Agent.EnvEntry + (*Resource_Metadata)(nil), // 48: provisioner.Resource.Metadata + nil, // 49: provisioner.ParseComplete.WorkspaceTagsEntry + (*timestamppb.Timestamp)(nil), // 50: google.protobuf.Timestamp } var file_provisionersdk_proto_provisioner_proto_depIdxs = []int32{ 8, // 0: provisioner.RichParameter.options:type_name -> provisioner.RichParameterOption 13, // 1: provisioner.Preset.parameters:type_name -> provisioner.PresetParameter 11, // 2: provisioner.Preset.prebuild:type_name -> provisioner.Prebuild 0, // 3: provisioner.Log.level:type_name -> provisioner.LogLevel - 46, // 4: provisioner.Agent.env:type_name -> provisioner.Agent.EnvEntry - 27, // 5: provisioner.Agent.apps:type_name -> provisioner.App - 45, // 6: provisioner.Agent.metadata:type_name -> provisioner.Agent.Metadata - 23, // 7: provisioner.Agent.display_apps:type_name -> provisioner.DisplayApps - 25, // 8: provisioner.Agent.scripts:type_name -> provisioner.Script - 24, // 9: provisioner.Agent.extra_envs:type_name -> provisioner.Env - 20, // 10: provisioner.Agent.resources_monitoring:type_name -> provisioner.ResourcesMonitoring - 26, // 11: provisioner.Agent.devcontainers:type_name -> provisioner.Devcontainer - 21, // 12: provisioner.ResourcesMonitoring.memory:type_name -> provisioner.MemoryResourceMonitor - 22, // 13: provisioner.ResourcesMonitoring.volumes:type_name -> provisioner.VolumeResourceMonitor - 28, // 14: provisioner.App.healthcheck:type_name -> provisioner.Healthcheck + 47, // 4: provisioner.Agent.env:type_name -> provisioner.Agent.EnvEntry + 28, // 5: provisioner.Agent.apps:type_name -> provisioner.App + 46, // 6: provisioner.Agent.metadata:type_name -> provisioner.Agent.Metadata + 24, // 7: provisioner.Agent.display_apps:type_name -> provisioner.DisplayApps + 26, // 8: provisioner.Agent.scripts:type_name -> provisioner.Script + 25, // 9: provisioner.Agent.extra_envs:type_name -> provisioner.Env + 21, // 10: provisioner.Agent.resources_monitoring:type_name -> provisioner.ResourcesMonitoring + 27, // 11: provisioner.Agent.devcontainers:type_name -> provisioner.Devcontainer + 22, // 12: provisioner.ResourcesMonitoring.memory:type_name -> provisioner.MemoryResourceMonitor + 23, // 13: provisioner.ResourcesMonitoring.volumes:type_name -> provisioner.VolumeResourceMonitor + 29, // 14: provisioner.App.healthcheck:type_name -> provisioner.Healthcheck 1, // 15: provisioner.App.sharing_level:type_name -> provisioner.AppSharingLevel 2, // 16: provisioner.App.open_in:type_name -> provisioner.AppOpenIn - 19, // 17: provisioner.Resource.agents:type_name -> provisioner.Agent - 47, // 18: provisioner.Resource.metadata:type_name -> provisioner.Resource.Metadata + 20, // 17: provisioner.Resource.agents:type_name -> provisioner.Agent + 48, // 18: provisioner.Resource.metadata:type_name -> provisioner.Resource.Metadata 3, // 19: provisioner.Metadata.workspace_transition:type_name -> provisioner.WorkspaceTransition - 31, // 20: provisioner.Metadata.workspace_owner_rbac_roles:type_name -> provisioner.Role + 32, // 20: provisioner.Metadata.workspace_owner_rbac_roles:type_name -> provisioner.Role 4, // 21: provisioner.Metadata.prebuilt_workspace_build_stage:type_name -> provisioner.PrebuiltWorkspaceBuildStage - 32, // 22: provisioner.Metadata.running_agent_auth_tokens:type_name -> provisioner.RunningAgentAuthToken + 33, // 22: provisioner.Metadata.running_agent_auth_tokens:type_name -> provisioner.RunningAgentAuthToken 7, // 23: provisioner.ParseComplete.template_variables:type_name -> provisioner.TemplateVariable - 48, // 24: provisioner.ParseComplete.workspace_tags:type_name -> provisioner.ParseComplete.WorkspaceTagsEntry - 33, // 25: provisioner.PlanRequest.metadata:type_name -> provisioner.Metadata + 49, // 24: provisioner.ParseComplete.workspace_tags:type_name -> provisioner.ParseComplete.WorkspaceTagsEntry + 34, // 25: provisioner.PlanRequest.metadata:type_name -> provisioner.Metadata 10, // 26: provisioner.PlanRequest.rich_parameter_values:type_name -> provisioner.RichParameterValue - 14, // 27: provisioner.PlanRequest.variable_values:type_name -> provisioner.VariableValue - 18, // 28: provisioner.PlanRequest.external_auth_providers:type_name -> provisioner.ExternalAuthProvider + 15, // 27: provisioner.PlanRequest.variable_values:type_name -> provisioner.VariableValue + 19, // 28: provisioner.PlanRequest.external_auth_providers:type_name -> provisioner.ExternalAuthProvider 10, // 29: provisioner.PlanRequest.previous_parameter_values:type_name -> provisioner.RichParameterValue - 29, // 30: provisioner.PlanComplete.resources:type_name -> provisioner.Resource + 30, // 30: provisioner.PlanComplete.resources:type_name -> provisioner.Resource 9, // 31: provisioner.PlanComplete.parameters:type_name -> provisioner.RichParameter - 17, // 32: provisioner.PlanComplete.external_auth_providers:type_name -> provisioner.ExternalAuthProviderResource - 41, // 33: provisioner.PlanComplete.timings:type_name -> provisioner.Timing - 30, // 34: provisioner.PlanComplete.modules:type_name -> provisioner.Module + 18, // 32: provisioner.PlanComplete.external_auth_providers:type_name -> provisioner.ExternalAuthProviderResource + 42, // 33: provisioner.PlanComplete.timings:type_name -> provisioner.Timing + 31, // 34: provisioner.PlanComplete.modules:type_name -> provisioner.Module 12, // 35: provisioner.PlanComplete.presets:type_name -> provisioner.Preset - 33, // 36: provisioner.ApplyRequest.metadata:type_name -> provisioner.Metadata - 29, // 37: provisioner.ApplyComplete.resources:type_name -> provisioner.Resource - 9, // 38: provisioner.ApplyComplete.parameters:type_name -> provisioner.RichParameter - 17, // 39: provisioner.ApplyComplete.external_auth_providers:type_name -> provisioner.ExternalAuthProviderResource - 41, // 40: provisioner.ApplyComplete.timings:type_name -> provisioner.Timing - 49, // 41: provisioner.Timing.start:type_name -> google.protobuf.Timestamp - 49, // 42: provisioner.Timing.end:type_name -> google.protobuf.Timestamp - 5, // 43: provisioner.Timing.state:type_name -> provisioner.TimingState - 34, // 44: provisioner.Request.config:type_name -> provisioner.Config - 35, // 45: provisioner.Request.parse:type_name -> provisioner.ParseRequest - 37, // 46: provisioner.Request.plan:type_name -> provisioner.PlanRequest - 39, // 47: provisioner.Request.apply:type_name -> provisioner.ApplyRequest - 42, // 48: provisioner.Request.cancel:type_name -> provisioner.CancelRequest - 15, // 49: provisioner.Response.log:type_name -> provisioner.Log - 36, // 50: provisioner.Response.parse:type_name -> provisioner.ParseComplete - 38, // 51: provisioner.Response.plan:type_name -> provisioner.PlanComplete - 40, // 52: provisioner.Response.apply:type_name -> provisioner.ApplyComplete - 43, // 53: provisioner.Provisioner.Session:input_type -> provisioner.Request - 44, // 54: provisioner.Provisioner.Session:output_type -> provisioner.Response - 54, // [54:55] is the sub-list for method output_type - 53, // [53:54] is the sub-list for method input_type - 53, // [53:53] is the sub-list for extension type_name - 53, // [53:53] is the sub-list for extension extendee - 0, // [0:53] is the sub-list for field type_name + 14, // 36: provisioner.PlanComplete.resource_replacements:type_name -> provisioner.ResourceReplacement + 34, // 37: provisioner.ApplyRequest.metadata:type_name -> provisioner.Metadata + 30, // 38: provisioner.ApplyComplete.resources:type_name -> provisioner.Resource + 9, // 39: provisioner.ApplyComplete.parameters:type_name -> provisioner.RichParameter + 18, // 40: provisioner.ApplyComplete.external_auth_providers:type_name -> provisioner.ExternalAuthProviderResource + 42, // 41: provisioner.ApplyComplete.timings:type_name -> provisioner.Timing + 50, // 42: provisioner.Timing.start:type_name -> google.protobuf.Timestamp + 50, // 43: provisioner.Timing.end:type_name -> google.protobuf.Timestamp + 5, // 44: provisioner.Timing.state:type_name -> provisioner.TimingState + 35, // 45: provisioner.Request.config:type_name -> provisioner.Config + 36, // 46: provisioner.Request.parse:type_name -> provisioner.ParseRequest + 38, // 47: provisioner.Request.plan:type_name -> provisioner.PlanRequest + 40, // 48: provisioner.Request.apply:type_name -> provisioner.ApplyRequest + 43, // 49: provisioner.Request.cancel:type_name -> provisioner.CancelRequest + 16, // 50: provisioner.Response.log:type_name -> provisioner.Log + 37, // 51: provisioner.Response.parse:type_name -> provisioner.ParseComplete + 39, // 52: provisioner.Response.plan:type_name -> provisioner.PlanComplete + 41, // 53: provisioner.Response.apply:type_name -> provisioner.ApplyComplete + 44, // 54: provisioner.Provisioner.Session:input_type -> provisioner.Request + 45, // 55: provisioner.Provisioner.Session:output_type -> provisioner.Response + 55, // [55:56] is the sub-list for method output_type + 54, // [54:55] is the sub-list for method input_type + 54, // [54:54] is the sub-list for extension type_name + 54, // [54:54] is the sub-list for extension extendee + 0, // [0:54] is the sub-list for field type_name } func init() { file_provisionersdk_proto_provisioner_proto_init() } @@ -4433,7 +4508,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.(*VariableValue); i { + switch v := v.(*ResourceReplacement); i { case 0: return &v.state case 1: @@ -4445,7 +4520,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.(*Log); i { + switch v := v.(*VariableValue); i { case 0: return &v.state case 1: @@ -4457,7 +4532,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.(*InstanceIdentityAuth); i { + switch v := v.(*Log); i { case 0: return &v.state case 1: @@ -4469,7 +4544,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.(*ExternalAuthProviderResource); i { + switch v := v.(*InstanceIdentityAuth); i { case 0: return &v.state case 1: @@ -4481,7 +4556,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.(*ExternalAuthProvider); i { + switch v := v.(*ExternalAuthProviderResource); i { case 0: return &v.state case 1: @@ -4493,7 +4568,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.(*Agent); i { + switch v := v.(*ExternalAuthProvider); i { case 0: return &v.state case 1: @@ -4505,7 +4580,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.(*ResourcesMonitoring); i { + switch v := v.(*Agent); i { case 0: return &v.state case 1: @@ -4517,7 +4592,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.(*MemoryResourceMonitor); i { + switch v := v.(*ResourcesMonitoring); i { case 0: return &v.state case 1: @@ -4529,7 +4604,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.(*VolumeResourceMonitor); i { + switch v := v.(*MemoryResourceMonitor); i { case 0: return &v.state case 1: @@ -4541,7 +4616,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.(*DisplayApps); i { + switch v := v.(*VolumeResourceMonitor); i { case 0: return &v.state case 1: @@ -4553,7 +4628,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.(*Env); i { + switch v := v.(*DisplayApps); i { case 0: return &v.state case 1: @@ -4565,7 +4640,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.(*Script); i { + switch v := v.(*Env); i { case 0: return &v.state case 1: @@ -4577,7 +4652,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.(*Devcontainer); i { + switch v := v.(*Script); i { case 0: return &v.state case 1: @@ -4589,7 +4664,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.(*App); i { + switch v := v.(*Devcontainer); i { case 0: return &v.state case 1: @@ -4601,7 +4676,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.(*Healthcheck); i { + switch v := v.(*App); i { case 0: return &v.state case 1: @@ -4613,7 +4688,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.(*Resource); i { + switch v := v.(*Healthcheck); i { case 0: return &v.state case 1: @@ -4625,7 +4700,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.(*Module); i { + switch v := v.(*Resource); i { case 0: return &v.state case 1: @@ -4637,7 +4712,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.(*Role); i { + switch v := v.(*Module); i { case 0: return &v.state case 1: @@ -4649,7 +4724,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.(*RunningAgentAuthToken); i { + switch v := v.(*Role); i { case 0: return &v.state case 1: @@ -4661,7 +4736,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.(*Metadata); i { + switch v := v.(*RunningAgentAuthToken); i { case 0: return &v.state case 1: @@ -4673,7 +4748,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.(*Config); i { + switch v := v.(*Metadata); i { case 0: return &v.state case 1: @@ -4685,7 +4760,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.(*ParseRequest); i { + switch v := v.(*Config); i { case 0: return &v.state case 1: @@ -4697,7 +4772,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ParseComplete); i { + switch v := v.(*ParseRequest); i { case 0: return &v.state case 1: @@ -4709,7 +4784,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PlanRequest); i { + switch v := v.(*ParseComplete); i { case 0: return &v.state case 1: @@ -4721,7 +4796,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PlanComplete); i { + switch v := v.(*PlanRequest); i { case 0: return &v.state case 1: @@ -4733,7 +4808,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[33].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ApplyRequest); i { + switch v := v.(*PlanComplete); i { case 0: return &v.state case 1: @@ -4745,7 +4820,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[34].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ApplyComplete); i { + switch v := v.(*ApplyRequest); i { case 0: return &v.state case 1: @@ -4757,7 +4832,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[35].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Timing); i { + switch v := v.(*ApplyComplete); i { case 0: return &v.state case 1: @@ -4769,7 +4844,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[36].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CancelRequest); i { + switch v := v.(*Timing); i { case 0: return &v.state case 1: @@ -4781,7 +4856,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[37].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Request); i { + switch v := v.(*CancelRequest); i { case 0: return &v.state case 1: @@ -4793,7 +4868,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[38].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Response); i { + switch v := v.(*Request); i { case 0: return &v.state case 1: @@ -4805,6 +4880,18 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[39].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[40].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Agent_Metadata); i { case 0: return &v.state @@ -4816,7 +4903,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[41].Exporter = func(v interface{}, i int) interface{} { + file_provisionersdk_proto_provisioner_proto_msgTypes[42].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Resource_Metadata); i { case 0: return &v.state @@ -4830,18 +4917,18 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[3].OneofWrappers = []interface{}{} - file_provisionersdk_proto_provisioner_proto_msgTypes[13].OneofWrappers = []interface{}{ + file_provisionersdk_proto_provisioner_proto_msgTypes[14].OneofWrappers = []interface{}{ (*Agent_Token)(nil), (*Agent_InstanceId)(nil), } - file_provisionersdk_proto_provisioner_proto_msgTypes[37].OneofWrappers = []interface{}{ + file_provisionersdk_proto_provisioner_proto_msgTypes[38].OneofWrappers = []interface{}{ (*Request_Config)(nil), (*Request_Parse)(nil), (*Request_Plan)(nil), (*Request_Apply)(nil), (*Request_Cancel)(nil), } - file_provisionersdk_proto_provisioner_proto_msgTypes[38].OneofWrappers = []interface{}{ + file_provisionersdk_proto_provisioner_proto_msgTypes[39].OneofWrappers = []interface{}{ (*Response_Log)(nil), (*Response_Parse)(nil), (*Response_Plan)(nil), @@ -4853,7 +4940,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_provisionersdk_proto_provisioner_proto_rawDesc, NumEnums: 6, - NumMessages: 43, + NumMessages: 44, NumExtensions: 0, NumServices: 1, }, diff --git a/provisionersdk/proto/provisioner.proto b/provisionersdk/proto/provisioner.proto index 9abcd9e900435..edd8ae07e0d04 100644 --- a/provisionersdk/proto/provisioner.proto +++ b/provisionersdk/proto/provisioner.proto @@ -73,6 +73,11 @@ message PresetParameter { string value = 2; } +message ResourceReplacement { + string resource = 1; + repeated string paths = 2; +} + // VariableValue holds the key/value mapping of a Terraform variable. message VariableValue { string name = 1; @@ -349,6 +354,7 @@ message PlanComplete { repeated Preset presets = 8; bytes plan = 9; bytes module_files = 10; + repeated ResourceReplacement resource_replacements = 11; } // ApplyRequest asks the provisioner to apply the changes. Apply MUST be preceded by a successful plan request/response diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index d56dc51f66622..75d8142295ccf 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -581,6 +581,7 @@ const createTemplateVersionTar = async ( externalAuthProviders: response.apply?.externalAuthProviders ?? [], timings: response.apply?.timings ?? [], presets: [], + resourceReplacements: [], plan: emptyPlan, moduleFiles: new Uint8Array(), }, @@ -705,6 +706,7 @@ const createTemplateVersionTar = async ( timings: [], modules: [], presets: [], + resourceReplacements: [], plan: emptyPlan, moduleFiles: new Uint8Array(), ...response.plan, diff --git a/site/e2e/provisionerGenerated.ts b/site/e2e/provisionerGenerated.ts index 68cd48576349d..28c225fc1ada3 100644 --- a/site/e2e/provisionerGenerated.ts +++ b/site/e2e/provisionerGenerated.ts @@ -120,6 +120,11 @@ export interface PresetParameter { value: string; } +export interface ResourceReplacement { + resource: string; + paths: string[]; +} + /** VariableValue holds the key/value mapping of a Terraform variable. */ export interface VariableValue { name: string; @@ -374,6 +379,7 @@ export interface PlanComplete { presets: Preset[]; plan: Uint8Array; moduleFiles: Uint8Array; + resourceReplacements: ResourceReplacement[]; } /** @@ -573,6 +579,18 @@ export const PresetParameter = { }, }; +export const ResourceReplacement = { + encode(message: ResourceReplacement, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.resource !== "") { + writer.uint32(10).string(message.resource); + } + for (const v of message.paths) { + writer.uint32(18).string(v!); + } + return writer; + }, +}; + export const VariableValue = { encode(message: VariableValue, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { if (message.name !== "") { @@ -1172,6 +1190,9 @@ export const PlanComplete = { if (message.moduleFiles.length !== 0) { writer.uint32(82).bytes(message.moduleFiles); } + for (const v of message.resourceReplacements) { + ResourceReplacement.encode(v!, writer.uint32(90).fork()).ldelim(); + } return writer; }, }; From df56a13947884af89cce95f80c69405113964664 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 14 May 2025 09:54:19 -0300 Subject: [PATCH 41/88] chore: replace MUI icons with Lucide icons - 12 (#17815) AddOutlined -> PlusIcon RemoveOutlined -> TrashIcon ScheduleOutlined -> ClockIcon --- .../LicensesSettingsPageView.tsx | 5 ++--- .../OAuth2AppsSettingsPageView.tsx | 4 ++-- site/src/pages/GroupsPage/GroupsPageView.tsx | 4 ++-- .../CustomRolesPage/CustomRolesPageView.tsx | 12 +++++++----- .../StarterTemplatePage/StarterTemplatePageView.tsx | 5 ++--- site/src/pages/TemplatePage/TemplatePageHeader.tsx | 12 +++++++----- .../TemplateVersionEditor.tsx | 5 ++--- .../pages/UserSettingsPage/TokensPage/TokensPage.tsx | 8 ++++++-- .../WorkspacePage/WorkspaceScheduleControls.tsx | 10 ++++------ 9 files changed, 34 insertions(+), 31 deletions(-) diff --git a/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicensesSettingsPageView.tsx b/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicensesSettingsPageView.tsx index bedf3f6de3b4d..eb60361883b72 100644 --- a/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicensesSettingsPageView.tsx +++ b/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicensesSettingsPageView.tsx @@ -1,5 +1,4 @@ import { type Interpolation, type Theme, useTheme } from "@emotion/react"; -import AddIcon from "@mui/icons-material/AddOutlined"; import MuiButton from "@mui/material/Button"; import MuiLink from "@mui/material/Link"; import Skeleton from "@mui/material/Skeleton"; @@ -15,7 +14,7 @@ import { import { Spinner } from "components/Spinner/Spinner"; import { Stack } from "components/Stack/Stack"; import { useWindowSize } from "hooks/useWindowSize"; -import { RotateCwIcon } from "lucide-react"; +import { PlusIcon, RotateCwIcon } from "lucide-react"; import type { FC } from "react"; import Confetti from "react-confetti"; import { Link } from "react-router-dom"; @@ -76,7 +75,7 @@ const LicensesSettingsPageView: FC = ({ } + startIcon={} > Add a license diff --git a/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/OAuth2AppsSettingsPageView.tsx b/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/OAuth2AppsSettingsPageView.tsx index b3ee4e0f5d0fa..8c443775b0848 100644 --- a/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/OAuth2AppsSettingsPageView.tsx +++ b/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/OAuth2AppsSettingsPageView.tsx @@ -1,5 +1,4 @@ import { useTheme } from "@emotion/react"; -import AddIcon from "@mui/icons-material/AddOutlined"; import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight"; import Button from "@mui/material/Button"; import Table from "@mui/material/Table"; @@ -19,6 +18,7 @@ import { import { Stack } from "components/Stack/Stack"; import { TableLoader } from "components/TableLoader/TableLoader"; import { useClickableTableRow } from "hooks/useClickableTableRow"; +import { PlusIcon } from "lucide-react"; import type { FC } from "react"; import { Link, useNavigate } from "react-router-dom"; @@ -52,7 +52,7 @@ const OAuth2AppsSettingsPageView: FC = ({ diff --git a/site/src/pages/GroupsPage/GroupsPageView.tsx b/site/src/pages/GroupsPage/GroupsPageView.tsx index f0e3647ebc664..c1cc60ec83aa6 100644 --- a/site/src/pages/GroupsPage/GroupsPageView.tsx +++ b/site/src/pages/GroupsPage/GroupsPageView.tsx @@ -1,5 +1,4 @@ import type { Interpolation, Theme } from "@emotion/react"; -import AddOutlined from "@mui/icons-material/AddOutlined"; import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight"; import Skeleton from "@mui/material/Skeleton"; import type { Group } from "api/typesGenerated"; @@ -24,6 +23,7 @@ import { TableRowSkeleton, } from "components/TableLoader/TableLoader"; import { useClickableTableRow } from "hooks"; +import { PlusIcon } from "lucide-react"; import type { FC } from "react"; import { Link as RouterLink, useNavigate } from "react-router-dom"; import { docs } from "utils/docs"; @@ -81,7 +81,7 @@ export const GroupsPageView: FC = ({ canCreateGroup && ( diff --git a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.tsx index 2c360a8dd4e45..91ca7b5fa2732 100644 --- a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.tsx +++ b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.tsx @@ -1,6 +1,4 @@ import type { Interpolation, Theme } from "@emotion/react"; -import AddIcon from "@mui/icons-material/AddOutlined"; -import AddOutlined from "@mui/icons-material/AddOutlined"; import Button from "@mui/material/Button"; import Skeleton from "@mui/material/Skeleton"; import type { AssignableRoles, Role } from "api/typesGenerated"; @@ -27,7 +25,7 @@ import { TableLoaderSkeleton, TableRowSkeleton, } from "components/TableLoader/TableLoader"; -import { EllipsisVertical } from "lucide-react"; +import { EllipsisVertical, PlusIcon } from "lucide-react"; import type { FC } from "react"; import { Link as RouterLink, useNavigate } from "react-router-dom"; import { docs } from "utils/docs"; @@ -74,7 +72,11 @@ export const CustomRolesPageView: FC = ({ {canCreateOrgRole && isCustomRolesEnabled && ( - )} @@ -158,7 +160,7 @@ const RoleTable: FC = ({ diff --git a/site/src/pages/TemplatePage/TemplatePageHeader.tsx b/site/src/pages/TemplatePage/TemplatePageHeader.tsx index 834c83905cbf5..7379ac0da0a96 100644 --- a/site/src/pages/TemplatePage/TemplatePageHeader.tsx +++ b/site/src/pages/TemplatePage/TemplatePageHeader.tsx @@ -1,4 +1,3 @@ -import AddIcon from "@mui/icons-material/AddOutlined"; import EditIcon from "@mui/icons-material/EditOutlined"; import CopyIcon from "@mui/icons-material/FileCopyOutlined"; import Button from "@mui/material/Button"; @@ -28,9 +27,12 @@ import { } from "components/PageHeader/PageHeader"; import { Pill } from "components/Pill/Pill"; import { Stack } from "components/Stack/Stack"; -import { SettingsIcon } from "lucide-react"; -import { TrashIcon } from "lucide-react"; -import { EllipsisVertical } from "lucide-react"; +import { + EllipsisVertical, + PlusIcon, + SettingsIcon, + TrashIcon, +} from "lucide-react"; import { linkToTemplate, useLinks } from "modules/navigation"; import type { WorkspacePermissions } from "modules/permissions/workspaces"; import type { FC } from "react"; @@ -190,7 +192,7 @@ export const TemplatePageHeader: FC = ({ workspacePermissions.createWorkspaceForUserID && (
diff --git a/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx b/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx index 92d0a4d977fd7..9668b0fa7bb96 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx +++ b/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx @@ -1,8 +1,8 @@ import { type Interpolation, type Theme, css } from "@emotion/react"; -import AddIcon from "@mui/icons-material/AddOutlined"; import Button from "@mui/material/Button"; import type { APIKeyWithOwner } from "api/typesGenerated"; import { Stack } from "components/Stack/Stack"; +import { PlusIcon } from "lucide-react"; import { type FC, useState } from "react"; import { Link as RouterLink } from "react-router-dom"; import { Section } from "../Section"; @@ -65,7 +65,11 @@ const TokensPage: FC = () => { const TokenActions: FC = () => ( - diff --git a/site/src/pages/WorkspacePage/WorkspaceScheduleControls.tsx b/site/src/pages/WorkspacePage/WorkspaceScheduleControls.tsx index dc5e14572e14e..5bced6f668d0f 100644 --- a/site/src/pages/WorkspacePage/WorkspaceScheduleControls.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceScheduleControls.tsx @@ -1,7 +1,4 @@ import type { Interpolation, Theme } from "@emotion/react"; -import AddIcon from "@mui/icons-material/AddOutlined"; -import RemoveIcon from "@mui/icons-material/RemoveOutlined"; -import ScheduleOutlined from "@mui/icons-material/ScheduleOutlined"; import IconButton from "@mui/material/IconButton"; import Link, { type LinkProps } from "@mui/material/Link"; import Tooltip from "@mui/material/Tooltip"; @@ -16,6 +13,7 @@ import { TopbarData, TopbarIcon } from "components/FullPageLayout/Topbar"; import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"; import dayjs, { type Dayjs } from "dayjs"; import { useTime } from "hooks/useTime"; +import { ClockIcon, MinusIcon, PlusIcon } from "lucide-react"; import { getWorkspaceActivityStatus } from "modules/workspaces/activity"; import { type FC, type ReactNode, forwardRef, useRef, useState } from "react"; import { useMutation, useQueryClient } from "react-query"; @@ -41,7 +39,7 @@ const WorkspaceScheduleContainer: FC = ({ }) => { const icon = ( - + ); @@ -211,7 +209,7 @@ const AutostopDisplay: FC = ({ handleDeadlineChange(deadline.subtract(1, "h")); }} > - + Subtract 1 hour @@ -224,7 +222,7 @@ const AutostopDisplay: FC = ({ handleDeadlineChange(deadline.add(1, "h")); }} > - + Add 1 hour From 74934e174eab448eacef776573d836cf67568212 Mon Sep 17 00:00:00 2001 From: Edward Angert Date: Wed, 14 May 2025 10:05:33 -0400 Subject: [PATCH 42/88] docs: add file sync to coder desktop docs (#17463) closes #16869 section could use more about: - [x] sync direction options? - [x] how to resolve conflicts - [x] EA --> Beta [preview](https://coder.com/docs/@16869-desktop-file-sync/user-guides/desktop) --------- Co-authored-by: EdwardAngert <17991901+EdwardAngert@users.noreply.github.com> --- .../desktop/coder-desktop-file-sync-add.png | Bin 0 -> 60360 bytes ...-desktop-file-sync-conflicts-mouseover.png | Bin 0 -> 128644 bytes .../coder-desktop-file-sync-staging.png | Bin 0 -> 28469 bytes .../coder-desktop-file-sync-watching.png | Bin 0 -> 27668 bytes .../desktop/coder-desktop-file-sync.png | Bin 0 -> 16365 bytes .../desktop/coder-desktop-workspaces.png | Bin 100150 -> 63939 bytes docs/manifest.json | 2 +- docs/user-guides/desktop/index.md | 84 ++++++++++++++---- 8 files changed, 69 insertions(+), 17 deletions(-) create mode 100644 docs/images/user-guides/desktop/coder-desktop-file-sync-add.png create mode 100644 docs/images/user-guides/desktop/coder-desktop-file-sync-conflicts-mouseover.png create mode 100644 docs/images/user-guides/desktop/coder-desktop-file-sync-staging.png create mode 100644 docs/images/user-guides/desktop/coder-desktop-file-sync-watching.png create mode 100644 docs/images/user-guides/desktop/coder-desktop-file-sync.png diff --git a/docs/images/user-guides/desktop/coder-desktop-file-sync-add.png b/docs/images/user-guides/desktop/coder-desktop-file-sync-add.png new file mode 100644 index 0000000000000000000000000000000000000000..35e59d76866f2209035e2317d93b1338039c95a8 GIT binary patch literal 60360 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

5Jti7=xHuKTBc& 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 G-Twj#aZ1Ag literal 0 HcmV?d00001 diff --git a/docs/images/user-guides/desktop/coder-desktop-file-sync-conflicts-mouseover.png b/docs/images/user-guides/desktop/coder-desktop-file-sync-conflicts-mouseover.png new file mode 100644 index 0000000000000000000000000000000000000000..80a5185585c1a90ceb831dd281495f51d5c984c5 GIT binary patch literal 128644 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%0AZ0j%-gpD6N?n!M%2xPJ1%NtR8K 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 M07*qoM6N<$f|IxxuK)l5 literal 0 HcmV?d00001 diff --git a/docs/images/user-guides/desktop/coder-desktop-file-sync-staging.png b/docs/images/user-guides/desktop/coder-desktop-file-sync-staging.png new file mode 100644 index 0000000000000000000000000000000000000000..6b846f3ef244fec029cc947df1e7580dc8e378e7 GIT binary patch literal 28469 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 literal 0 HcmV?d00001 diff --git a/docs/images/user-guides/desktop/coder-desktop-file-sync-watching.png b/docs/images/user-guides/desktop/coder-desktop-file-sync-watching.png new file mode 100644 index 0000000000000000000000000000000000000000..7875980186e335709f52621a5b545d21ac540754 GIT binary patch literal 27668 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� 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℞O4URH(J5lud*B~m4u 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 ~nWo<6j8 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 diff --git a/docs/manifest.json b/docs/manifest.json index c7d558b87bfd7..adbac7d4250dc 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -193,7 +193,7 @@ "description": "Use Coder Desktop to access your workspace like it's a local machine", "path": "./user-guides/desktop/index.md", "icon_path": "./images/icons/computer-code.svg", - "state": ["early access"] + "state": ["beta"] }, { "title": "Workspace Management", diff --git a/docs/user-guides/desktop/index.md b/docs/user-guides/desktop/index.md index 72d627c7a3e71..69a32837a8b87 100644 --- a/docs/user-guides/desktop/index.md +++ b/docs/user-guides/desktop/index.md @@ -1,4 +1,4 @@ -# Coder Desktop (Early Access) +# Coder Desktop (Beta) Use Coder Desktop to work on your workspaces as though they're on your LAN, no port-forwarding required. @@ -22,7 +22,7 @@ You can install Coder Desktop on macOS or Windows. Alternatively, you can manually install Coder Desktop from the [releases page](https://github.com/coder/coder-desktop-macos/releases). -1. Open **Coder Desktop** from the Applications directory. When macOS asks if you want to open it, select **Open**. +1. Open **Coder Desktop** from the Applications directory. 1. The application is treated as a system VPN. macOS will prompt you to confirm with: @@ -79,11 +79,11 @@ Before you can use Coder Desktop, you will need to sign in. ## macOS - Coder Desktop menu before the user signs in + ![Coder Desktop menu before the user signs in](../../images/user-guides/desktop/coder-desktop-mac-pre-sign-in.png) ## Windows - Coder Desktop menu before the user signs in + ![Coder Desktop menu before the user signs in](../../images/user-guides/desktop/coder-desktop-win-pre-sign-in.png) @@ -97,19 +97,19 @@ Before you can use Coder Desktop, you will need to sign in. 1. In your web browser, you may be prompted to sign in to Coder with your credentials: - Sign in to your Coder deployment + ![Sign in to your Coder deployment](../../images/templates/coder-login-web.png) 1. Copy the session token to the clipboard: - Copy session token + ![Copy session token](../../images/templates/coder-session-token.png) 1. Paste the token in the **Session Token** field of the **Sign In** screen, then select **Sign In**: ![Paste the session token in to sign in](../../images/user-guides/desktop/coder-desktop-session-token.png) -1. macOS: Allow the VPN configuration for Coder Desktop if you are prompted. +1. macOS: Allow the VPN configuration for Coder Desktop if you are prompted: - Copy session token + ![Copy session token](../../images/user-guides/desktop/mac-allow-vpn.png) 1. Select the Coder icon in the menu bar (macOS) or system tray (Windows), and click the **Coder Connect** toggle to enable the connection. @@ -129,28 +129,80 @@ While active, Coder Connect will list the workspaces you own and will configure To copy the `.coder` hostname of a workspace agent, you can click the copy icon beside it. -On macOS you can use `ping6` in your terminal to verify the connection to your workspace: +You can also connect to the SSH server in your workspace using any SSH client, such as OpenSSH or PuTTY: ```shell - ping6 -c 5 your-workspace.coder + ssh your-workspace.coder ``` -On Windows, you can use `ping` in a Command Prompt or PowerShell terminal to verify the connection to your workspace: +Any services listening on ports in your workspace will be available on the same hostname. For example, you can access a web server on port `8080` by visiting `http://your-workspace.coder:8080` in your browser. + +> [!NOTE] +> Currently, the Coder IDE extensions for VSCode and JetBrains create their own tunnel and do not utilize the Coder Connect tunnel to connect to workspaces. + +### Ping your workspace + +

+ +## Sync a local directory with your workspace + +Coder Desktop file sync provides bidirectional synchronization between a local directory and your workspace. +You can work offline, add screenshots to documentation, or use local development tools while keeping your files in sync with your workspace. + +1. Create a new local directory. + + If you select an existing clone of your repository, Desktop will recognize it as conflicting files. + +1. In the Coder Desktop app, select **File sync**. + + ![Coder Desktop File Sync screen](../../images/user-guides/desktop/coder-desktop-file-sync.png) + +1. Select the **+** in the corner to select the local path, workspace, and remote path, then select **Add**: + + ![Coder Desktop File Sync add paths](../../images/user-guides/desktop/coder-desktop-file-sync-add.png) + +1. File sync clones your workspace directory to your local directory, then watches for changes: + + ![Coder Desktop File Sync watching](../../images/user-guides/desktop/coder-desktop-file-sync-watching.png) + + For more information about the current status, hover your mouse over the status. + +File sync excludes version control system directories like `.git/` from synchronization, so keep your Git-cloned repository wherever you run Git commands. +This means that if you use an IDE with a built-in terminal to edit files on your remote workspace, that should be the Git clone and your local directory should be for file syncs. + > [!NOTE] -> Currently, the Coder IDE extensions for VSCode and JetBrains create their own tunnel and do not utilize the Coder Connect tunnel to connect to workspaces. +> Coder Desktop uses `alpha` and `beta` to distinguish between the: +> +> - Local directory: `alpha` +> - Remote directory: `beta` + +### File sync conflicts + +File sync shows a `Conflicts` status when it detects conflicting files. + +You can hover your mouse over the status for the list of conflicts: + +![Desktop file sync conflicts mouseover](../../images/user-guides/desktop/coder-desktop-file-sync-conflicts-mouseover.png) + +If you encounter a synchronization conflict, delete the conflicting file that contains changes you don't want to keep. ## Accessing web apps in a secure browser context From 4d00b76ef4bf0d643799ac6b2df0685dfbe80380 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 15:08:52 +0000 Subject: [PATCH 43/88] chore: bump github.com/justinas/nosurf from 1.1.1 to 1.2.0 (#17829) Bumps [github.com/justinas/nosurf](https://github.com/justinas/nosurf) from 1.1.1 to 1.2.0.
Release notes

Sourced from github.com/justinas/nosurf's releases.

v1.2.0

This is a security release for nosurf. It mainly addresses CVE-2025-46721.

This release technically includes breaking changes, as nosurf starts applying same-origin checks that were not previously enforced. In most cases, users will not need to make any changes to their code. However, it is recommended to read the documentation on nosurf's trusted origin checks before upgrading.

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/justinas/nosurf&package-manager=go_modules&previous-version=1.1.1&new-version=1.2.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/coder/coder/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 8fe4da248d522..9dfa9a5ab7adf 100644 --- a/go.mod +++ b/go.mod @@ -146,7 +146,7 @@ require ( github.com/imulab/go-scim/pkg/v2 v2.2.0 github.com/jedib0t/go-pretty/v6 v6.6.7 github.com/jmoiron/sqlx v1.4.0 - github.com/justinas/nosurf v1.1.1 + github.com/justinas/nosurf v1.2.0 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f github.com/klauspost/compress v1.18.0 diff --git a/go.sum b/go.sum index b03afb434ce38..3a6d9d92cfb93 100644 --- a/go.sum +++ b/go.sum @@ -1442,8 +1442,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= -github.com/justinas/nosurf v1.1.1 h1:92Aw44hjSK4MxJeMSyDa7jwuI9GR2J/JCQiaKvXXSlk= -github.com/justinas/nosurf v1.1.1/go.mod h1:ALpWdSbuNGy2lZWtyXdjkYv4edL23oSEgfBT1gPJ5BQ= +github.com/justinas/nosurf v1.2.0 h1:yMs1bSRrNiwXk4AS6n8vL2Ssgpb9CB25T/4xrixaK0s= +github.com/justinas/nosurf v1.2.0/go.mod h1:ALpWdSbuNGy2lZWtyXdjkYv4edL23oSEgfBT1gPJ5BQ= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= From f3bcac2e9043a363ee88cf1ae90c2d29e0482e50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B1=E3=82=A4=E3=83=A9?= Date: Wed, 14 May 2025 10:26:47 -0600 Subject: [PATCH 44/88] refactor: improve overlayFS errors (#17808) --- coderd/files/overlay.go | 69 +++++++++--------------------------- coderd/files/overlay_test.go | 3 +- coderd/parameters.go | 9 +---- 3 files changed, 19 insertions(+), 62 deletions(-) diff --git a/coderd/files/overlay.go b/coderd/files/overlay.go index d7e2adf8db4e8..fa0e590d1e6c2 100644 --- a/coderd/files/overlay.go +++ b/coderd/files/overlay.go @@ -4,15 +4,12 @@ import ( "io/fs" "path" "strings" - - "golang.org/x/xerrors" ) -// overlayFS allows you to "join" together the template files tar file fs.FS -// with the Terraform modules tar file fs.FS. We could potentially turn this -// into something more parameterized/configurable, but the requirements here are -// a _bit_ odd, because every file in the modulesFS includes the -// .terraform/modules/ folder at the beginning of it's path. +// overlayFS allows you to "join" together multiple fs.FS. Files in any specific +// overlay will only be accessible if their path starts with the base path +// provided for the overlay. eg. An overlay at the path .terraform/modules +// should contain files with paths inside the .terraform/modules folder. type overlayFS struct { baseFS fs.FS overlays []Overlay @@ -23,64 +20,32 @@ type Overlay struct { fs.FS } -func NewOverlayFS(baseFS fs.FS, overlays []Overlay) (fs.FS, error) { - if err := valid(baseFS); err != nil { - return nil, xerrors.Errorf("baseFS: %w", err) - } - - for _, overlay := range overlays { - if err := valid(overlay.FS); err != nil { - return nil, xerrors.Errorf("overlayFS: %w", err) - } - } - +func NewOverlayFS(baseFS fs.FS, overlays []Overlay) fs.FS { return overlayFS{ baseFS: baseFS, overlays: overlays, - }, nil + } } -func (f overlayFS) Open(p string) (fs.File, error) { +func (f overlayFS) target(p string) fs.FS { + target := f.baseFS for _, overlay := range f.overlays { if strings.HasPrefix(path.Clean(p), overlay.Path) { - return overlay.FS.Open(p) + target = overlay.FS + break } } - return f.baseFS.Open(p) + return target } -func (f overlayFS) ReadDir(p string) ([]fs.DirEntry, error) { - for _, overlay := range f.overlays { - if strings.HasPrefix(path.Clean(p), overlay.Path) { - //nolint:forcetypeassert - return overlay.FS.(fs.ReadDirFS).ReadDir(p) - } - } - //nolint:forcetypeassert - return f.baseFS.(fs.ReadDirFS).ReadDir(p) +func (f overlayFS) Open(p string) (fs.File, error) { + return f.target(p).Open(p) } -func (f overlayFS) ReadFile(p string) ([]byte, error) { - for _, overlay := range f.overlays { - if strings.HasPrefix(path.Clean(p), overlay.Path) { - //nolint:forcetypeassert - return overlay.FS.(fs.ReadFileFS).ReadFile(p) - } - } - //nolint:forcetypeassert - return f.baseFS.(fs.ReadFileFS).ReadFile(p) +func (f overlayFS) ReadDir(p string) ([]fs.DirEntry, error) { + return fs.ReadDir(f.target(p), p) } -// valid checks that the fs.FS implements the required interfaces. -// The fs.FS interface is not sufficient. -func valid(fsys fs.FS) error { - _, ok := fsys.(fs.ReadDirFS) - if !ok { - return xerrors.New("overlayFS does not implement ReadDirFS") - } - _, ok = fsys.(fs.ReadFileFS) - if !ok { - return xerrors.New("overlayFS does not implement ReadFileFS") - } - return nil +func (f overlayFS) ReadFile(p string) ([]byte, error) { + return fs.ReadFile(f.target(p), p) } diff --git a/coderd/files/overlay_test.go b/coderd/files/overlay_test.go index 8d30f6e0a5a1f..29209a478d552 100644 --- a/coderd/files/overlay_test.go +++ b/coderd/files/overlay_test.go @@ -21,11 +21,10 @@ func TestOverlayFS(t *testing.T) { afero.WriteFile(b, ".terraform/modules/modules.json", []byte("{}"), 0o644) afero.WriteFile(b, ".terraform/modules/example_module/main.tf", []byte("terraform {}"), 0o644) - it, err := files.NewOverlayFS(afero.NewIOFS(a), []files.Overlay{{ + it := files.NewOverlayFS(afero.NewIOFS(a), []files.Overlay{{ Path: ".terraform/modules", FS: afero.NewIOFS(b), }}) - require.NoError(t, err) content, err := fs.ReadFile(it, "main.tf") require.NoError(t, err) diff --git a/coderd/parameters.go b/coderd/parameters.go index 6b6f4db531533..24a91d3a7514b 100644 --- a/coderd/parameters.go +++ b/coderd/parameters.go @@ -97,14 +97,7 @@ func (api *API) templateVersionDynamicParameters(rw http.ResponseWriter, r *http return } defer api.FileCache.Release(tf.CachedModuleFiles.UUID) - templateFS, err = files.NewOverlayFS(templateFS, []files.Overlay{{Path: ".terraform/modules", FS: moduleFilesFS}}) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error creating overlay filesystem.", - Detail: err.Error(), - }) - return - } + templateFS = files.NewOverlayFS(templateFS, []files.Overlay{{Path: ".terraform/modules", FS: moduleFilesFS}}) } } else if !xerrors.Is(err, sql.ErrNoRows) { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ From 789c4beba7417108df33bedd6c9cf5514e9eb99c Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 14 May 2025 12:21:36 -0500 Subject: [PATCH 45/88] chore: add dynamic parameter error if missing metadata from provisioner (#17809) --- coderd/coderd.go | 1 + coderd/database/dbgen/dbgen.go | 10 ++- coderd/database/dbmem/dbmem.go | 9 ++- coderd/database/dump.sql | 5 +- ...00325_dynamic_parameters_metadata.down.sql | 1 + .../000325_dynamic_parameters_metadata.up.sql | 4 + coderd/database/models.go | 2 + coderd/database/queries.sql.go | 19 +++-- .../templateversionterraformvalues.sql | 6 +- coderd/parameters.go | 37 ++++++++- coderd/parameters_internal_test.go | 77 +++++++++++++++++++ .../provisionerdserver/provisionerdserver.go | 15 ++-- .../provisionerdserver_test.go | 1 + enterprise/coderd/provisionerdaemons.go | 1 + 14 files changed, 163 insertions(+), 25 deletions(-) create mode 100644 coderd/database/migrations/000325_dynamic_parameters_metadata.down.sql create mode 100644 coderd/database/migrations/000325_dynamic_parameters_metadata.up.sql create mode 100644 coderd/parameters_internal_test.go diff --git a/coderd/coderd.go b/coderd/coderd.go index 98ae7a8ede413..a12a5624b931c 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1774,6 +1774,7 @@ func (api *API) CreateInMemoryTaggedProvisionerDaemon(dialCtx context.Context, n logger := api.Logger.Named(fmt.Sprintf("inmem-provisionerd-%s", name)) srv, err := provisionerdserver.NewServer( api.ctx, // use the same ctx as the API + daemon.APIVersion, api.AccessURL, daemon.ID, defaultOrg.ID, diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index fa1f297b908ba..8a345fa0fd6e7 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -29,6 +29,7 @@ import ( "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/cryptorand" + "github.com/coder/coder/v2/provisionerd/proto" "github.com/coder/coder/v2/testutil" ) @@ -1000,10 +1001,11 @@ func TemplateVersionTerraformValues(t testing.TB, db database.Store, orig databa t.Helper() params := database.InsertTemplateVersionTerraformValuesByJobIDParams{ - JobID: takeFirst(orig.JobID, uuid.New()), - CachedPlan: takeFirstSlice(orig.CachedPlan, []byte("{}")), - CachedModuleFiles: orig.CachedModuleFiles, - UpdatedAt: takeFirst(orig.UpdatedAt, dbtime.Now()), + JobID: takeFirst(orig.JobID, uuid.New()), + CachedPlan: takeFirstSlice(orig.CachedPlan, []byte("{}")), + CachedModuleFiles: orig.CachedModuleFiles, + UpdatedAt: takeFirst(orig.UpdatedAt, dbtime.Now()), + ProvisionerdVersion: takeFirst(orig.ProvisionerdVersion, proto.CurrentVersion.String()), } err := db.InsertTemplateVersionTerraformValuesByJobID(genCtx, params) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index dfa28097ab60c..63693604ae262 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -9343,10 +9343,11 @@ func (q *FakeQuerier) InsertTemplateVersionTerraformValuesByJobID(_ context.Cont // Insert the new row row := database.TemplateVersionTerraformValue{ - TemplateVersionID: templateVersion.ID, - CachedPlan: arg.CachedPlan, - CachedModuleFiles: arg.CachedModuleFiles, - UpdatedAt: arg.UpdatedAt, + TemplateVersionID: templateVersion.ID, + UpdatedAt: arg.UpdatedAt, + CachedPlan: arg.CachedPlan, + CachedModuleFiles: arg.CachedModuleFiles, + ProvisionerdVersion: arg.ProvisionerdVersion, } q.templateVersionTerraformValues = append(q.templateVersionTerraformValues, row) return nil diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index a03ea910f937c..f56b417dbe4d4 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -1441,9 +1441,12 @@ CREATE TABLE template_version_terraform_values ( template_version_id uuid NOT NULL, updated_at timestamp with time zone DEFAULT now() NOT NULL, cached_plan jsonb NOT NULL, - cached_module_files uuid + cached_module_files uuid, + provisionerd_version text DEFAULT ''::text NOT NULL ); +COMMENT ON COLUMN template_version_terraform_values.provisionerd_version IS 'What version of the provisioning engine was used to generate the cached plan and module files.'; + CREATE TABLE template_version_variables ( template_version_id uuid NOT NULL, name text NOT NULL, diff --git a/coderd/database/migrations/000325_dynamic_parameters_metadata.down.sql b/coderd/database/migrations/000325_dynamic_parameters_metadata.down.sql new file mode 100644 index 0000000000000..991871b5700ab --- /dev/null +++ b/coderd/database/migrations/000325_dynamic_parameters_metadata.down.sql @@ -0,0 +1 @@ +ALTER TABLE template_version_terraform_values DROP COLUMN provisionerd_version; diff --git a/coderd/database/migrations/000325_dynamic_parameters_metadata.up.sql b/coderd/database/migrations/000325_dynamic_parameters_metadata.up.sql new file mode 100644 index 0000000000000..211693b7f3e79 --- /dev/null +++ b/coderd/database/migrations/000325_dynamic_parameters_metadata.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE template_version_terraform_values ADD COLUMN IF NOT EXISTS provisionerd_version TEXT NOT NULL DEFAULT ''; + +COMMENT ON COLUMN template_version_terraform_values.provisionerd_version IS + 'What version of the provisioning engine was used to generate the cached plan and module files.'; diff --git a/coderd/database/models.go b/coderd/database/models.go index 3944d56268eaf..1b6ea7591d652 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -3225,6 +3225,8 @@ type TemplateVersionTerraformValue struct { UpdatedAt time.Time `db:"updated_at" json:"updated_at"` CachedPlan json.RawMessage `db:"cached_plan" json:"cached_plan"` CachedModuleFiles uuid.NullUUID `db:"cached_module_files" json:"cached_module_files"` + // What version of the provisioning engine was used to generate the cached plan and module files. + ProvisionerdVersion string `db:"provisionerd_version" json:"provisionerd_version"` } type TemplateVersionVariable struct { diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index bd1d5cddd43ed..0fd886cf39f2b 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -11702,7 +11702,7 @@ func (q *sqlQuerier) UpdateTemplateVersionExternalAuthProvidersByJobID(ctx conte const getTemplateVersionTerraformValues = `-- name: GetTemplateVersionTerraformValues :one SELECT - template_version_terraform_values.template_version_id, template_version_terraform_values.updated_at, template_version_terraform_values.cached_plan, template_version_terraform_values.cached_module_files + template_version_terraform_values.template_version_id, template_version_terraform_values.updated_at, template_version_terraform_values.cached_plan, template_version_terraform_values.cached_module_files, template_version_terraform_values.provisionerd_version FROM template_version_terraform_values WHERE @@ -11717,6 +11717,7 @@ func (q *sqlQuerier) GetTemplateVersionTerraformValues(ctx context.Context, temp &i.UpdatedAt, &i.CachedPlan, &i.CachedModuleFiles, + &i.ProvisionerdVersion, ) return i, err } @@ -11727,22 +11728,25 @@ INSERT INTO template_version_id, cached_plan, cached_module_files, - updated_at + updated_at, + provisionerd_version ) VALUES ( (select id from template_versions where job_id = $1), $2, $3, - $4 + $4, + $5 ) ` type InsertTemplateVersionTerraformValuesByJobIDParams struct { - JobID uuid.UUID `db:"job_id" json:"job_id"` - CachedPlan json.RawMessage `db:"cached_plan" json:"cached_plan"` - CachedModuleFiles uuid.NullUUID `db:"cached_module_files" json:"cached_module_files"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + JobID uuid.UUID `db:"job_id" json:"job_id"` + CachedPlan json.RawMessage `db:"cached_plan" json:"cached_plan"` + CachedModuleFiles uuid.NullUUID `db:"cached_module_files" json:"cached_module_files"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + ProvisionerdVersion string `db:"provisionerd_version" json:"provisionerd_version"` } func (q *sqlQuerier) InsertTemplateVersionTerraformValuesByJobID(ctx context.Context, arg InsertTemplateVersionTerraformValuesByJobIDParams) error { @@ -11751,6 +11755,7 @@ func (q *sqlQuerier) InsertTemplateVersionTerraformValuesByJobID(ctx context.Con arg.CachedPlan, arg.CachedModuleFiles, arg.UpdatedAt, + arg.ProvisionerdVersion, ) return err } diff --git a/coderd/database/queries/templateversionterraformvalues.sql b/coderd/database/queries/templateversionterraformvalues.sql index b4c93081177f1..2ded4a2675375 100644 --- a/coderd/database/queries/templateversionterraformvalues.sql +++ b/coderd/database/queries/templateversionterraformvalues.sql @@ -12,12 +12,14 @@ INSERT INTO template_version_id, cached_plan, cached_module_files, - updated_at + updated_at, + provisionerd_version ) VALUES ( (select id from template_versions where job_id = @job_id), @cached_plan, @cached_module_files, - @updated_at + @updated_at, + @provisionerd_version ); diff --git a/coderd/parameters.go b/coderd/parameters.go index 24a91d3a7514b..8d32096f29522 100644 --- a/coderd/parameters.go +++ b/coderd/parameters.go @@ -8,9 +8,11 @@ import ( "time" "github.com/google/uuid" + "github.com/hashicorp/hcl/v2" "golang.org/x/sync/errgroup" "golang.org/x/xerrors" + "github.com/coder/coder/v2/apiversion" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/files" @@ -107,6 +109,9 @@ func (api *API) templateVersionDynamicParameters(rw http.ResponseWriter, r *http return } + // If the err is sql.ErrNoRows, an empty terraform values struct is correct. + staticDiagnostics := parameterProvisionerVersionDiagnostic(tf) + owner, err := api.getWorkspaceOwnerData(ctx, user, templateVersion.OrganizationID) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ @@ -141,7 +146,7 @@ func (api *API) templateVersionDynamicParameters(rw http.ResponseWriter, r *http result, diagnostics := preview.Preview(ctx, input, templateFS) response := codersdk.DynamicParametersResponse{ ID: -1, - Diagnostics: previewtypes.Diagnostics(diagnostics), + Diagnostics: previewtypes.Diagnostics(diagnostics.Extend(staticDiagnostics)), } if result != nil { response.Parameters = result.Parameters @@ -169,7 +174,7 @@ func (api *API) templateVersionDynamicParameters(rw http.ResponseWriter, r *http result, diagnostics := preview.Preview(ctx, input, templateFS) response := codersdk.DynamicParametersResponse{ ID: update.ID, - Diagnostics: previewtypes.Diagnostics(diagnostics), + Diagnostics: previewtypes.Diagnostics(diagnostics.Extend(staticDiagnostics)), } if result != nil { response.Parameters = result.Parameters @@ -262,3 +267,31 @@ func (api *API) getWorkspaceOwnerData( Groups: groupNames, }, nil } + +// parameterProvisionerVersionDiagnostic checks the version of the provisioner +// used to create the template version. If the version is less than 1.5, it +// returns a warning diagnostic. Only versions 1.5+ return the module & plan data +// required. +func parameterProvisionerVersionDiagnostic(tf database.TemplateVersionTerraformValue) hcl.Diagnostics { + missingMetadata := hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "This template version is missing required metadata to support dynamic parameters. Go back to the classic creation flow.", + Detail: "To restore full functionality, please re-import the terraform as a new template version.", + } + + if tf.ProvisionerdVersion == "" { + return hcl.Diagnostics{&missingMetadata} + } + + major, minor, err := apiversion.Parse(tf.ProvisionerdVersion) + if err != nil || tf.ProvisionerdVersion == "" { + return hcl.Diagnostics{&missingMetadata} + } else if major < 1 || (major == 1 && minor < 5) { + missingMetadata.Detail = "This template version does not support dynamic parameters. " + + "Some options may be missing or incorrect. " + + "Please contact an administrator to update the provisioner and re-import the template version." + return hcl.Diagnostics{&missingMetadata} + } + + return nil +} diff --git a/coderd/parameters_internal_test.go b/coderd/parameters_internal_test.go new file mode 100644 index 0000000000000..a02baeae380b6 --- /dev/null +++ b/coderd/parameters_internal_test.go @@ -0,0 +1,77 @@ +package coderd + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/coderd/database" +) + +func Test_parameterProvisionerVersionDiagnostic(t *testing.T) { + t.Parallel() + + testCases := []struct { + version string + warning bool + }{ + { + version: "", + warning: true, + }, + { + version: "invalid", + warning: true, + }, + { + version: "0.4", + warning: true, + }, + { + version: "0.5", + warning: true, + }, + { + version: "0.6", + warning: true, + }, + { + version: "1.4", + warning: true, + }, + { + version: "1.5", + warning: false, + }, + { + version: "1.6", + warning: false, + }, + { + version: "2.0", + warning: false, + }, + { + version: "2.5", + warning: false, + }, + { + version: "2.6", + warning: false, + }, + } + + for _, tc := range testCases { + t.Run("Version_"+tc.version, func(t *testing.T) { + t.Parallel() + diags := parameterProvisionerVersionDiagnostic(database.TemplateVersionTerraformValue{ + ProvisionerdVersion: tc.version, + }) + if tc.warning { + require.Len(t, diags, 1, "expected warning") + } else { + require.Len(t, diags, 0, "expected no warning") + } + }) + } +} diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 075f927650284..cb7aefb717ab0 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -95,6 +95,7 @@ type Options struct { } type server struct { + apiVersion string // lifecycleCtx must be tied to the API server's lifecycle // as when the API server shuts down, we want to cancel any // long-running operations. @@ -153,7 +154,9 @@ func (t Tags) Valid() error { return nil } -func NewServer(lifecycleCtx context.Context, +func NewServer( + lifecycleCtx context.Context, + apiVersion string, accessURL *url.URL, id uuid.UUID, organizationID uuid.UUID, @@ -214,6 +217,7 @@ func NewServer(lifecycleCtx context.Context, s := &server{ lifecycleCtx: lifecycleCtx, + apiVersion: apiVersion, AccessURL: accessURL, ID: id, OrganizationID: organizationID, @@ -1536,10 +1540,11 @@ func (s *server) CompleteJob(ctx context.Context, completed *proto.CompletedJob) } err = s.Database.InsertTemplateVersionTerraformValuesByJobID(ctx, database.InsertTemplateVersionTerraformValuesByJobIDParams{ - JobID: jobID, - UpdatedAt: now, - CachedPlan: plan, - CachedModuleFiles: fileID, + JobID: jobID, + UpdatedAt: now, + CachedPlan: plan, + CachedModuleFiles: fileID, + ProvisionerdVersion: s.apiVersion, }) if err != nil { return nil, xerrors.Errorf("insert template version terraform data: %w", err) diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index b6c60781dac35..e125db348e701 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -2993,6 +2993,7 @@ func setup(t *testing.T, ignoreLogErrors bool, ov *overrides) (proto.DRPCProvisi srv, err := provisionerdserver.NewServer( ov.ctx, + proto.CurrentVersion.String(), &url.URL{}, daemon.ID, defOrg.ID, diff --git a/enterprise/coderd/provisionerdaemons.go b/enterprise/coderd/provisionerdaemons.go index 0315209e29543..9039d2e97dbc5 100644 --- a/enterprise/coderd/provisionerdaemons.go +++ b/enterprise/coderd/provisionerdaemons.go @@ -336,6 +336,7 @@ func (api *API) provisionerDaemonServe(rw http.ResponseWriter, r *http.Request) logger.Info(ctx, "starting external provisioner daemon") srv, err := provisionerdserver.NewServer( srvCtx, + daemon.APIVersion, api.AccessURL, daemon.ID, authRes.orgID, From 9093dbc5160e7eab51486ec200e73cc08b71ac4b Mon Sep 17 00:00:00 2001 From: brettkolodny Date: Wed, 14 May 2025 13:51:45 -0400 Subject: [PATCH 46/88] feat: hide hidden and non-healthy apps in the workspaces table (#17830) Closes [coder/internal#633](https://github.com/coder/internal/issues/633) --- site/src/pages/WorkspacesPage/WorkspacesTable.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx index 389189bb7629c..047d7a6126b26 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx @@ -626,7 +626,9 @@ const WorkspaceApps: FC = ({ workspace }) => { builtinApps.delete("ssh_helper"); const remainingSlots = WORKSPACE_APPS_SLOTS - builtinApps.size; - const userApps = agent.apps.slice(0, remainingSlots); + const userApps = agent.apps + .filter((app) => app.health === "healthy" && !app.hidden) + .slice(0, remainingSlots); const buttons: ReactNode[] = []; From 73251cf5b2e556380c4854389dbe1743924796f1 Mon Sep 17 00:00:00 2001 From: brettkolodny Date: Wed, 14 May 2025 15:42:44 -0400 Subject: [PATCH 47/88] chore: add documentation to the coder ssh command regarding feature parity with ssh (#17827) Closes [coder/internal#628](https://github.com/coder/internal/issues/628) --------- Co-authored-by: M Atif Ali --- cli/ssh.go | 1 + cli/testdata/coder_ssh_--help.golden | 4 ++++ docs/reference/cli/ssh.md | 6 ++++++ docs/user-guides/workspace-access/index.md | 5 +++++ 4 files changed, 16 insertions(+) diff --git a/cli/ssh.go b/cli/ssh.go index 7c5bda073f973..ea812082b9882 100644 --- a/cli/ssh.go +++ b/cli/ssh.go @@ -92,6 +92,7 @@ func (r *RootCmd) ssh() *serpent.Command { Annotations: workspaceCommand, Use: "ssh ", Short: "Start a shell into a workspace", + Long: "This command does not have full parity with the standard SSH command. For users who need the full functionality of SSH, create an ssh configuration with `coder config-ssh`.", Middleware: serpent.Chain( serpent.RequireNArgs(1), r.InitClient(client), diff --git a/cli/testdata/coder_ssh_--help.golden b/cli/testdata/coder_ssh_--help.golden index 1f7122dd655a2..63a944a3fa2ea 100644 --- a/cli/testdata/coder_ssh_--help.golden +++ b/cli/testdata/coder_ssh_--help.golden @@ -5,6 +5,10 @@ USAGE: Start a shell into a workspace + This command does not have full parity with the standard SSH command. For + users who need the full functionality of SSH, create an ssh configuration with + `coder config-ssh`. + OPTIONS: --disable-autostart bool, $CODER_SSH_DISABLE_AUTOSTART (default: false) Disable starting the workspace automatically when connecting via SSH. diff --git a/docs/reference/cli/ssh.md b/docs/reference/cli/ssh.md index c5bae755c8419..959b75de5f89a 100644 --- a/docs/reference/cli/ssh.md +++ b/docs/reference/cli/ssh.md @@ -9,6 +9,12 @@ Start a shell into a workspace coder ssh [flags] ``` +## Description + +```console +This command does not have full parity with the standard SSH command. For users who need the full functionality of SSH, create an ssh configuration with `coder config-ssh`. +``` + ## Options ### --stdio diff --git a/docs/user-guides/workspace-access/index.md b/docs/user-guides/workspace-access/index.md index 7260cfe309a2d..ed7d152486bf1 100644 --- a/docs/user-guides/workspace-access/index.md +++ b/docs/user-guides/workspace-access/index.md @@ -33,6 +33,11 @@ coder ssh my-workspace Or, you can configure plain SSH on your client below. +> [!Note] +> The `coder ssh` command does not have full parity with the standard +> SSH command. For users who need the full functionality of SSH, use the +> configuration method below. + ### Configure SSH Coder generates [SSH key pairs](../../admin/security/secrets.md#ssh-keys) for From 35a04c7fb2389910472da393658d5a77e8f6e918 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 14 May 2025 17:11:32 -0300 Subject: [PATCH 48/88] refactor: use the new Table component for the Templates table (#17838) Screenshot 2025-05-14 at 15 11 56 --- .../pages/TemplatesPage/TemplatesPageView.tsx | 80 ++++++++++--------- 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/site/src/pages/TemplatesPage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/TemplatesPageView.tsx index 814efbe259f9b..a2b32fed58e7e 100644 --- a/site/src/pages/TemplatesPage/TemplatesPageView.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPageView.tsx @@ -2,12 +2,6 @@ import type { Interpolation, Theme } from "@emotion/react"; import ArrowForwardOutlined from "@mui/icons-material/ArrowForwardOutlined"; import MuiButton 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 { hasError, isApiValidationError } from "api/errors"; import type { Template, TemplateExample } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; @@ -33,6 +27,14 @@ import { PageHeaderTitle, } from "components/PageHeader/PageHeader"; import { Stack } from "components/Stack/Stack"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "components/Table/Table"; import { TableLoaderSkeleton, TableRowSkeleton, @@ -231,41 +233,41 @@ export const TemplatesPageView: FC = ({ )} - - - - - {Language.nameLabel} - - {showOrganizations ? "Organization" : Language.usedByLabel} - - {Language.buildTimeLabel} - {Language.lastUpdatedLabel} - - - - - {isLoading && } +
+ + + {Language.nameLabel} + + {showOrganizations ? "Organization" : Language.usedByLabel} + + {Language.buildTimeLabel} + + {Language.lastUpdatedLabel} + + + + + + {isLoading && } - {isEmpty ? ( - + ) : ( + templates?.map((template) => ( + - ) : ( - templates?.map((template) => ( - - )) - )} - -
-
+ )) + )} +
+ ); }; From b6d72c8dee5dbf167d54b18783aa2a5d61962b11 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 14 May 2025 22:18:10 -0300 Subject: [PATCH 49/88] chore: replace MUI LoadingButton - 4 (#17834) - ScheduleForm - SecurityForm - HistorySidebar - WorkspacesPageView --- .../SchedulePage/ScheduleForm.tsx | 10 ++++---- .../SecurityPage/SecurityForm.tsx | 12 ++++------ .../pages/WorkspacePage/HistorySidebar.tsx | 24 ++++++++----------- .../WorkspacesPage/WorkspacesPageView.tsx | 17 ++++++------- 4 files changed, 29 insertions(+), 34 deletions(-) diff --git a/site/src/pages/UserSettingsPage/SchedulePage/ScheduleForm.tsx b/site/src/pages/UserSettingsPage/SchedulePage/ScheduleForm.tsx index b30cb129f4827..c408ea30416d2 100644 --- a/site/src/pages/UserSettingsPage/SchedulePage/ScheduleForm.tsx +++ b/site/src/pages/UserSettingsPage/SchedulePage/ScheduleForm.tsx @@ -1,4 +1,3 @@ -import LoadingButton from "@mui/lab/LoadingButton"; import MenuItem from "@mui/material/MenuItem"; import TextField from "@mui/material/TextField"; import type { @@ -7,7 +6,9 @@ import type { } from "api/typesGenerated"; import { Alert } from "components/Alert/Alert"; import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { Button } from "components/Button/Button"; import { Form, FormFields } from "components/Form/Form"; +import { Spinner } from "components/Spinner/Spinner"; import { Stack } from "components/Stack/Stack"; import { type FormikContextType, useFormik } from "formik"; import { type FC, useEffect, useState } from "react"; @@ -137,14 +138,13 @@ export const ScheduleForm: FC = ({ />
- + Update schedule - +
diff --git a/site/src/pages/UserSettingsPage/SecurityPage/SecurityForm.tsx b/site/src/pages/UserSettingsPage/SecurityPage/SecurityForm.tsx index 12b69ae52082e..3153841622e83 100644 --- a/site/src/pages/UserSettingsPage/SecurityPage/SecurityForm.tsx +++ b/site/src/pages/UserSettingsPage/SecurityPage/SecurityForm.tsx @@ -1,9 +1,10 @@ -import LoadingButton from "@mui/lab/LoadingButton"; import TextField from "@mui/material/TextField"; import { Alert } from "components/Alert/Alert"; import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { Button } from "components/Button/Button"; import { Form, FormFields } from "components/Form/Form"; import { PasswordField } from "components/PasswordField/PasswordField"; +import { Spinner } from "components/Spinner/Spinner"; import { type FormikContextType, useFormik } from "formik"; import type { FC } from "react"; import { getFormHelpers } from "utils/formUtils"; @@ -98,13 +99,10 @@ export const SecurityForm: FC = ({ />
- +
diff --git a/site/src/pages/WorkspacePage/HistorySidebar.tsx b/site/src/pages/WorkspacePage/HistorySidebar.tsx index 0d5960210e755..2d978fb2a7d83 100644 --- a/site/src/pages/WorkspacePage/HistorySidebar.tsx +++ b/site/src/pages/WorkspacePage/HistorySidebar.tsx @@ -1,13 +1,14 @@ import ArrowDownwardOutlined from "@mui/icons-material/ArrowDownwardOutlined"; -import LoadingButton from "@mui/lab/LoadingButton"; import { infiniteWorkspaceBuilds } from "api/queries/workspaceBuilds"; import type { Workspace } from "api/typesGenerated"; +import { Button } from "components/Button/Button"; import { Sidebar, SidebarCaption, SidebarItem, SidebarLink, } from "components/FullPageLayout/Sidebar"; +import { Spinner } from "components/Spinner/Spinner"; import { WorkspaceBuildData, WorkspaceBuildDataSkeleton, @@ -46,22 +47,17 @@ export const HistorySidebar: FC = ({ workspace }) => { ))} {buildsQuery.hasNextPage && (
- buildsQuery.fetchNextPage()} - loading={buildsQuery.isFetchingNextPage} - loadingPosition="start" - variant="outlined" - color="neutral" - startIcon={} - css={{ - display: "inline-flex", - borderRadius: "9999px", - fontSize: 13, - }} + disabled={buildsQuery.isFetchingNextPage} + variant="outline" + className="w-full" > + + + Show more builds - +
)} diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index 569e3df0d347c..f4fd998f87410 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -1,5 +1,4 @@ import CloudQueue from "@mui/icons-material/CloudQueue"; -import LoadingButton from "@mui/lab/LoadingButton"; import { hasError, isApiValidationError } from "api/errors"; import type { Template, Workspace } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; @@ -16,6 +15,7 @@ import { Margins } from "components/Margins/Margins"; import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; import { PaginationHeader } from "components/PaginationWidget/PaginationHeader"; import { PaginationWidgetBase } from "components/PaginationWidget/PaginationWidgetBase"; +import { Spinner } from "components/Spinner/Spinner"; import { Stack } from "components/Stack/Stack"; import { TableToolbar } from "components/TableToolbar/TableToolbar"; import { ChevronDownIcon, PlayIcon, SquareIcon, TrashIcon } from "lucide-react"; @@ -135,16 +135,17 @@ export const WorkspacesPageView: FC = ({ - } > Bulk actions - + + + + Date: Thu, 15 May 2025 11:29:26 +0300 Subject: [PATCH 50/88] refactor(agent/agentcontainers): update routes and locking in container api (#17768) This refactor updates the devcontainer routes and in-api locking for better clarity. Updates #16424 --- agent/agentcontainers/api.go | 136 ++++++++++++++++++------------ agent/agentcontainers/api_test.go | 4 +- 2 files changed, 83 insertions(+), 57 deletions(-) diff --git a/agent/agentcontainers/api.go b/agent/agentcontainers/api.go index c3779af67633a..9ecf70a4681a1 100644 --- a/agent/agentcontainers/api.go +++ b/agent/agentcontainers/api.go @@ -214,8 +214,10 @@ func (api *API) Routes() http.Handler { r := chi.NewRouter() r.Get("/", api.handleList) - r.Get("/devcontainers", api.handleListDevcontainers) - r.Post("/{id}/recreate", api.handleRecreate) + r.Route("/devcontainers", func(r chi.Router) { + r.Get("/", api.handleDevcontainersList) + r.Post("/container/{container}/recreate", api.handleDevcontainerRecreate) + }) return r } @@ -376,12 +378,13 @@ func (api *API) getContainers(ctx context.Context) (codersdk.WorkspaceAgentListC return copyListContainersResponse(api.containers), nil } -// handleRecreate handles the HTTP request to recreate a container. -func (api *API) handleRecreate(w http.ResponseWriter, r *http.Request) { +// handleDevcontainerRecreate handles the HTTP request to recreate a +// devcontainer by referencing the container. +func (api *API) handleDevcontainerRecreate(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - id := chi.URLParam(r, "id") + containerID := chi.URLParam(r, "container") - if id == "" { + if containerID == "" { httpapi.Write(ctx, w, http.StatusBadRequest, codersdk.Response{ Message: "Missing container ID or name", Detail: "Container ID or name is required to recreate a devcontainer.", @@ -399,7 +402,7 @@ func (api *API) handleRecreate(w http.ResponseWriter, r *http.Request) { } containerIdx := slices.IndexFunc(containers.Containers, func(c codersdk.WorkspaceAgentContainer) bool { - return c.Match(id) + return c.Match(containerID) }) if containerIdx == -1 { httpapi.Write(ctx, w, http.StatusNotFound, codersdk.Response{ @@ -418,7 +421,7 @@ func (api *API) handleRecreate(w http.ResponseWriter, r *http.Request) { if workspaceFolder == "" { httpapi.Write(ctx, w, http.StatusBadRequest, codersdk.Response{ Message: "Missing workspace folder label", - Detail: "The workspace folder label is required to recreate a devcontainer.", + Detail: "The container is not a devcontainer, the container must have the workspace folder label to support recreation.", }) return } @@ -434,32 +437,28 @@ func (api *API) handleRecreate(w http.ResponseWriter, r *http.Request) { // TODO(mafredri): Temporarily handle clearing the dirty state after // recreation, later on this should be handled by a "container watcher". - select { - case <-api.ctx.Done(): - return - case <-ctx.Done(): - return - case api.lockCh <- struct{}{}: - defer func() { <-api.lockCh }() - } - for i := range api.knownDevcontainers { - if api.knownDevcontainers[i].WorkspaceFolder == workspaceFolder { - if api.knownDevcontainers[i].Dirty { - api.logger.Info(ctx, "clearing dirty flag after recreation", - slog.F("workspace_folder", workspaceFolder), - slog.F("name", api.knownDevcontainers[i].Name), - ) - api.knownDevcontainers[i].Dirty = false + if !api.doLockedHandler(w, r, func() { + for i := range api.knownDevcontainers { + if api.knownDevcontainers[i].WorkspaceFolder == workspaceFolder { + if api.knownDevcontainers[i].Dirty { + api.logger.Info(ctx, "clearing dirty flag after recreation", + slog.F("workspace_folder", workspaceFolder), + slog.F("name", api.knownDevcontainers[i].Name), + ) + api.knownDevcontainers[i].Dirty = false + } + return } - break } + }) { + return } w.WriteHeader(http.StatusNoContent) } -// handleListDevcontainers handles the HTTP request to list known devcontainers. -func (api *API) handleListDevcontainers(w http.ResponseWriter, r *http.Request) { +// handleDevcontainersList handles the HTTP request to list known devcontainers. +func (api *API) handleDevcontainersList(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // Run getContainers to detect the latest devcontainers and their state. @@ -472,15 +471,12 @@ func (api *API) handleListDevcontainers(w http.ResponseWriter, r *http.Request) return } - select { - case <-api.ctx.Done(): + var devcontainers []codersdk.WorkspaceAgentDevcontainer + if !api.doLockedHandler(w, r, func() { + devcontainers = slices.Clone(api.knownDevcontainers) + }) { return - case <-ctx.Done(): - return - case api.lockCh <- struct{}{}: } - devcontainers := slices.Clone(api.knownDevcontainers) - <-api.lockCh slices.SortFunc(devcontainers, func(a, b codersdk.WorkspaceAgentDevcontainer) int { if cmp := strings.Compare(a.WorkspaceFolder, b.WorkspaceFolder); cmp != 0 { @@ -499,34 +495,64 @@ func (api *API) handleListDevcontainers(w http.ResponseWriter, r *http.Request) // markDevcontainerDirty finds the devcontainer with the given config file path // and marks it as dirty. It acquires the lock before modifying the state. func (api *API) markDevcontainerDirty(configPath string, modifiedAt time.Time) { + ok := api.doLocked(func() { + // Record the timestamp of when this configuration file was modified. + api.configFileModifiedTimes[configPath] = modifiedAt + + for i := range api.knownDevcontainers { + if api.knownDevcontainers[i].ConfigPath != configPath { + continue + } + + // TODO(mafredri): Simplistic mark for now, we should check if the + // container is running and if the config file was modified after + // the container was created. + if !api.knownDevcontainers[i].Dirty { + api.logger.Info(api.ctx, "marking devcontainer as dirty", + slog.F("file", configPath), + slog.F("name", api.knownDevcontainers[i].Name), + slog.F("workspace_folder", api.knownDevcontainers[i].WorkspaceFolder), + slog.F("modified_at", modifiedAt), + ) + api.knownDevcontainers[i].Dirty = true + } + } + }) + if !ok { + api.logger.Debug(api.ctx, "mark devcontainer dirty failed", slog.F("file", configPath)) + } +} + +func (api *API) doLockedHandler(w http.ResponseWriter, r *http.Request, f func()) bool { select { + case <-r.Context().Done(): + httpapi.Write(r.Context(), w, http.StatusRequestTimeout, codersdk.Response{ + Message: "Request canceled", + Detail: "Request was canceled before we could process it.", + }) + return false case <-api.ctx.Done(): - return + httpapi.Write(r.Context(), w, http.StatusServiceUnavailable, codersdk.Response{ + Message: "API closed", + Detail: "The API is closed and cannot process requests.", + }) + return false case api.lockCh <- struct{}{}: defer func() { <-api.lockCh }() } + f() + return true +} - // Record the timestamp of when this configuration file was modified. - api.configFileModifiedTimes[configPath] = modifiedAt - - for i := range api.knownDevcontainers { - if api.knownDevcontainers[i].ConfigPath != configPath { - continue - } - - // TODO(mafredri): Simplistic mark for now, we should check if the - // container is running and if the config file was modified after - // the container was created. - if !api.knownDevcontainers[i].Dirty { - api.logger.Info(api.ctx, "marking devcontainer as dirty", - slog.F("file", configPath), - slog.F("name", api.knownDevcontainers[i].Name), - slog.F("workspace_folder", api.knownDevcontainers[i].WorkspaceFolder), - slog.F("modified_at", modifiedAt), - ) - api.knownDevcontainers[i].Dirty = true - } +func (api *API) doLocked(f func()) bool { + select { + case <-api.ctx.Done(): + return false + case api.lockCh <- struct{}{}: + defer func() { <-api.lockCh }() } + f() + return true } func (api *API) Close() error { diff --git a/agent/agentcontainers/api_test.go b/agent/agentcontainers/api_test.go index 45044b4e43e2e..d6cac1cd2a97e 100644 --- a/agent/agentcontainers/api_test.go +++ b/agent/agentcontainers/api_test.go @@ -173,7 +173,7 @@ func TestAPI(t *testing.T) { wantBody string }{ { - name: "Missing ID", + name: "Missing container ID", containerID: "", lister: &fakeLister{}, devcontainerCLI: &fakeDevcontainerCLI{}, @@ -260,7 +260,7 @@ func TestAPI(t *testing.T) { r.Mount("/", api.Routes()) // Simulate HTTP request to the recreate endpoint. - req := httptest.NewRequest(http.MethodPost, "/"+tt.containerID+"/recreate", nil) + req := httptest.NewRequest(http.MethodPost, "/devcontainers/container/"+tt.containerID+"/recreate", nil) rec := httptest.NewRecorder() r.ServeHTTP(rec, req) From 522c17827154c87e143e019427b9aac7d2c5e503 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Thu, 15 May 2025 12:49:52 +0300 Subject: [PATCH 51/88] fix(agent/agentcontainers): always use /bin/sh for devcontainer autostart (#17847) This fixes startup issues when the user shell is set to Fish. Refs: #17845 --- agent/agentcontainers/devcontainer.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/agent/agentcontainers/devcontainer.go b/agent/agentcontainers/devcontainer.go index e04c308934a2c..09d4837d4b27a 100644 --- a/agent/agentcontainers/devcontainer.go +++ b/agent/agentcontainers/devcontainer.go @@ -22,7 +22,8 @@ const ( const devcontainerUpScriptTemplate = ` if ! which devcontainer > /dev/null 2>&1; then - echo "ERROR: Unable to start devcontainer, @devcontainers/cli is not installed." + echo "ERROR: Unable to start devcontainer, @devcontainers/cli is not installed or not found in \$PATH." 1>&2 + echo "Please install @devcontainers/cli by running \"npm install -g @devcontainers/cli\" or by using the \"devcontainers-cli\" Coder module." 1>&2 exit 1 fi devcontainer up %s @@ -65,7 +66,9 @@ func devcontainerStartupScript(dc codersdk.WorkspaceAgentDevcontainer, script co args = append(args, fmt.Sprintf("--config %q", dc.ConfigPath)) } cmd := fmt.Sprintf(devcontainerUpScriptTemplate, strings.Join(args, " ")) - script.Script = cmd + // Force the script to run in /bin/sh, since some shells (e.g. fish) + // don't support the script. + script.Script = fmt.Sprintf("/bin/sh -c '%s'", cmd) // Disable RunOnStart, scripts have this set so that when devcontainers // have not been enabled, a warning will be surfaced in the agent logs. script.RunOnStart = false From f2edcf3f59e600b8b23b32840df62b2c26fafbea Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Thu, 15 May 2025 13:02:30 +0200 Subject: [PATCH 52/88] fix: add missing clause for tracking replacements (#17849) We should only be tracking resource replacements during a prebuild claim. Signed-off-by: Danny Kopping --- coderd/provisionerdserver/provisionerdserver.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index cb7aefb717ab0..38924300ac7a2 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -1836,7 +1836,7 @@ func (s *server) CompleteJob(ctx context.Context, completed *proto.CompletedJob) }) } - if s.PrebuildsOrchestrator != nil { + if s.PrebuildsOrchestrator != nil && input.PrebuiltWorkspaceBuildStage == sdkproto.PrebuiltWorkspaceBuildStage_CLAIM { // Track resource replacements, if there are any. orchestrator := s.PrebuildsOrchestrator.Load() if resourceReplacements := completed.GetWorkspaceBuild().GetResourceReplacements(); orchestrator != nil && len(resourceReplacements) > 0 { From 2aa8cbebd71fa691c9443a0992187906a87b8b20 Mon Sep 17 00:00:00 2001 From: Yevhenii Shcherbina Date: Thu, 15 May 2025 07:33:58 -0400 Subject: [PATCH 53/88] fix: exclude deleted templates from metrics collection (#17839) Also add some clarification about the lack of database constraints for soft template deletion. --------- Signed-off-by: Danny Kopping Co-authored-by: Danny Kopping --- coderd/database/queries.sql.go | 4 + coderd/database/queries/prebuilds.sql | 4 + .../coderd/prebuilds/metricscollector.go | 4 + .../coderd/prebuilds/metricscollector_test.go | 189 ++++++++++++++++-- enterprise/coderd/prebuilds/reconcile_test.go | 34 +++- 5 files changed, 213 insertions(+), 22 deletions(-) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 0fd886cf39f2b..af20e4b4403ff 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -6149,6 +6149,7 @@ WHERE w.id IN ( AND b.template_version_id = t.active_version_id AND p.current_preset_id = $3::uuid AND p.ready + AND NOT t.deleted LIMIT 1 FOR UPDATE OF p SKIP LOCKED -- Ensure that a concurrent request will not select the same prebuild. ) RETURNING w.id, w.name @@ -6184,6 +6185,7 @@ FROM workspace_latest_builds wlb -- prebuilds that are still building. INNER JOIN templates t ON t.active_version_id = wlb.template_version_id WHERE wlb.job_status IN ('pending'::provisioner_job_status, 'running'::provisioner_job_status) + -- AND NOT t.deleted -- We don't exclude deleted templates because there's no constraint in the DB preventing a soft deletion on a template while workspaces are running. GROUP BY t.id, wpb.template_version_id, wpb.transition, wlb.template_version_preset_id ` @@ -6298,6 +6300,7 @@ WITH filtered_builds AS ( WHERE tvp.desired_instances IS NOT NULL -- Consider only presets that have a prebuild configuration. AND wlb.transition = 'start'::workspace_transition AND w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0' + AND NOT t.deleted ), time_sorted_builds AS ( -- Group builds by preset, then sort each group by created_at. @@ -6449,6 +6452,7 @@ FROM templates t INNER JOIN template_version_presets tvp ON tvp.template_version_id = tv.id INNER JOIN organizations o ON o.id = t.organization_id WHERE tvp.desired_instances IS NOT NULL -- Consider only presets that have a prebuild configuration. + -- AND NOT t.deleted -- We don't exclude deleted templates because there's no constraint in the DB preventing a soft deletion on a template while workspaces are running. AND (t.id = $1::uuid OR $1 IS NULL) ` diff --git a/coderd/database/queries/prebuilds.sql b/coderd/database/queries/prebuilds.sql index 1d3a827c98586..8c27ddf62b7c3 100644 --- a/coderd/database/queries/prebuilds.sql +++ b/coderd/database/queries/prebuilds.sql @@ -15,6 +15,7 @@ WHERE w.id IN ( AND b.template_version_id = t.active_version_id AND p.current_preset_id = @preset_id::uuid AND p.ready + AND NOT t.deleted LIMIT 1 FOR UPDATE OF p SKIP LOCKED -- Ensure that a concurrent request will not select the same prebuild. ) RETURNING w.id, w.name; @@ -40,6 +41,7 @@ FROM templates t INNER JOIN template_version_presets tvp ON tvp.template_version_id = tv.id INNER JOIN organizations o ON o.id = t.organization_id WHERE tvp.desired_instances IS NOT NULL -- Consider only presets that have a prebuild configuration. + -- AND NOT t.deleted -- We don't exclude deleted templates because there's no constraint in the DB preventing a soft deletion on a template while workspaces are running. AND (t.id = sqlc.narg('template_id')::uuid OR sqlc.narg('template_id') IS NULL); -- name: GetRunningPrebuiltWorkspaces :many @@ -70,6 +72,7 @@ FROM workspace_latest_builds wlb -- prebuilds that are still building. INNER JOIN templates t ON t.active_version_id = wlb.template_version_id WHERE wlb.job_status IN ('pending'::provisioner_job_status, 'running'::provisioner_job_status) + -- AND NOT t.deleted -- We don't exclude deleted templates because there's no constraint in the DB preventing a soft deletion on a template while workspaces are running. GROUP BY t.id, wpb.template_version_id, wpb.transition, wlb.template_version_preset_id; -- GetPresetsBackoff groups workspace builds by preset ID. @@ -98,6 +101,7 @@ WITH filtered_builds AS ( WHERE tvp.desired_instances IS NOT NULL -- Consider only presets that have a prebuild configuration. AND wlb.transition = 'start'::workspace_transition AND w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0' + AND NOT t.deleted ), time_sorted_builds AS ( -- Group builds by preset, then sort each group by created_at. diff --git a/enterprise/coderd/prebuilds/metricscollector.go b/enterprise/coderd/prebuilds/metricscollector.go index 9f1cc837d0474..7a7734b6f8093 100644 --- a/enterprise/coderd/prebuilds/metricscollector.go +++ b/enterprise/coderd/prebuilds/metricscollector.go @@ -157,6 +157,10 @@ func (mc *MetricsCollector) Collect(metricsCh chan<- prometheus.Metric) { continue } + if preset.Deleted { + continue + } + presetSnapshot, err := currentState.snapshot.FilterByPreset(preset.ID) if err != nil { mc.logger.Error(context.Background(), "failed to filter by preset", slog.Error(err)) diff --git a/enterprise/coderd/prebuilds/metricscollector_test.go b/enterprise/coderd/prebuilds/metricscollector_test.go index 07c3c3c6996bb..dce9e07dd110f 100644 --- a/enterprise/coderd/prebuilds/metricscollector_test.go +++ b/enterprise/coderd/prebuilds/metricscollector_test.go @@ -19,6 +19,7 @@ import ( "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbgen" "github.com/coder/coder/v2/coderd/database/dbtestutil" + "github.com/coder/coder/v2/coderd/database/dbtime" agplprebuilds "github.com/coder/coder/v2/coderd/prebuilds" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/enterprise/coderd/prebuilds" @@ -165,27 +166,12 @@ func TestMetricsCollector(t *testing.T) { eligible: []bool{false}, }, { - name: "deleted templates never desire prebuilds", - transitions: allTransitions, - jobStatuses: allJobStatuses, - initiatorIDs: []uuid.UUID{agplprebuilds.SystemUserID}, - ownerIDs: []uuid.UUID{agplprebuilds.SystemUserID, uuid.New()}, - metrics: []metricCheck{ - {prebuilds.MetricDesiredGauge, ptr.To(0.0), false}, - }, - templateDeleted: []bool{true}, - eligible: []bool{false}, - }, - { - name: "running prebuilds for deleted templates are still counted, so that they can be deleted", - transitions: []database.WorkspaceTransition{database.WorkspaceTransitionStart}, - jobStatuses: []database.ProvisionerJobStatus{database.ProvisionerJobStatusSucceeded}, - initiatorIDs: []uuid.UUID{agplprebuilds.SystemUserID}, - ownerIDs: []uuid.UUID{agplprebuilds.SystemUserID}, - metrics: []metricCheck{ - {prebuilds.MetricRunningGauge, ptr.To(1.0), false}, - {prebuilds.MetricEligibleGauge, ptr.To(0.0), false}, - }, + name: "deleted templates should not be included in exported metrics", + transitions: allTransitions, + jobStatuses: allJobStatuses, + initiatorIDs: []uuid.UUID{agplprebuilds.SystemUserID}, + ownerIDs: []uuid.UUID{agplprebuilds.SystemUserID, uuid.New()}, + metrics: nil, templateDeleted: []bool{true}, eligible: []bool{false}, }, @@ -281,6 +267,12 @@ func TestMetricsCollector(t *testing.T) { "organization_name": org.Name, } + // If no expected metrics have been defined, ensure we don't find any metric series (i.e. metrics with given labels). + if test.metrics == nil { + series := findAllMetricSeries(metricsFamilies, labels) + require.Empty(t, series) + } + for _, check := range test.metrics { metric := findMetric(metricsFamilies, check.name, labels) if check.value == nil { @@ -307,6 +299,131 @@ func TestMetricsCollector(t *testing.T) { } } +// TestMetricsCollector_DuplicateTemplateNames validates a bug that we saw previously which caused duplicate metric series +// registration when a template was deleted and a new one created with the same name (and preset name). +// We are now excluding deleted templates from our metric collection. +func TestMetricsCollector_DuplicateTemplateNames(t *testing.T) { + t.Parallel() + + if !dbtestutil.WillUsePostgres() { + t.Skip("this test requires postgres") + } + + type metricCheck struct { + name string + value *float64 + isCounter bool + } + + type testCase struct { + transition database.WorkspaceTransition + jobStatus database.ProvisionerJobStatus + initiatorID uuid.UUID + ownerID uuid.UUID + metrics []metricCheck + eligible bool + } + + test := testCase{ + transition: database.WorkspaceTransitionStart, + jobStatus: database.ProvisionerJobStatusSucceeded, + initiatorID: agplprebuilds.SystemUserID, + ownerID: agplprebuilds.SystemUserID, + metrics: []metricCheck{ + {prebuilds.MetricCreatedCount, ptr.To(1.0), true}, + {prebuilds.MetricClaimedCount, ptr.To(0.0), true}, + {prebuilds.MetricFailedCount, ptr.To(0.0), true}, + {prebuilds.MetricDesiredGauge, ptr.To(1.0), false}, + {prebuilds.MetricRunningGauge, ptr.To(1.0), false}, + {prebuilds.MetricEligibleGauge, ptr.To(1.0), false}, + }, + eligible: true, + } + + logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}) + clock := quartz.NewMock(t) + db, pubsub := dbtestutil.NewDB(t) + reconciler := prebuilds.NewStoreReconciler(db, pubsub, codersdk.PrebuildsConfig{}, logger, quartz.NewMock(t), prometheus.NewRegistry(), newNoopEnqueuer()) + ctx := testutil.Context(t, testutil.WaitLong) + + collector := prebuilds.NewMetricsCollector(db, logger, reconciler) + registry := prometheus.NewPedanticRegistry() + registry.Register(collector) + + presetName := "default-preset" + defaultOrg := dbgen.Organization(t, db, database.Organization{}) + setupTemplateWithDeps := func() database.Template { + template := setupTestDBTemplateWithinOrg(t, db, test.ownerID, false, "default-template", defaultOrg) + templateVersionID := setupTestDBTemplateVersion(ctx, t, clock, db, pubsub, defaultOrg.ID, test.ownerID, template.ID) + preset := setupTestDBPreset(t, db, templateVersionID, 1, "default-preset") + workspace, _ := setupTestDBWorkspace( + t, clock, db, pubsub, + test.transition, test.jobStatus, defaultOrg.ID, preset, template.ID, templateVersionID, test.initiatorID, test.ownerID, + ) + setupTestDBWorkspaceAgent(t, db, workspace.ID, test.eligible) + return template + } + + // When: starting with a regular template. + template := setupTemplateWithDeps() + labels := map[string]string{ + "template_name": template.Name, + "preset_name": presetName, + "organization_name": defaultOrg.Name, + } + + // nolint:gocritic // Authz context needed to retrieve state. + ctx = dbauthz.AsPrebuildsOrchestrator(ctx) + + // Then: metrics collect successfully. + require.NoError(t, collector.UpdateState(ctx, testutil.WaitLong)) + metricsFamilies, err := registry.Gather() + require.NoError(t, err) + require.NotEmpty(t, findAllMetricSeries(metricsFamilies, labels)) + + // When: the template is deleted. + require.NoError(t, db.UpdateTemplateDeletedByID(ctx, database.UpdateTemplateDeletedByIDParams{ + ID: template.ID, + Deleted: true, + UpdatedAt: dbtime.Now(), + })) + + // Then: metrics collect successfully but are empty because the template is deleted. + require.NoError(t, collector.UpdateState(ctx, testutil.WaitLong)) + metricsFamilies, err = registry.Gather() + require.NoError(t, err) + require.Empty(t, findAllMetricSeries(metricsFamilies, labels)) + + // When: a new template is created with the same name as the deleted template. + newTemplate := setupTemplateWithDeps() + + // Ensure the database has both the new and old (delete) template. + { + deleted, err := db.GetTemplateByOrganizationAndName(ctx, database.GetTemplateByOrganizationAndNameParams{ + OrganizationID: template.OrganizationID, + Deleted: true, + Name: template.Name, + }) + require.NoError(t, err) + require.Equal(t, template.ID, deleted.ID) + + current, err := db.GetTemplateByOrganizationAndName(ctx, database.GetTemplateByOrganizationAndNameParams{ + // Use details from deleted template to ensure they're aligned. + OrganizationID: template.OrganizationID, + Deleted: false, + Name: template.Name, + }) + require.NoError(t, err) + require.Equal(t, newTemplate.ID, current.ID) + } + + // Then: metrics collect successfully. + require.NoError(t, collector.UpdateState(ctx, testutil.WaitLong)) + metricsFamilies, err = registry.Gather() + require.NoError(t, err) + require.NotEmpty(t, findAllMetricSeries(metricsFamilies, labels)) +} + func findMetric(metricsFamilies []*prometheus_client.MetricFamily, name string, labels map[string]string) *prometheus_client.Metric { for _, metricFamily := range metricsFamilies { if metricFamily.GetName() != name { @@ -334,3 +451,33 @@ func findMetric(metricsFamilies []*prometheus_client.MetricFamily, name string, } return nil } + +// findAllMetricSeries finds all metrics with a given set of labels. +func findAllMetricSeries(metricsFamilies []*prometheus_client.MetricFamily, labels map[string]string) map[string]*prometheus_client.Metric { + series := make(map[string]*prometheus_client.Metric) + for _, metricFamily := range metricsFamilies { + for _, metric := range metricFamily.GetMetric() { + labelPairs := metric.GetLabel() + + if len(labelPairs) != len(labels) { + continue + } + + // Convert label pairs to map for easier lookup + metricLabels := make(map[string]string, len(labelPairs)) + for _, label := range labelPairs { + metricLabels[label.GetName()] = label.GetValue() + } + + // Check if all requested labels match + for wantName, wantValue := range labels { + if metricLabels[wantName] != wantValue { + continue + } + } + + series[metricFamily.GetName()] = metric + } + } + return series +} diff --git a/enterprise/coderd/prebuilds/reconcile_test.go b/enterprise/coderd/prebuilds/reconcile_test.go index bdf447dcfae22..660b1733e6cc9 100644 --- a/enterprise/coderd/prebuilds/reconcile_test.go +++ b/enterprise/coderd/prebuilds/reconcile_test.go @@ -294,10 +294,15 @@ func TestPrebuildReconciliation(t *testing.T) { templateDeleted: []bool{false}, }, { - name: "delete prebuilds for deleted templates", + // Templates can be soft-deleted (`deleted=true`) or hard-deleted (row is removed). + // On the former there is *no* DB constraint to prevent soft deletion, so we have to ensure that if somehow + // the template was soft-deleted any running prebuilds will be removed. + // On the latter there is a DB constraint to prevent row deletion if any workspaces reference the deleting template. + name: "soft-deleted templates MAY have prebuilds", prebuildLatestTransitions: []database.WorkspaceTransition{database.WorkspaceTransitionStart}, prebuildJobStatuses: []database.ProvisionerJobStatus{database.ProvisionerJobStatusSucceeded}, templateVersionActive: []bool{true, false}, + shouldCreateNewPrebuild: ptr.To(false), shouldDeleteOldPrebuild: ptr.To(true), templateDeleted: []bool{true}, }, @@ -1060,6 +1065,33 @@ func setupTestDBTemplate( return org, template } +// nolint:revive // It's a control flag, but this is a test. +func setupTestDBTemplateWithinOrg( + t *testing.T, + db database.Store, + userID uuid.UUID, + templateDeleted bool, + templateName string, + org database.Organization, +) database.Template { + t.Helper() + + template := dbgen.Template(t, db, database.Template{ + Name: templateName, + CreatedBy: userID, + OrganizationID: org.ID, + CreatedAt: time.Now().Add(muchEarlier), + }) + if templateDeleted { + ctx := testutil.Context(t, testutil.WaitShort) + require.NoError(t, db.UpdateTemplateDeletedByID(ctx, database.UpdateTemplateDeletedByIDParams{ + ID: template.ID, + Deleted: true, + })) + } + return template +} + const ( earlier = -time.Hour muchEarlier = -time.Hour * 2 From 6e1ba75b06c4f5044fc67734f0128299adfb0f05 Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Thu, 15 May 2025 14:11:36 +0200 Subject: [PATCH 54/88] chore: retry failed race tests in CI (#17846) This PR enables retrying failed tests in the race suites unless a data race was detected. The goal is to reduce how often flakes disrupt developers' workflows. I bumped gotestsum to a revision from the `main` branch because it includes the `--rerun-fails-abort-on-data-race` flag which [I recently contributed](https://github.com/gotestyourself/gotestsum/pull/497). Incidentally, you can see it [in action in a CI job on this very PR](https://github.com/coder/coder/actions/runs/15040840724/job/42271999592?pr=17846#step:8:647). --- .github/actions/setup-go/action.yaml | 2 +- .github/workflows/ci.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/setup-go/action.yaml b/.github/actions/setup-go/action.yaml index 4d91a9b2acb07..6ee57ff57db6b 100644 --- a/.github/actions/setup-go/action.yaml +++ b/.github/actions/setup-go/action.yaml @@ -42,7 +42,7 @@ runs: - name: Install gotestsum shell: bash - run: go install gotest.tools/gotestsum@3f7ff0ec4aeb6f95f5d67c998b71f272aa8a8b41 # v1.12.1 + run: go install gotest.tools/gotestsum@0d9599e513d70e5792bb9334869f82f6e8b53d4d # main as of 2025-05-15 # It isn't necessary that we ever do this, but it helps # separate the "setup" from the "run" times. diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f27885314b8e7..6901a7d52358f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -613,7 +613,7 @@ jobs: # c.f. discussion on https://github.com/coder/coder/pull/15106 - name: Run Tests run: | - gotestsum --junitfile="gotests.xml" -- -race -parallel 4 -p 4 ./... + gotestsum --junitfile="gotests.xml" --packages="./..." --rerun-fails=2 --rerun-fails-abort-on-data-race -- -race -parallel 4 -p 4 - name: Upload Test Cache uses: ./.github/actions/test-cache/upload @@ -665,7 +665,7 @@ jobs: POSTGRES_VERSION: "16" run: | make test-postgres-docker - DB=ci gotestsum --junitfile="gotests.xml" -- -race -parallel 4 -p 4 ./... + DB=ci gotestsum --junitfile="gotests.xml" --packages="./..." --rerun-fails=2 --rerun-fails-abort-on-data-race -- -race -parallel 4 -p 4 - name: Upload Test Cache uses: ./.github/actions/test-cache/upload From 3de0003e4b48cef31146530242b521b32a4cf94d Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Thu, 15 May 2025 16:06:56 +0300 Subject: [PATCH 55/88] feat(agent): send devcontainer CLI logs during recreate (#17845) We need a way to surface what's happening to the user, since autostart logs here, it's natural we do so during re-create as well. Updates #16424 --- agent/agent_test.go | 186 ++++++++++++++++-- agent/agentcontainers/api.go | 78 +++++++- agent/agentcontainers/api_test.go | 11 +- agent/agentcontainers/devcontainercli.go | 30 ++- agent/agentcontainers/devcontainercli_test.go | 39 ++++ agent/api.go | 7 +- codersdk/workspacesdk/agentconn.go | 16 ++ 7 files changed, 342 insertions(+), 25 deletions(-) diff --git a/agent/agent_test.go b/agent/agent_test.go index fe2c99059e9d8..741d245ec3f6f 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -1935,8 +1935,6 @@ func TestAgent_ReconnectingPTYContainer(t *testing.T) { t.Skip("Set CODER_TEST_USE_DOCKER=1 to run this test") } - ctx := testutil.Context(t, testutil.WaitLong) - pool, err := dockertest.NewPool("") require.NoError(t, err, "Could not connect to docker") ct, err := pool.RunWithOptions(&dockertest.RunOptions{ @@ -1948,10 +1946,10 @@ func TestAgent_ReconnectingPTYContainer(t *testing.T) { config.RestartPolicy = docker.RestartPolicy{Name: "no"} }) require.NoError(t, err, "Could not start container") - t.Cleanup(func() { + defer func() { err := pool.Purge(ct) require.NoError(t, err, "Could not stop container") - }) + }() // Wait for container to start require.Eventually(t, func() bool { ct, ok := pool.ContainerByName(ct.Container.Name) @@ -1962,6 +1960,7 @@ func TestAgent_ReconnectingPTYContainer(t *testing.T) { conn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0, func(_ *agenttest.Client, o *agent.Options) { o.ExperimentalDevcontainersEnabled = true }) + ctx := testutil.Context(t, testutil.WaitLong) ac, err := conn.ReconnectingPTY(ctx, uuid.New(), 80, 80, "/bin/sh", func(arp *workspacesdk.AgentReconnectingPTYInit) { arp.Container = ct.Container.ID }) @@ -2005,9 +2004,6 @@ func TestAgent_DevcontainerAutostart(t *testing.T) { t.Skip("Set CODER_TEST_USE_DOCKER=1 to run this test") } - ctx := testutil.Context(t, testutil.WaitLong) - - // Connect to Docker pool, err := dockertest.NewPool("") require.NoError(t, err, "Could not connect to docker") @@ -2051,7 +2047,7 @@ func TestAgent_DevcontainerAutostart(t *testing.T) { }, }, } - // nolint: dogsled + //nolint:dogsled conn, _, _, _, _ := setupAgent(t, manifest, 0, func(_ *agenttest.Client, o *agent.Options) { o.ExperimentalDevcontainersEnabled = true }) @@ -2079,8 +2075,7 @@ func TestAgent_DevcontainerAutostart(t *testing.T) { return false }, testutil.WaitSuperLong, testutil.IntervalMedium, "no container with workspace folder label found") - - t.Cleanup(func() { + defer func() { // We can't rely on pool here because the container is not // managed by it (it is managed by @devcontainer/cli). err := pool.Client.RemoveContainer(docker.RemoveContainerOptions{ @@ -2089,13 +2084,15 @@ func TestAgent_DevcontainerAutostart(t *testing.T) { Force: true, }) assert.NoError(t, err, "remove container") - }) + }() containerInfo, err := pool.Client.InspectContainer(container.ID) require.NoError(t, err, "inspect container") t.Logf("Container state: status: %v", containerInfo.State.Status) require.True(t, containerInfo.State.Running, "container should be running") + ctx := testutil.Context(t, testutil.WaitLong) + ac, err := conn.ReconnectingPTY(ctx, uuid.New(), 80, 80, "", func(opts *workspacesdk.AgentReconnectingPTYInit) { opts.Container = container.ID }) @@ -2124,6 +2121,173 @@ func TestAgent_DevcontainerAutostart(t *testing.T) { require.NoError(t, err, "file should exist outside devcontainer") } +// TestAgent_DevcontainerRecreate tests that RecreateDevcontainer +// recreates a devcontainer and emits logs. +// +// This tests end-to-end functionality of auto-starting a devcontainer. +// It runs "devcontainer up" which creates a real Docker container. As +// such, it does not run by default in CI. +// +// You can run it manually as follows: +// +// CODER_TEST_USE_DOCKER=1 go test -count=1 ./agent -run TestAgent_DevcontainerRecreate +func TestAgent_DevcontainerRecreate(t *testing.T) { + if os.Getenv("CODER_TEST_USE_DOCKER") != "1" { + t.Skip("Set CODER_TEST_USE_DOCKER=1 to run this test") + } + t.Parallel() + + pool, err := dockertest.NewPool("") + require.NoError(t, err, "Could not connect to docker") + + // Prepare temporary devcontainer for test (mywork). + devcontainerID := uuid.New() + devcontainerLogSourceID := uuid.New() + workspaceFolder := filepath.Join(t.TempDir(), "mywork") + t.Logf("Workspace folder: %s", workspaceFolder) + devcontainerPath := filepath.Join(workspaceFolder, ".devcontainer") + err = os.MkdirAll(devcontainerPath, 0o755) + require.NoError(t, err, "create devcontainer directory") + devcontainerFile := filepath.Join(devcontainerPath, "devcontainer.json") + err = os.WriteFile(devcontainerFile, []byte(`{ + "name": "mywork", + "image": "busybox:latest", + "cmd": ["sleep", "infinity"] + }`), 0o600) + require.NoError(t, err, "write devcontainer.json") + + manifest := agentsdk.Manifest{ + // Set up pre-conditions for auto-starting a devcontainer, the + // script is used to extract the log source ID. + Devcontainers: []codersdk.WorkspaceAgentDevcontainer{ + { + ID: devcontainerID, + Name: "test", + WorkspaceFolder: workspaceFolder, + }, + }, + Scripts: []codersdk.WorkspaceAgentScript{ + { + ID: devcontainerID, + LogSourceID: devcontainerLogSourceID, + }, + }, + } + + //nolint:dogsled + conn, client, _, _, _ := setupAgent(t, manifest, 0, func(_ *agenttest.Client, o *agent.Options) { + o.ExperimentalDevcontainersEnabled = true + }) + + ctx := testutil.Context(t, testutil.WaitLong) + + // We enabled autostart for the devcontainer, so ready is a good + // indication that the devcontainer is up and running. Importantly, + // this also means that the devcontainer startup is no longer + // producing logs that may interfere with the recreate logs. + testutil.Eventually(ctx, t, func(context.Context) bool { + states := client.GetLifecycleStates() + return slices.Contains(states, codersdk.WorkspaceAgentLifecycleReady) + }, testutil.IntervalMedium, "devcontainer not ready") + + t.Logf("Looking for container with label: devcontainer.local_folder=%s", workspaceFolder) + + var container docker.APIContainers + testutil.Eventually(ctx, t, func(context.Context) bool { + containers, err := pool.Client.ListContainers(docker.ListContainersOptions{All: true}) + if err != nil { + t.Logf("Error listing containers: %v", err) + return false + } + for _, c := range containers { + t.Logf("Found container: %s with labels: %v", c.ID[:12], c.Labels) + if v, ok := c.Labels["devcontainer.local_folder"]; ok && v == workspaceFolder { + t.Logf("Found matching container: %s", c.ID[:12]) + container = c + return true + } + } + return false + }, testutil.IntervalMedium, "no container with workspace folder label found") + defer func(container docker.APIContainers) { + // We can't rely on pool here because the container is not + // managed by it (it is managed by @devcontainer/cli). + err := pool.Client.RemoveContainer(docker.RemoveContainerOptions{ + ID: container.ID, + RemoveVolumes: true, + Force: true, + }) + assert.Error(t, err, "container should be removed by recreate") + }(container) + + ctx = testutil.Context(t, testutil.WaitLong) // Reset context. + + // Capture logs via ScriptLogger. + logsCh := make(chan *proto.BatchCreateLogsRequest, 1) + client.SetLogsChannel(logsCh) + + // Invoke recreate to trigger the destruction and recreation of the + // devcontainer, we do it in a goroutine so we can process logs + // concurrently. + go func(container docker.APIContainers) { + err := conn.RecreateDevcontainer(ctx, container.ID) + assert.NoError(t, err, "recreate devcontainer should succeed") + }(container) + + t.Logf("Checking recreate logs for outcome...") + + // Wait for the logs to be emitted, the @devcontainer/cli up command + // will emit a log with the outcome at the end suggesting we did + // receive all the logs. +waitForOutcomeLoop: + for { + batch := testutil.RequireReceive(ctx, t, logsCh) + + if bytes.Equal(batch.LogSourceId, devcontainerLogSourceID[:]) { + for _, log := range batch.Logs { + t.Logf("Received log: %s", log.Output) + if strings.Contains(log.Output, "\"outcome\"") { + break waitForOutcomeLoop + } + } + } + } + + t.Logf("Checking there's a new container with label: devcontainer.local_folder=%s", workspaceFolder) + + // Make sure the container exists and isn't the same as the old one. + testutil.Eventually(ctx, t, func(context.Context) bool { + containers, err := pool.Client.ListContainers(docker.ListContainersOptions{All: true}) + if err != nil { + t.Logf("Error listing containers: %v", err) + return false + } + for _, c := range containers { + t.Logf("Found container: %s with labels: %v", c.ID[:12], c.Labels) + if v, ok := c.Labels["devcontainer.local_folder"]; ok && v == workspaceFolder { + if c.ID == container.ID { + t.Logf("Found same container: %s", c.ID[:12]) + return false + } + t.Logf("Found new container: %s", c.ID[:12]) + container = c + return true + } + } + return false + }, testutil.IntervalMedium, "new devcontainer not found") + defer func(container docker.APIContainers) { + // We can't rely on pool here because the container is not + // managed by it (it is managed by @devcontainer/cli). + err := pool.Client.RemoveContainer(docker.RemoveContainerOptions{ + ID: container.ID, + RemoveVolumes: true, + Force: true, + }) + assert.NoError(t, err, "remove container") + }(container) +} + func TestAgent_Dial(t *testing.T) { t.Parallel() diff --git a/agent/agentcontainers/api.go b/agent/agentcontainers/api.go index 9ecf70a4681a1..c3393c3fdec9e 100644 --- a/agent/agentcontainers/api.go +++ b/agent/agentcontainers/api.go @@ -20,6 +20,7 @@ import ( "github.com/coder/coder/v2/agent/agentexec" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/quartz" ) @@ -43,6 +44,7 @@ type API struct { cl Lister dccli DevcontainerCLI clock quartz.Clock + scriptLogger func(logSourceID uuid.UUID) ScriptLogger // lockCh protects the below fields. We use a channel instead of a // mutex so we can handle cancellation properly. @@ -52,6 +54,8 @@ type API struct { devcontainerNames map[string]struct{} // Track devcontainer names to avoid duplicates. knownDevcontainers []codersdk.WorkspaceAgentDevcontainer // Track predefined and runtime-detected devcontainers. configFileModifiedTimes map[string]time.Time // Track when config files were last modified. + + devcontainerLogSourceIDs map[string]uuid.UUID // Track devcontainer log source IDs. } // Option is a functional option for API. @@ -91,13 +95,30 @@ func WithDevcontainerCLI(dccli DevcontainerCLI) Option { // WithDevcontainers sets the known devcontainers for the API. This // allows the API to be aware of devcontainers defined in the workspace // agent manifest. -func WithDevcontainers(devcontainers []codersdk.WorkspaceAgentDevcontainer) Option { +func WithDevcontainers(devcontainers []codersdk.WorkspaceAgentDevcontainer, scripts []codersdk.WorkspaceAgentScript) Option { return func(api *API) { - if len(devcontainers) > 0 { - api.knownDevcontainers = slices.Clone(devcontainers) - api.devcontainerNames = make(map[string]struct{}, len(devcontainers)) - for _, devcontainer := range devcontainers { - api.devcontainerNames[devcontainer.Name] = struct{}{} + if len(devcontainers) == 0 { + return + } + api.knownDevcontainers = slices.Clone(devcontainers) + api.devcontainerNames = make(map[string]struct{}, len(devcontainers)) + api.devcontainerLogSourceIDs = make(map[string]uuid.UUID) + for _, devcontainer := range devcontainers { + api.devcontainerNames[devcontainer.Name] = struct{}{} + for _, script := range scripts { + // The devcontainer scripts match the devcontainer ID for + // identification. + if script.ID == devcontainer.ID { + api.devcontainerLogSourceIDs[devcontainer.WorkspaceFolder] = script.LogSourceID + break + } + } + if api.devcontainerLogSourceIDs[devcontainer.WorkspaceFolder] == uuid.Nil { + api.logger.Error(api.ctx, "devcontainer log source ID not found for devcontainer", + slog.F("devcontainer", devcontainer.Name), + slog.F("workspace_folder", devcontainer.WorkspaceFolder), + slog.F("config_path", devcontainer.ConfigPath), + ) } } } @@ -112,6 +133,27 @@ func WithWatcher(w watcher.Watcher) Option { } } +// ScriptLogger is an interface for sending devcontainer logs to the +// controlplane. +type ScriptLogger interface { + Send(ctx context.Context, log ...agentsdk.Log) error + Flush(ctx context.Context) error +} + +// noopScriptLogger is a no-op implementation of the ScriptLogger +// interface. +type noopScriptLogger struct{} + +func (noopScriptLogger) Send(context.Context, ...agentsdk.Log) error { return nil } +func (noopScriptLogger) Flush(context.Context) error { return nil } + +// WithScriptLogger sets the script logger provider for devcontainer operations. +func WithScriptLogger(scriptLogger func(logSourceID uuid.UUID) ScriptLogger) Option { + return func(api *API) { + api.scriptLogger = scriptLogger + } +} + // NewAPI returns a new API with the given options applied. func NewAPI(logger slog.Logger, options ...Option) *API { ctx, cancel := context.WithCancel(context.Background()) @@ -127,7 +169,10 @@ func NewAPI(logger slog.Logger, options ...Option) *API { devcontainerNames: make(map[string]struct{}), knownDevcontainers: []codersdk.WorkspaceAgentDevcontainer{}, configFileModifiedTimes: make(map[string]time.Time), + scriptLogger: func(uuid.UUID) ScriptLogger { return noopScriptLogger{} }, } + // The ctx and logger must be set before applying options to avoid + // nil pointer dereference. for _, opt := range options { opt(api) } @@ -426,7 +471,26 @@ func (api *API) handleDevcontainerRecreate(w http.ResponseWriter, r *http.Reques return } - _, err = api.dccli.Up(ctx, workspaceFolder, configPath, WithRemoveExistingContainer()) + // Send logs via agent logging facilities. + logSourceID := api.devcontainerLogSourceIDs[workspaceFolder] + if logSourceID == uuid.Nil { + // Fallback to the external log source ID if not found. + logSourceID = agentsdk.ExternalLogSourceID + } + scriptLogger := api.scriptLogger(logSourceID) + defer func() { + flushCtx, cancel := context.WithTimeout(api.ctx, 5*time.Second) + defer cancel() + if err := scriptLogger.Flush(flushCtx); err != nil { + api.logger.Error(flushCtx, "flush devcontainer logs failed", slog.Error(err)) + } + }() + infoW := agentsdk.LogsWriter(ctx, scriptLogger.Send, logSourceID, codersdk.LogLevelInfo) + defer infoW.Close() + errW := agentsdk.LogsWriter(ctx, scriptLogger.Send, logSourceID, codersdk.LogLevelError) + defer errW.Close() + + _, err = api.dccli.Up(ctx, workspaceFolder, configPath, WithOutput(infoW, errW), WithRemoveExistingContainer()) if err != nil { httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ Message: "Could not recreate devcontainer", diff --git a/agent/agentcontainers/api_test.go b/agent/agentcontainers/api_test.go index d6cac1cd2a97e..2c602de5cff3a 100644 --- a/agent/agentcontainers/api_test.go +++ b/agent/agentcontainers/api_test.go @@ -563,8 +563,17 @@ func TestAPI(t *testing.T) { agentcontainers.WithWatcher(watcher.NewNoop()), } + // Generate matching scripts for the known devcontainers + // (required to extract log source ID). + var scripts []codersdk.WorkspaceAgentScript + for i := range tt.knownDevcontainers { + scripts = append(scripts, codersdk.WorkspaceAgentScript{ + ID: tt.knownDevcontainers[i].ID, + LogSourceID: uuid.New(), + }) + } if len(tt.knownDevcontainers) > 0 { - apiOptions = append(apiOptions, agentcontainers.WithDevcontainers(tt.knownDevcontainers)) + apiOptions = append(apiOptions, agentcontainers.WithDevcontainers(tt.knownDevcontainers, scripts)) } api := agentcontainers.NewAPI(logger, apiOptions...) diff --git a/agent/agentcontainers/devcontainercli.go b/agent/agentcontainers/devcontainercli.go index d6060f862cb40..7e3122b182fdb 100644 --- a/agent/agentcontainers/devcontainercli.go +++ b/agent/agentcontainers/devcontainercli.go @@ -31,8 +31,18 @@ func WithRemoveExistingContainer() DevcontainerCLIUpOptions { } } +// WithOutput sets stdout and stderr writers for Up command logs. +func WithOutput(stdout, stderr io.Writer) DevcontainerCLIUpOptions { + return func(o *devcontainerCLIUpConfig) { + o.stdout = stdout + o.stderr = stderr + } +} + type devcontainerCLIUpConfig struct { removeExistingContainer bool + stdout io.Writer + stderr io.Writer } func applyDevcontainerCLIUpOptions(opts []DevcontainerCLIUpOptions) devcontainerCLIUpConfig { @@ -78,18 +88,28 @@ func (d *devcontainerCLI) Up(ctx context.Context, workspaceFolder, configPath st } cmd := d.execer.CommandContext(ctx, "devcontainer", args...) - var stdout bytes.Buffer - cmd.Stdout = io.MultiWriter(&stdout, &devcontainerCLILogWriter{ctx: ctx, logger: logger.With(slog.F("stdout", true))}) - cmd.Stderr = &devcontainerCLILogWriter{ctx: ctx, logger: logger.With(slog.F("stderr", true))} + // Capture stdout for parsing and stream logs for both default and provided writers. + var stdoutBuf bytes.Buffer + stdoutWriters := []io.Writer{&stdoutBuf, &devcontainerCLILogWriter{ctx: ctx, logger: logger.With(slog.F("stdout", true))}} + if conf.stdout != nil { + stdoutWriters = append(stdoutWriters, conf.stdout) + } + cmd.Stdout = io.MultiWriter(stdoutWriters...) + // Stream stderr logs and provided writer if any. + stderrWriters := []io.Writer{&devcontainerCLILogWriter{ctx: ctx, logger: logger.With(slog.F("stderr", true))}} + if conf.stderr != nil { + stderrWriters = append(stderrWriters, conf.stderr) + } + cmd.Stderr = io.MultiWriter(stderrWriters...) if err := cmd.Run(); err != nil { - if _, err2 := parseDevcontainerCLILastLine(ctx, logger, stdout.Bytes()); err2 != nil { + if _, err2 := parseDevcontainerCLILastLine(ctx, logger, stdoutBuf.Bytes()); err2 != nil { err = errors.Join(err, err2) } return "", err } - result, err := parseDevcontainerCLILastLine(ctx, logger, stdout.Bytes()) + result, err := parseDevcontainerCLILastLine(ctx, logger, stdoutBuf.Bytes()) if err != nil { return "", err } diff --git a/agent/agentcontainers/devcontainercli_test.go b/agent/agentcontainers/devcontainercli_test.go index d768b997cc1e1..cdba0211ab94e 100644 --- a/agent/agentcontainers/devcontainercli_test.go +++ b/agent/agentcontainers/devcontainercli_test.go @@ -128,6 +128,45 @@ func TestDevcontainerCLI_ArgsAndParsing(t *testing.T) { }) } +// TestDevcontainerCLI_WithOutput tests that WithOutput captures CLI +// logs to provided writers. +func TestDevcontainerCLI_WithOutput(t *testing.T) { + t.Parallel() + + // Prepare test executable and logger. + testExePath, err := os.Executable() + require.NoError(t, err, "get test executable path") + logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug) + ctx := testutil.Context(t, testutil.WaitMedium) + + // Buffers to capture stdout and stderr. + outBuf := &bytes.Buffer{} + errBuf := &bytes.Buffer{} + + // Simulate CLI execution with a standard up.log file. + wantArgs := "up --log-format json --workspace-folder /test/workspace" + testExecer := &testDevcontainerExecer{ + testExePath: testExePath, + wantArgs: wantArgs, + wantError: false, + logFile: filepath.Join("testdata", "devcontainercli", "parse", "up.log"), + } + dccli := agentcontainers.NewDevcontainerCLI(logger, testExecer) + + // Call Up with WithOutput to capture CLI logs. + containerID, err := dccli.Up(ctx, "/test/workspace", "", agentcontainers.WithOutput(outBuf, errBuf)) + require.NoError(t, err, "Up should succeed") + require.NotEmpty(t, containerID, "expected non-empty container ID") + + // Read expected log content. + expLog, err := os.ReadFile(filepath.Join("testdata", "devcontainercli", "parse", "up.log")) + require.NoError(t, err, "reading expected log file") + + // Verify stdout buffer contains the CLI logs and stderr is empty. + assert.Equal(t, string(expLog), outBuf.String(), "stdout buffer should match CLI logs") + assert.Empty(t, errBuf.String(), "stderr buffer should be empty on success") +} + // testDevcontainerExecer implements the agentexec.Execer interface for testing. type testDevcontainerExecer struct { testExePath string diff --git a/agent/api.go b/agent/api.go index f09d39b172bd5..2e15530adc608 100644 --- a/agent/api.go +++ b/agent/api.go @@ -7,6 +7,8 @@ import ( "github.com/go-chi/chi/v5" + "github.com/google/uuid" + "github.com/coder/coder/v2/agent/agentcontainers" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/codersdk" @@ -40,12 +42,15 @@ func (a *agent) apiHandler() (http.Handler, func() error) { if a.experimentalDevcontainersEnabled { containerAPIOpts := []agentcontainers.Option{ agentcontainers.WithExecer(a.execer), + agentcontainers.WithScriptLogger(func(logSourceID uuid.UUID) agentcontainers.ScriptLogger { + return a.logSender.GetScriptLogger(logSourceID) + }), } manifest := a.manifest.Load() if manifest != nil && len(manifest.Devcontainers) > 0 { containerAPIOpts = append( containerAPIOpts, - agentcontainers.WithDevcontainers(manifest.Devcontainers), + agentcontainers.WithDevcontainers(manifest.Devcontainers, manifest.Scripts), ) } diff --git a/codersdk/workspacesdk/agentconn.go b/codersdk/workspacesdk/agentconn.go index 97b4268c68780..f3c68d38b5575 100644 --- a/codersdk/workspacesdk/agentconn.go +++ b/codersdk/workspacesdk/agentconn.go @@ -387,6 +387,22 @@ func (c *AgentConn) ListContainers(ctx context.Context) (codersdk.WorkspaceAgent return resp, json.NewDecoder(res.Body).Decode(&resp) } +// RecreateDevcontainer recreates a devcontainer with the given container. +// This is a blocking call and will wait for the container to be recreated. +func (c *AgentConn) RecreateDevcontainer(ctx context.Context, containerIDOrName string) error { + ctx, span := tracing.StartSpan(ctx) + defer span.End() + res, err := c.apiRequest(ctx, http.MethodPost, "/api/v0/containers/devcontainers/container/"+containerIDOrName+"/recreate", nil) + if err != nil { + return xerrors.Errorf("do request: %w", err) + } + defer res.Body.Close() + if res.StatusCode != http.StatusNoContent { + return codersdk.ReadBodyAsError(res) + } + return nil +} + // 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) From c42a3156cc9bd2c44f8429a75961d7dd6137f1e2 Mon Sep 17 00:00:00 2001 From: Edward Angert Date: Thu, 15 May 2025 09:18:21 -0400 Subject: [PATCH 56/88] docs: add dev containers to manifest.json (#17854) [preview](http://coder.com/docs/@dev-container-manifest/admin/templates/extending-templates/devcontainers) --- docs/manifest.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/manifest.json b/docs/manifest.json index adbac7d4250dc..d9a23bbc0efaa 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -503,6 +503,11 @@ "description": "Authenticate with provider APIs to provision workspaces", "path": "./admin/templates/extending-templates/provider-authentication.md" }, + { + "title": "Configure a template for dev containers", + "description": "How to use configure your template for dev containers", + "path": "./admin/templates/extending-templates/devcontainers.md" + }, { "title": "Process Logging", "description": "Log workspace processes", From ee2aeb44d7b1489a1f9f6feb0f9bb8e00bc42d8e Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 15 May 2025 11:13:09 -0300 Subject: [PATCH 57/88] fix: avoid pulling containers when it is not enabled (#17855) We've been continuously pulling the containers endpoint even when the agent does not support containers. To optimize the requests, we can check if it is throwing an error and stop if it is a 403 status code. --- site/src/api/api.ts | 20 +++++--------------- site/src/modules/resources/AgentRow.tsx | 8 +++++++- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 85a9860bc57c5..206e6e5f466f2 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -2453,21 +2453,11 @@ class ApiMethods { const params = new URLSearchParams( labels?.map((label) => ["label", label]), ); - - try { - const res = - await this.axios.get( - `/api/v2/workspaceagents/${agentId}/containers?${params.toString()}`, - ); - return res.data; - } catch (err) { - // If the error is a 403, it means that experimental - // containers are not enabled on the agent. - if (isAxiosError(err) && err.response?.status === 403) { - return { containers: [] }; - } - throw err; - } + const res = + await this.axios.get( + `/api/v2/workspaceagents/${agentId}/containers?${params.toString()}`, + ); + return res.data; }; getInboxNotifications = async (startingBeforeId?: string) => { diff --git a/site/src/modules/resources/AgentRow.tsx b/site/src/modules/resources/AgentRow.tsx index c4d104501fd67..bc0751c332942 100644 --- a/site/src/modules/resources/AgentRow.tsx +++ b/site/src/modules/resources/AgentRow.tsx @@ -10,6 +10,7 @@ import type { WorkspaceAgent, WorkspaceAgentMetadata, } from "api/typesGenerated"; +import { isAxiosError } from "axios"; import { DropdownArrow } from "components/DropdownArrow/DropdownArrow"; import type { Line } from "components/Logs/LogLine"; import { Stack } from "components/Stack/Stack"; @@ -160,7 +161,12 @@ export const AgentRow: FC = ({ select: (res) => res.containers.filter((c) => c.status === "running"), // TODO: Implement a websocket connection to get updates on containers // without having to poll. - refetchInterval: 10_000, + refetchInterval: (_, query) => { + const { error } = query.state; + return isAxiosError(error) && error.response?.status === 403 + ? false + : 10_000; + }, }); return ( From 1bacd82e80780134c4a90414d6151b32e797fd87 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Thu, 15 May 2025 15:32:52 +0100 Subject: [PATCH 58/88] feat: add API key scope to restrict access to user data (#17692) --- coderd/coderd.go | 10 +- coderd/database/dbauthz/dbauthz_test.go | 5 +- coderd/database/dbgen/dbgen.go | 1 + coderd/database/dbmem/dbmem.go | 1 + coderd/database/dump.sql | 8 + ...api_key_scope_to_workspace_agents.down.sql | 6 + ...d_api_key_scope_to_workspace_agents.up.sql | 10 + coderd/database/models.go | 60 ++ coderd/database/queries.sql.go | 29 +- coderd/database/queries/workspaceagents.sql | 5 +- coderd/externalauth_test.go | 78 ++ coderd/gitsshkey.go | 4 + coderd/gitsshkey_test.go | 50 ++ coderd/httpmw/workspaceagent.go | 18 +- .../provisionerdserver/provisionerdserver.go | 6 + coderd/rbac/authz_internal_test.go | 58 ++ coderd/rbac/scopes.go | 36 +- coderd/util/tz/tz_darwin.go | 2 +- coderd/workspaceagents.go | 9 + docs/admin/security/audit-logs.md | 2 +- enterprise/audit/table.go | 1 + provisioner/echo/serve.go | 23 + provisioner/terraform/resources.go | 4 +- provisionerd/proto/version.go | 1 + provisionersdk/proto/provisioner.pb.go | 836 +++++++++--------- provisionersdk/proto/provisioner.proto | 1 + site/e2e/helpers.ts | 1 + site/e2e/provisionerGenerated.ts | 4 + 28 files changed, 823 insertions(+), 446 deletions(-) create mode 100644 coderd/database/migrations/000326_add_api_key_scope_to_workspace_agents.down.sql create mode 100644 coderd/database/migrations/000326_add_api_key_scope_to_workspace_agents.up.sql diff --git a/coderd/coderd.go b/coderd/coderd.go index a12a5624b931c..cd8253792c354 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -802,6 +802,11 @@ func New(options *Options) *API { PostAuthAdditionalHeadersFunc: options.PostAuthAdditionalHeadersFunc, }) + workspaceAgentInfo := httpmw.ExtractWorkspaceAgentAndLatestBuild(httpmw.ExtractWorkspaceAgentAndLatestBuildConfig{ + DB: options.Database, + Optional: false, + }) + // API rate limit middleware. The counter is local and not shared between // replicas or instances of this middleware. apiRateLimiter := httpmw.RateLimit(options.APIRateLimit, time.Minute) @@ -1289,10 +1294,7 @@ func New(options *Options) *API { httpmw.RequireAPIKeyOrWorkspaceProxyAuth(), ).Get("/connection", api.workspaceAgentConnectionGeneric) r.Route("/me", func(r chi.Router) { - r.Use(httpmw.ExtractWorkspaceAgentAndLatestBuild(httpmw.ExtractWorkspaceAgentAndLatestBuildConfig{ - DB: options.Database, - Optional: false, - })) + r.Use(workspaceAgentInfo) r.Get("/rpc", api.workspaceAgentRPC) r.Patch("/logs", api.patchWorkspaceAgentLogs) r.Patch("/app-status", api.patchWorkspaceAgentAppStatus) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 9936208ae04c1..e152960a26ef7 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -4018,8 +4018,9 @@ 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(), - Name: "dev", + ID: uuid.New(), + Name: "dev", + APIKeyScope: database.AgentKeyScopeEnumAll, }).Asserts(rbac.ResourceSystem, policy.ActionCreate) })) s.Run("InsertWorkspaceApp", s.Subtest(func(db database.Store, check *expects) { diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index 8a345fa0fd6e7..5069ce0cbe0ad 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -212,6 +212,7 @@ func WorkspaceAgent(t testing.TB, db database.Store, orig database.WorkspaceAgen MOTDFile: takeFirst(orig.TroubleshootingURL, ""), DisplayApps: append([]database.DisplayApp{}, orig.DisplayApps...), DisplayOrder: takeFirst(orig.DisplayOrder, 1), + APIKeyScope: takeFirst(orig.APIKeyScope, database.AgentKeyScopeEnumAll), }) require.NoError(t, err, "insert workspace agent") return agt diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 63693604ae262..ac9b601bee983 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -9620,6 +9620,7 @@ func (q *FakeQuerier) InsertWorkspaceAgent(_ context.Context, arg database.Inser LifecycleState: database.WorkspaceAgentLifecycleStateCreated, DisplayApps: arg.DisplayApps, DisplayOrder: arg.DisplayOrder, + APIKeyScope: arg.APIKeyScope, } q.workspaceAgents = append(q.workspaceAgents, agent) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index f56b417dbe4d4..0b1356ede11cc 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -5,6 +5,11 @@ CREATE TYPE agent_id_name_pair AS ( name text ); +CREATE TYPE agent_key_scope_enum AS ENUM ( + 'all', + 'no_user_data' +); + CREATE TYPE api_key_scope AS ENUM ( 'all', 'application_connect' @@ -1837,6 +1842,7 @@ CREATE TABLE workspace_agents ( api_version text DEFAULT ''::text NOT NULL, display_order integer DEFAULT 0 NOT NULL, parent_id uuid, + api_key_scope agent_key_scope_enum DEFAULT 'all'::agent_key_scope_enum NOT NULL, CONSTRAINT max_logs_length CHECK ((logs_length <= 1048576)), CONSTRAINT subsystems_not_none CHECK ((NOT ('none'::workspace_agent_subsystem = ANY (subsystems)))) ); @@ -1863,6 +1869,8 @@ COMMENT ON COLUMN workspace_agents.ready_at IS 'The time the agent entered the r COMMENT ON COLUMN workspace_agents.display_order IS 'Specifies the order in which to display agents in user interfaces.'; +COMMENT ON COLUMN workspace_agents.api_key_scope IS 'Defines the scope of the API key associated with the agent. ''all'' allows access to everything, ''no_user_data'' restricts it to exclude user data.'; + CREATE UNLOGGED TABLE workspace_app_audit_sessions ( agent_id uuid NOT NULL, app_id uuid NOT NULL, diff --git a/coderd/database/migrations/000326_add_api_key_scope_to_workspace_agents.down.sql b/coderd/database/migrations/000326_add_api_key_scope_to_workspace_agents.down.sql new file mode 100644 index 0000000000000..48477606d80b1 --- /dev/null +++ b/coderd/database/migrations/000326_add_api_key_scope_to_workspace_agents.down.sql @@ -0,0 +1,6 @@ +-- Remove the api_key_scope column from the workspace_agents table +ALTER TABLE workspace_agents +DROP COLUMN IF EXISTS api_key_scope; + +-- Drop the enum type for API key scope +DROP TYPE IF EXISTS agent_key_scope_enum; diff --git a/coderd/database/migrations/000326_add_api_key_scope_to_workspace_agents.up.sql b/coderd/database/migrations/000326_add_api_key_scope_to_workspace_agents.up.sql new file mode 100644 index 0000000000000..ee0581fcdb145 --- /dev/null +++ b/coderd/database/migrations/000326_add_api_key_scope_to_workspace_agents.up.sql @@ -0,0 +1,10 @@ +-- Create the enum type for API key scope +CREATE TYPE agent_key_scope_enum AS ENUM ('all', 'no_user_data'); + +-- Add the api_key_scope column to the workspace_agents table +-- It defaults to 'all' to maintain existing behavior for current agents. +ALTER TABLE workspace_agents +ADD COLUMN api_key_scope agent_key_scope_enum NOT NULL DEFAULT 'all'; + +-- Add a comment explaining the purpose of the column +COMMENT ON COLUMN workspace_agents.api_key_scope IS 'Defines the scope of the API key associated with the agent. ''all'' allows access to everything, ''no_user_data'' restricts it to exclude user data.'; diff --git a/coderd/database/models.go b/coderd/database/models.go index 1b6ea7591d652..3674af6ed6981 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -74,6 +74,64 @@ func AllAPIKeyScopeValues() []APIKeyScope { } } +type AgentKeyScopeEnum string + +const ( + AgentKeyScopeEnumAll AgentKeyScopeEnum = "all" + AgentKeyScopeEnumNoUserData AgentKeyScopeEnum = "no_user_data" +) + +func (e *AgentKeyScopeEnum) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = AgentKeyScopeEnum(s) + case string: + *e = AgentKeyScopeEnum(s) + default: + return fmt.Errorf("unsupported scan type for AgentKeyScopeEnum: %T", src) + } + return nil +} + +type NullAgentKeyScopeEnum struct { + AgentKeyScopeEnum AgentKeyScopeEnum `json:"agent_key_scope_enum"` + Valid bool `json:"valid"` // Valid is true if AgentKeyScopeEnum is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullAgentKeyScopeEnum) Scan(value interface{}) error { + if value == nil { + ns.AgentKeyScopeEnum, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.AgentKeyScopeEnum.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullAgentKeyScopeEnum) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.AgentKeyScopeEnum), nil +} + +func (e AgentKeyScopeEnum) Valid() bool { + switch e { + case AgentKeyScopeEnumAll, + AgentKeyScopeEnumNoUserData: + return true + } + return false +} + +func AllAgentKeyScopeEnumValues() []AgentKeyScopeEnum { + return []AgentKeyScopeEnum{ + AgentKeyScopeEnumAll, + AgentKeyScopeEnumNoUserData, + } +} + type AppSharingLevel string const ( @@ -3406,6 +3464,8 @@ type WorkspaceAgent struct { // Specifies the order in which to display agents in user interfaces. DisplayOrder int32 `db:"display_order" json:"display_order"` ParentID uuid.NullUUID `db:"parent_id" json:"parent_id"` + // Defines the scope of the API key associated with the agent. 'all' allows access to everything, 'no_user_data' restricts it to exclude user data. + APIKeyScope AgentKeyScopeEnum `db:"api_key_scope" json:"api_key_scope"` } // Workspace agent devcontainer configuration diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index af20e4b4403ff..a273b937324f0 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -13939,7 +13939,7 @@ func (q *sqlQuerier) DeleteOldWorkspaceAgentLogs(ctx context.Context, threshold const getWorkspaceAgentAndLatestBuildByAuthToken = `-- name: GetWorkspaceAgentAndLatestBuildByAuthToken :one 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_agents.parent_id, + 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_agents.parent_id, workspace_agents.api_key_scope, 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 @@ -14030,6 +14030,7 @@ func (q *sqlQuerier) GetWorkspaceAgentAndLatestBuildByAuthToken(ctx context.Cont &i.WorkspaceAgent.APIVersion, &i.WorkspaceAgent.DisplayOrder, &i.WorkspaceAgent.ParentID, + &i.WorkspaceAgent.APIKeyScope, &i.WorkspaceBuild.ID, &i.WorkspaceBuild.CreatedAt, &i.WorkspaceBuild.UpdatedAt, @@ -14053,7 +14054,7 @@ func (q *sqlQuerier) GetWorkspaceAgentAndLatestBuildByAuthToken(ctx context.Cont const getWorkspaceAgentByID = `-- name: GetWorkspaceAgentByID :one SELECT - id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id + id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope FROM workspace_agents WHERE @@ -14096,13 +14097,14 @@ func (q *sqlQuerier) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (W &i.APIVersion, &i.DisplayOrder, &i.ParentID, + &i.APIKeyScope, ) return i, err } const getWorkspaceAgentByInstanceID = `-- name: GetWorkspaceAgentByInstanceID :one SELECT - id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id + id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope FROM workspace_agents WHERE @@ -14147,6 +14149,7 @@ func (q *sqlQuerier) GetWorkspaceAgentByInstanceID(ctx context.Context, authInst &i.APIVersion, &i.DisplayOrder, &i.ParentID, + &i.APIKeyScope, ) return i, err } @@ -14366,7 +14369,7 @@ func (q *sqlQuerier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Context const getWorkspaceAgentsByResourceIDs = `-- name: GetWorkspaceAgentsByResourceIDs :many SELECT - id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id + id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope FROM workspace_agents WHERE @@ -14415,6 +14418,7 @@ func (q *sqlQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids [] &i.APIVersion, &i.DisplayOrder, &i.ParentID, + &i.APIKeyScope, ); err != nil { return nil, err } @@ -14431,7 +14435,7 @@ func (q *sqlQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids [] const getWorkspaceAgentsByWorkspaceAndBuildNumber = `-- name: GetWorkspaceAgentsByWorkspaceAndBuildNumber :many SELECT - 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_agents.parent_id + 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_agents.parent_id, workspace_agents.api_key_scope FROM workspace_agents JOIN @@ -14490,6 +14494,7 @@ func (q *sqlQuerier) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Con &i.APIVersion, &i.DisplayOrder, &i.ParentID, + &i.APIKeyScope, ); err != nil { return nil, err } @@ -14505,7 +14510,7 @@ func (q *sqlQuerier) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Con } const getWorkspaceAgentsCreatedAfter = `-- name: GetWorkspaceAgentsCreatedAfter :many -SELECT id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id FROM workspace_agents WHERE created_at > $1 +SELECT id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope FROM workspace_agents WHERE created_at > $1 ` func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceAgent, error) { @@ -14550,6 +14555,7 @@ func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, created &i.APIVersion, &i.DisplayOrder, &i.ParentID, + &i.APIKeyScope, ); err != nil { return nil, err } @@ -14566,7 +14572,7 @@ func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, created const getWorkspaceAgentsInLatestBuildByWorkspaceID = `-- name: GetWorkspaceAgentsInLatestBuildByWorkspaceID :many SELECT - 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_agents.parent_id + 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_agents.parent_id, workspace_agents.api_key_scope FROM workspace_agents JOIN @@ -14627,6 +14633,7 @@ func (q *sqlQuerier) GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx context.Co &i.APIVersion, &i.DisplayOrder, &i.ParentID, + &i.APIKeyScope, ); err != nil { return nil, err } @@ -14662,10 +14669,11 @@ INSERT INTO troubleshooting_url, motd_file, display_apps, - display_order + display_order, + api_key_scope ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19) RETURNING id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20) RETURNING id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope ` type InsertWorkspaceAgentParams struct { @@ -14688,6 +14696,7 @@ type InsertWorkspaceAgentParams struct { MOTDFile string `db:"motd_file" json:"motd_file"` DisplayApps []DisplayApp `db:"display_apps" json:"display_apps"` DisplayOrder int32 `db:"display_order" json:"display_order"` + APIKeyScope AgentKeyScopeEnum `db:"api_key_scope" json:"api_key_scope"` } func (q *sqlQuerier) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspaceAgentParams) (WorkspaceAgent, error) { @@ -14711,6 +14720,7 @@ func (q *sqlQuerier) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspa arg.MOTDFile, pq.Array(arg.DisplayApps), arg.DisplayOrder, + arg.APIKeyScope, ) var i WorkspaceAgent err := row.Scan( @@ -14746,6 +14756,7 @@ func (q *sqlQuerier) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspa &i.APIVersion, &i.DisplayOrder, &i.ParentID, + &i.APIKeyScope, ) return i, err } diff --git a/coderd/database/queries/workspaceagents.sql b/coderd/database/queries/workspaceagents.sql index cb4fa3f8cf968..5965f0cb16fbf 100644 --- a/coderd/database/queries/workspaceagents.sql +++ b/coderd/database/queries/workspaceagents.sql @@ -48,10 +48,11 @@ INSERT INTO troubleshooting_url, motd_file, display_apps, - display_order + display_order, + api_key_scope ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19) RETURNING *; + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20) RETURNING *; -- name: UpdateWorkspaceAgentConnectionByID :exec UPDATE diff --git a/coderd/externalauth_test.go b/coderd/externalauth_test.go index 87197528fc087..c9ba4911214de 100644 --- a/coderd/externalauth_test.go +++ b/coderd/externalauth_test.go @@ -706,4 +706,82 @@ func TestExternalAuthCallback(t *testing.T) { }) require.NoError(t, err) }) + t.Run("AgentAPIKeyScope", func(t *testing.T) { + t.Parallel() + + for _, tt := range []struct { + apiKeyScope string + expectsError bool + }{ + {apiKeyScope: "all", expectsError: false}, + {apiKeyScope: "no_user_data", expectsError: true}, + } { + t.Run(tt.apiKeyScope, func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, &coderdtest.Options{ + IncludeProvisionerDaemon: true, + ExternalAuthConfigs: []*externalauth.Config{{ + InstrumentedOAuth2Config: &testutil.OAuth2Config{}, + ID: "github", + Regex: regexp.MustCompile(`github\.com`), + Type: codersdk.EnhancedExternalAuthProviderGitHub.String(), + }}, + }) + user := coderdtest.CreateFirstUser(t, client) + authToken := uuid.NewString() + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionPlan: echo.PlanComplete, + ProvisionApply: echo.ProvisionApplyWithAgentAndAPIKeyScope(authToken, tt.apiKeyScope), + }) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, template.ID) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) + + agentClient := agentsdk.New(client.URL) + agentClient.SetSessionToken(authToken) + + token, err := agentClient.ExternalAuth(t.Context(), agentsdk.ExternalAuthRequest{ + Match: "github.com/asd/asd", + }) + + if tt.expectsError { + require.Error(t, err) + var sdkErr *codersdk.Error + require.ErrorAs(t, err, &sdkErr) + require.Equal(t, http.StatusForbidden, sdkErr.StatusCode()) + return + } + + require.NoError(t, err) + require.NotEmpty(t, token.URL) + + // Start waiting for the token callback... + tokenChan := make(chan agentsdk.ExternalAuthResponse, 1) + go func() { + token, err := agentClient.ExternalAuth(t.Context(), agentsdk.ExternalAuthRequest{ + Match: "github.com/asd/asd", + Listen: true, + }) + assert.NoError(t, err) + tokenChan <- token + }() + + time.Sleep(250 * time.Millisecond) + + resp := coderdtest.RequestExternalAuthCallback(t, "github", client) + require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) + + token = <-tokenChan + require.Equal(t, "access_token", token.Username) + + token, err = agentClient.ExternalAuth(t.Context(), agentsdk.ExternalAuthRequest{ + Match: "github.com/asd/asd", + }) + require.NoError(t, err) + }) + } + }) } diff --git a/coderd/gitsshkey.go b/coderd/gitsshkey.go index 110c16c7409d2..b9724689c5a7b 100644 --- a/coderd/gitsshkey.go +++ b/coderd/gitsshkey.go @@ -145,6 +145,10 @@ func (api *API) agentGitSSHKey(rw http.ResponseWriter, r *http.Request) { } gitSSHKey, err := api.Database.GetGitSSHKey(ctx, workspace.OwnerID) + if httpapi.IsUnauthorizedError(err) { + httpapi.Forbidden(rw) + return + } if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error fetching git SSH key.", diff --git a/coderd/gitsshkey_test.go b/coderd/gitsshkey_test.go index 22d23176aa1c8..abd18508ce018 100644 --- a/coderd/gitsshkey_test.go +++ b/coderd/gitsshkey_test.go @@ -2,6 +2,7 @@ package coderd_test import ( "context" + "net/http" "testing" "github.com/google/uuid" @@ -12,6 +13,7 @@ import ( "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/gitsshkey" + "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/coder/v2/provisioner/echo" "github.com/coder/coder/v2/testutil" @@ -126,3 +128,51 @@ func TestAgentGitSSHKey(t *testing.T) { require.NoError(t, err) require.NotEmpty(t, agentKey.PrivateKey) } + +func TestAgentGitSSHKey_APIKeyScopes(t *testing.T) { + t.Parallel() + + for _, tt := range []struct { + apiKeyScope string + expectError bool + }{ + {apiKeyScope: "all", expectError: false}, + {apiKeyScope: "no_user_data", expectError: true}, + } { + t.Run(tt.apiKeyScope, func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, &coderdtest.Options{ + IncludeProvisionerDaemon: true, + }) + user := coderdtest.CreateFirstUser(t, client) + authToken := uuid.NewString() + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionPlan: echo.PlanComplete, + ProvisionApply: echo.ProvisionApplyWithAgentAndAPIKeyScope(authToken, tt.apiKeyScope), + }) + project := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, project.ID) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) + + agentClient := agentsdk.New(client.URL) + agentClient.SetSessionToken(authToken) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + _, err := agentClient.GitSSHKey(ctx) + + if tt.expectError { + require.Error(t, err) + var sdkErr *codersdk.Error + require.ErrorAs(t, err, &sdkErr) + require.Equal(t, http.StatusForbidden, sdkErr.StatusCode()) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/coderd/httpmw/workspaceagent.go b/coderd/httpmw/workspaceagent.go index 241fa385681e6..0ee231b2f5a12 100644 --- a/coderd/httpmw/workspaceagent.go +++ b/coderd/httpmw/workspaceagent.go @@ -109,12 +109,18 @@ func ExtractWorkspaceAgentAndLatestBuild(opts ExtractWorkspaceAgentAndLatestBuil return } - subject, _, err := UserRBACSubject(ctx, opts.DB, row.WorkspaceTable.OwnerID, rbac.WorkspaceAgentScope(rbac.WorkspaceAgentScopeParams{ - WorkspaceID: row.WorkspaceTable.ID, - OwnerID: row.WorkspaceTable.OwnerID, - TemplateID: row.WorkspaceTable.TemplateID, - VersionID: row.WorkspaceBuild.TemplateVersionID, - })) + subject, _, err := UserRBACSubject( + ctx, + opts.DB, + row.WorkspaceTable.OwnerID, + rbac.WorkspaceAgentScope(rbac.WorkspaceAgentScopeParams{ + WorkspaceID: row.WorkspaceTable.ID, + OwnerID: row.WorkspaceTable.OwnerID, + TemplateID: row.WorkspaceTable.TemplateID, + VersionID: row.WorkspaceBuild.TemplateVersionID, + BlockUserData: row.WorkspaceAgent.APIKeyScope == database.AgentKeyScopeEnumNoUserData, + }), + ) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error with workspace agent authorization context.", diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 38924300ac7a2..423e9bbe584c6 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -2140,6 +2140,11 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. } } + apiKeyScope := database.AgentKeyScopeEnumAll + if prAgent.ApiKeyScope == string(database.AgentKeyScopeEnumNoUserData) { + apiKeyScope = database.AgentKeyScopeEnumNoUserData + } + agentID := uuid.New() dbAgent, err := db.InsertWorkspaceAgent(ctx, database.InsertWorkspaceAgentParams{ ID: agentID, @@ -2162,6 +2167,7 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. ResourceMetadata: pqtype.NullRawMessage{}, // #nosec G115 - Order represents a display order value that's always small and fits in int32 DisplayOrder: int32(prAgent.Order), + APIKeyScope: apiKeyScope, }) if err != nil { return xerrors.Errorf("insert agent: %w", err) diff --git a/coderd/rbac/authz_internal_test.go b/coderd/rbac/authz_internal_test.go index a9de3c56cb26a..9c09837c7915d 100644 --- a/coderd/rbac/authz_internal_test.go +++ b/coderd/rbac/authz_internal_test.go @@ -1053,6 +1053,64 @@ func TestAuthorizeScope(t *testing.T) { {resource: ResourceWorkspace.InOrg(unusedID).WithOwner("not-me"), actions: []policy.Action{policy.ActionCreate}, allow: false}, }, ) + + meID := uuid.New() + user = Subject{ + ID: meID.String(), + Roles: Roles{ + must(RoleByName(RoleMember())), + must(RoleByName(ScopedRoleOrgMember(defOrg))), + }, + Scope: must(ScopeNoUserData.Expand()), + } + + // Test 1: Verify that no_user_data scope prevents accessing user data + testAuthorize(t, "ReadPersonalUser", user, + cases(func(c authTestCase) authTestCase { + c.actions = ResourceUser.AvailableActions() + c.allow = false + c.resource.ID = meID.String() + return c + }, []authTestCase{ + {resource: ResourceUser.WithOwner(meID.String()).InOrg(defOrg).WithID(meID)}, + }), + ) + + // Test 2: Verify token can still perform regular member actions that don't involve user data + testAuthorize(t, "NoUserData_CanStillUseRegularPermissions", user, + // Test workspace access - should still work + cases(func(c authTestCase) authTestCase { + c.actions = []policy.Action{policy.ActionRead} + c.allow = true + return c + }, []authTestCase{ + // Can still read owned workspaces + {resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID)}, + }), + // Test workspace create - should still work + cases(func(c authTestCase) authTestCase { + c.actions = []policy.Action{policy.ActionCreate} + c.allow = true + return c + }, []authTestCase{ + // Can still create workspaces + {resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID)}, + }), + ) + + // Test 3: Verify token cannot perform actions outside of member role + testAuthorize(t, "NoUserData_CannotExceedMemberRole", user, + cases(func(c authTestCase) authTestCase { + c.actions = []policy.Action{policy.ActionRead, policy.ActionUpdate, policy.ActionDelete} + c.allow = false + return c + }, []authTestCase{ + // Cannot access other users' workspaces + {resource: ResourceWorkspace.InOrg(defOrg).WithOwner("other-user")}, + // Cannot access admin resources + {resource: ResourceOrganization.WithID(defOrg)}, + }), + ) } // cases applies a given function to all test cases. This makes generalities easier to create. diff --git a/coderd/rbac/scopes.go b/coderd/rbac/scopes.go index d6a95ccec1b35..4dd930699a053 100644 --- a/coderd/rbac/scopes.go +++ b/coderd/rbac/scopes.go @@ -11,10 +11,11 @@ import ( ) type WorkspaceAgentScopeParams struct { - WorkspaceID uuid.UUID - OwnerID uuid.UUID - TemplateID uuid.UUID - VersionID uuid.UUID + WorkspaceID uuid.UUID + OwnerID uuid.UUID + TemplateID uuid.UUID + VersionID uuid.UUID + BlockUserData bool } // WorkspaceAgentScope returns a scope that is the same as ScopeAll but can only @@ -25,16 +26,25 @@ func WorkspaceAgentScope(params WorkspaceAgentScopeParams) Scope { panic("all uuids must be non-nil, this is a developer error") } - allScope, err := ScopeAll.Expand() + var ( + scope Scope + err error + ) + if params.BlockUserData { + scope, err = ScopeNoUserData.Expand() + } else { + scope, err = ScopeAll.Expand() + } if err != nil { - panic("failed to expand scope all, this should never happen") + panic("failed to expand scope, this should never happen") } + return Scope{ // TODO: We want to limit the role too to be extra safe. // Even though the allowlist blocks anything else, it is still good // incase we change the behavior of the allowlist. The allowlist is new // and evolving. - Role: allScope.Role, + Role: scope.Role, // This prevents the agent from being able to access any other resource. // Include the list of IDs of anything that is required for the // agent to function. @@ -50,6 +60,7 @@ func WorkspaceAgentScope(params WorkspaceAgentScopeParams) Scope { const ( ScopeAll ScopeName = "all" ScopeApplicationConnect ScopeName = "application_connect" + ScopeNoUserData ScopeName = "no_user_data" ) // TODO: Support passing in scopeID list for allowlisting resources. @@ -81,6 +92,17 @@ var builtinScopes = map[ScopeName]Scope{ }, AllowIDList: []string{policy.WildcardSymbol}, }, + + ScopeNoUserData: { + Role: Role{ + Identifier: RoleIdentifier{Name: fmt.Sprintf("Scope_%s", ScopeNoUserData)}, + DisplayName: "Scope without access to user data", + Site: allPermsExcept(ResourceUser), + Org: map[string][]Permission{}, + User: []Permission{}, + }, + AllowIDList: []string{policy.WildcardSymbol}, + }, } type ExpandableScope interface { diff --git a/coderd/util/tz/tz_darwin.go b/coderd/util/tz/tz_darwin.go index 00250cb97b7a3..56c19037bd1d1 100644 --- a/coderd/util/tz/tz_darwin.go +++ b/coderd/util/tz/tz_darwin.go @@ -42,7 +42,7 @@ func TimezoneIANA() (*time.Location, error) { return nil, xerrors.Errorf("read location of %s: %w", zoneInfoPath, err) } - stripped := strings.Replace(lp, realZoneInfoPath, "", -1) + stripped := strings.ReplaceAll(lp, realZoneInfoPath, "") stripped = strings.TrimPrefix(stripped, string(filepath.Separator)) loc, err = time.LoadLocation(stripped) if err != nil { diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 5af9fc009b5aa..72a03580121af 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -1635,6 +1635,15 @@ func (api *API) workspaceAgentsExternalAuth(rw http.ResponseWriter, r *http.Requ return } + // Pre-check if the caller can read the external auth links for the owner of the + // workspace. Do this up front because a sql.ErrNoRows is expected if the user is + // in the flow of authenticating. If no row is present, the auth check is delayed + // until the user authenticates. It is preferred to reject early. + if !api.Authorize(r, policy.ActionReadPersonal, rbac.ResourceUserObject(workspace.OwnerID)) { + httpapi.Forbidden(rw) + return + } + var previousToken *database.ExternalAuthLink // handleRetrying will attempt to continually check for a new token // if listen is true. This is useful if an error is encountered in the diff --git a/docs/admin/security/audit-logs.md b/docs/admin/security/audit-logs.md index 2ab7d454ca71b..626f998f5954d 100644 --- a/docs/admin/security/audit-logs.md +++ b/docs/admin/security/audit-logs.md @@ -29,7 +29,7 @@ We track the following resources: | 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
is_systemtrue
last_seen_atfalse
login_typetrue
nametrue
one_time_passcode_expires_attrue
quiet_hours_scheduletrue
rbac_rolestrue
statustrue
updated_atfalse
usernametrue
| -| 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
parent_idfalse
ready_atfalse
resource_idfalse
resource_metadatafalse
started_atfalse
subsystemsfalse
troubleshooting_urlfalse
updated_atfalse
versionfalse
| +| WorkspaceAgent
connect, disconnect | |
FieldTracked
api_key_scopefalse
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
parent_idfalse
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
| diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index d5bf22df9ff5e..fa6e64cce51ab 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -343,6 +343,7 @@ var auditableResourcesTypes = map[any]map[string]Action{ "api_version": ActionIgnore, "display_order": ActionIgnore, "parent_id": ActionIgnore, + "api_key_scope": ActionIgnore, }, &database.WorkspaceApp{}: { "id": ActionIgnore, diff --git a/provisioner/echo/serve.go b/provisioner/echo/serve.go index 4cb1f50716e31..031af97317aca 100644 --- a/provisioner/echo/serve.go +++ b/provisioner/echo/serve.go @@ -19,6 +19,29 @@ import ( "github.com/coder/coder/v2/provisionersdk/proto" ) +// ProvisionApplyWithAgent returns provision responses that will mock a fake +// "aws_instance" resource with an agent that has the given auth token. +func ProvisionApplyWithAgentAndAPIKeyScope(authToken string, apiKeyScope string) []*proto.Response { + return []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ + Resources: []*proto.Resource{{ + Name: "example_with_scope", + Type: "aws_instance", + Agents: []*proto.Agent{{ + Id: uuid.NewString(), + Name: "example", + Auth: &proto.Agent_Token{ + Token: authToken, + }, + ApiKeyScope: apiKeyScope, + }}, + }}, + }, + }, + }} +} + // ProvisionApplyWithAgent returns provision responses that will mock a fake // "aws_instance" resource with an agent that has the given auth token. func ProvisionApplyWithAgent(authToken string) []*proto.Response { diff --git a/provisioner/terraform/resources.go b/provisioner/terraform/resources.go index d474c24289ef3..22f608c7a8597 100644 --- a/provisioner/terraform/resources.go +++ b/provisioner/terraform/resources.go @@ -42,6 +42,7 @@ type agentAttributes struct { Directory string `mapstructure:"dir"` ID string `mapstructure:"id"` Token string `mapstructure:"token"` + APIKeyScope string `mapstructure:"api_key_scope"` Env map[string]string `mapstructure:"env"` // Deprecated: but remains here for backwards compatibility. StartupScript string `mapstructure:"startup_script"` @@ -319,6 +320,7 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s Metadata: metadata, DisplayApps: displayApps, Order: attrs.Order, + ApiKeyScope: attrs.APIKeyScope, } // Support the legacy script attributes in the agent! if attrs.StartupScript != "" { @@ -394,7 +396,7 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s agents, exists := resourceAgents[agentResource.Label] if !exists { - agents = make([]*proto.Agent, 0) + agents = make([]*proto.Agent, 0, 1) } agents = append(agents, agent) resourceAgents[agentResource.Label] = agents diff --git a/provisionerd/proto/version.go b/provisionerd/proto/version.go index 58f4548404e7a..64ab779f2bfc4 100644 --- a/provisionerd/proto/version.go +++ b/provisionerd/proto/version.go @@ -21,6 +21,7 @@ import "github.com/coder/coder/v2/apiversion" // in the terraform provider. // - Add new field named `running_agent_auth_tokens` to provisioner job metadata // - Add new field named `resource_replacements` in PlanComplete & CompletedJob.WorkspaceBuild. +// - Add new field named `api_key_scope` to WorkspaceAgent to support running without user data access. const ( CurrentMajor = 1 CurrentMinor = 5 diff --git a/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go index 8f5260e902bc8..b29cbbfc2a9e5 100644 --- a/provisionersdk/proto/provisioner.pb.go +++ b/provisionersdk/proto/provisioner.pb.go @@ -1278,6 +1278,7 @@ type Agent struct { 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"` Devcontainers []*Devcontainer `protobuf:"bytes,25,rep,name=devcontainers,proto3" json:"devcontainers,omitempty"` + ApiKeyScope string `protobuf:"bytes,26,opt,name=api_key_scope,json=apiKeyScope,proto3" json:"api_key_scope,omitempty"` } func (x *Agent) Reset() { @@ -1452,6 +1453,13 @@ func (x *Agent) GetDevcontainers() []*Devcontainer { return nil } +func (x *Agent) GetApiKeyScope() string { + if x != nil { + return x.ApiKeyScope + } + return "" +} + type isAgent_Auth interface { isAgent_Auth() } @@ -3806,7 +3814,7 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 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, 0xb6, 0x08, 0x0a, 0x05, 0x41, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xda, 0x08, 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, @@ -3858,420 +3866,422 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 0x65, 0x72, 0x73, 0x18, 0x19, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x44, 0x65, 0x76, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0d, 0x64, 0x65, 0x76, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, - 0x65, 0x72, 0x73, 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, 0x6e, 0x0a, 0x0c, 0x44, 0x65, 0x76, 0x63, 0x6f, - 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x29, 0x0a, 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x5f, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x46, 0x6f, 0x6c, 0x64, - 0x65, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x70, 0x61, 0x74, - 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x50, - 0x61, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 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, 0x5e, - 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, 0x12, 0x10, 0x0a, 0x03, - 0x64, 0x69, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x64, 0x69, 0x72, 0x22, 0x31, - 0x0a, 0x04, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x15, 0x0a, 0x06, 0x6f, 0x72, - 0x67, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6f, 0x72, 0x67, 0x49, - 0x64, 0x22, 0x48, 0x0a, 0x15, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x41, 0x67, 0x65, 0x6e, - 0x74, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x67, - 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x67, - 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xca, 0x09, 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, + 0x65, 0x72, 0x73, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x70, 0x69, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x73, + 0x63, 0x6f, 0x70, 0x65, 0x18, 0x1a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x70, 0x69, 0x4b, + 0x65, 0x79, 0x53, 0x63, 0x6f, 0x70, 0x65, 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, 0x6e, 0x0a, 0x0c, 0x44, + 0x65, 0x76, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x29, 0x0a, 0x10, 0x77, + 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x50, 0x61, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 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, 0x5e, 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, + 0x12, 0x10, 0x0a, 0x03, 0x64, 0x69, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x64, + 0x69, 0x72, 0x22, 0x31, 0x0a, 0x04, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x15, + 0x0a, 0x06, 0x6f, 0x72, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x6f, 0x72, 0x67, 0x49, 0x64, 0x22, 0x48, 0x0a, 0x15, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, + 0x41, 0x67, 0x65, 0x6e, 0x74, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x19, + 0x0a, 0x08, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, + 0xca, 0x09, 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, 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, 0x12, 0x4e, 0x0a, 0x1a, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x72, 0x62, - 0x61, 0x63, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x18, 0x13, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x6f, 0x6c, - 0x65, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, - 0x72, 0x52, 0x62, 0x61, 0x63, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x12, 0x6d, 0x0a, 0x1e, 0x70, 0x72, - 0x65, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x5f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x67, 0x65, 0x18, 0x14, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x28, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x2e, 0x50, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x53, 0x74, 0x61, 0x67, 0x65, 0x52, 0x1b, 0x70, 0x72, - 0x65, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, - 0x75, 0x69, 0x6c, 0x64, 0x53, 0x74, 0x61, 0x67, 0x65, 0x12, 0x5d, 0x0a, 0x19, 0x72, 0x75, 0x6e, - 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, - 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x15, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x70, - 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x75, 0x6e, 0x6e, 0x69, - 0x6e, 0x67, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, - 0x52, 0x16, 0x72, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x41, 0x75, - 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 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, 0x92, 0x03, 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, 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, 0x12, 0x5b, 0x0a, 0x19, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x5f, - 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, - 0x18, 0x05, 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, 0x17, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, - 0x73, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, - 0x22, 0x93, 0x04, 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, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, - 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x12, 0x21, 0x0a, 0x0c, - 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x0b, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, - 0x55, 0x0a, 0x15, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x72, 0x65, 0x70, 0x6c, - 0x61, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, + 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, 0x12, 0x4e, + 0x0a, 0x1a, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, + 0x72, 0x5f, 0x72, 0x62, 0x61, 0x63, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x18, 0x13, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, + 0x2e, 0x52, 0x6f, 0x6c, 0x65, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x52, 0x62, 0x61, 0x63, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x12, 0x6d, + 0x0a, 0x1e, 0x70, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x5f, 0x77, 0x6f, 0x72, 0x6b, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x67, 0x65, + 0x18, 0x14, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x28, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x57, 0x6f, 0x72, + 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x53, 0x74, 0x61, 0x67, 0x65, + 0x52, 0x1b, 0x70, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x53, 0x74, 0x61, 0x67, 0x65, 0x12, 0x5d, 0x0a, + 0x19, 0x72, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x61, + 0x75, 0x74, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x15, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x22, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, + 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x41, 0x75, 0x74, 0x68, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x16, 0x72, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x41, 0x67, 0x65, + 0x6e, 0x74, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 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, + 0x92, 0x03, 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, 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, 0x12, 0x5b, 0x0a, 0x19, 0x70, 0x72, 0x65, 0x76, 0x69, + 0x6f, 0x75, 0x73, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x73, 0x18, 0x05, 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, 0x17, 0x70, 0x72, 0x65, + 0x76, 0x69, 0x6f, 0x75, 0x73, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x73, 0x22, 0x93, 0x04, 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, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x52, 0x14, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, - 0x65, 0x6d, 0x65, 0x6e, 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, 0x3e, 0x0a, 0x1b, 0x50, 0x72, 0x65, - 0x62, 0x75, 0x69, 0x6c, 0x74, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, - 0x69, 0x6c, 0x64, 0x53, 0x74, 0x61, 0x67, 0x65, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4e, 0x45, - 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x01, 0x12, 0x09, - 0x0a, 0x05, 0x43, 0x4c, 0x41, 0x49, 0x4d, 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, + 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, 0x12, 0x12, 0x0a, 0x04, + 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, + 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x73, + 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x46, 0x69, + 0x6c, 0x65, 0x73, 0x12, 0x55, 0x0a, 0x15, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, + 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x0b, 0x20, 0x03, + 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, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x14, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, + 0x70, 0x6c, 0x61, 0x63, 0x65, 0x6d, 0x65, 0x6e, 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, 0x3e, 0x0a, + 0x1b, 0x50, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x53, 0x74, 0x61, 0x67, 0x65, 0x12, 0x08, 0x0a, 0x04, + 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, + 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x43, 0x4c, 0x41, 0x49, 0x4d, 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 ( diff --git a/provisionersdk/proto/provisioner.proto b/provisionersdk/proto/provisioner.proto index edd8ae07e0d04..be480c1875942 100644 --- a/provisionersdk/proto/provisioner.proto +++ b/provisionersdk/proto/provisioner.proto @@ -152,6 +152,7 @@ message Agent { int64 order = 23; ResourcesMonitoring resources_monitoring = 24; repeated Devcontainer devcontainers = 25; + string api_key_scope = 26; } enum AppSharingLevel { diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index 75d8142295ccf..16d40d11f1f02 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -644,6 +644,7 @@ const createTemplateVersionTar = async ( troubleshootingUrl: "", token: randomUUID(), devcontainers: [], + apiKeyScope: "all", ...agent, } as Agent; diff --git a/site/e2e/provisionerGenerated.ts b/site/e2e/provisionerGenerated.ts index 28c225fc1ada3..d84d87755ad94 100644 --- a/site/e2e/provisionerGenerated.ts +++ b/site/e2e/provisionerGenerated.ts @@ -179,6 +179,7 @@ export interface Agent { order: number; resourcesMonitoring: ResourcesMonitoring | undefined; devcontainers: Devcontainer[]; + apiKeyScope: string; } export interface Agent_Metadata { @@ -710,6 +711,9 @@ export const Agent = { for (const v of message.devcontainers) { Devcontainer.encode(v!, writer.uint32(202).fork()).ldelim(); } + if (message.apiKeyScope !== "") { + writer.uint32(210).string(message.apiKeyScope); + } return writer; }, }; From ba6690f2ee6a9d806dae9c8b2d6a3e3d24c85e8b Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 15 May 2025 11:37:20 -0300 Subject: [PATCH 59/88] fix: show no provisioners warning (#17835) Screenshot 2025-05-14 at 14 53 02 Fix https://github.com/coder/coder/issues/17421 --- .../CreateTemplatePage/BuildLogsDrawer.stories.tsx | 4 ++++ site/src/pages/CreateTemplatePage/BuildLogsDrawer.tsx | 10 +++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/site/src/pages/CreateTemplatePage/BuildLogsDrawer.stories.tsx b/site/src/pages/CreateTemplatePage/BuildLogsDrawer.stories.tsx index 29229fadfd0ad..f2a773c09c099 100644 --- a/site/src/pages/CreateTemplatePage/BuildLogsDrawer.stories.tsx +++ b/site/src/pages/CreateTemplatePage/BuildLogsDrawer.stories.tsx @@ -78,6 +78,10 @@ export const Logs: Story = { ...MockTemplateVersion.job, status: "running", }, + matched_provisioners: { + count: 1, + available: 1, + }, }, }, decorators: [withWebSocket], diff --git a/site/src/pages/CreateTemplatePage/BuildLogsDrawer.tsx b/site/src/pages/CreateTemplatePage/BuildLogsDrawer.tsx index 35ad8eaba60cf..3f7099ebe673a 100644 --- a/site/src/pages/CreateTemplatePage/BuildLogsDrawer.tsx +++ b/site/src/pages/CreateTemplatePage/BuildLogsDrawer.tsx @@ -29,10 +29,6 @@ export const BuildLogsDrawer: FC = ({ variablesSectionRef, ...drawerProps }) => { - const matchingProvisioners = templateVersion?.matched_provisioners?.count; - const availableProvisioners = - templateVersion?.matched_provisioners?.available; - const logs = useWatchVersionLogs(templateVersion); const logsContainer = useRef(null); @@ -60,6 +56,10 @@ export const BuildLogsDrawer: FC = ({ error instanceof JobError && error.job.error_code === "REQUIRED_TEMPLATE_VARIABLES"; + const matchingProvisioners = templateVersion?.matched_provisioners?.count; + const availableProvisioners = + templateVersion?.matched_provisioners?.available; + return (
@@ -85,7 +85,7 @@ export const BuildLogsDrawer: FC = ({ drawerProps.onClose(); }} /> - ) : logs ? ( + ) : availableProvisioners && availableProvisioners > 0 && logs ? (
From 6ff6e954177a9d9a8b33043cd4bfb12b1ed82b23 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 15 May 2025 11:43:35 -0300 Subject: [PATCH 60/88] chore: replace MUI icons with Lucide icons - 13 (#17831) OpenInNew -> ExternalLinkIcon InfoOutlined -> InfoIcon CloudDownload -> CloudDownloadIcon CloudUpload -> CloudUploadIcon Compare -> GitCompareArrowsIcon SettingsEthernet -> GaugeIcon WebAsset -> AppWindowIcon --- .../src/modules/dashboard/DashboardLayout.tsx | 5 +++-- .../DeploymentBanner/DeploymentBannerView.tsx | 20 +++++++++---------- .../WorkspaceAppStatus/WorkspaceAppStatus.tsx | 11 +++++----- .../WorkspaceOutdatedTooltip.tsx | 2 +- .../WorkspaceTiming/StagesChart.tsx | 7 +++++-- .../AuditPage/AuditLogRow/AuditLogRow.tsx | 5 ++--- 6 files changed, 26 insertions(+), 24 deletions(-) diff --git a/site/src/modules/dashboard/DashboardLayout.tsx b/site/src/modules/dashboard/DashboardLayout.tsx index df3478ab18394..21fc29859f0ea 100644 --- a/site/src/modules/dashboard/DashboardLayout.tsx +++ b/site/src/modules/dashboard/DashboardLayout.tsx @@ -1,9 +1,9 @@ -import InfoOutlined from "@mui/icons-material/InfoOutlined"; import Link from "@mui/material/Link"; import Snackbar from "@mui/material/Snackbar"; import { Button } from "components/Button/Button"; import { Loader } from "components/Loader/Loader"; import { useAuthenticated } from "hooks"; +import { InfoIcon } from "lucide-react"; import { AnnouncementBanners } from "modules/dashboard/AnnouncementBanners/AnnouncementBanners"; import { LicenseBanner } from "modules/dashboard/LicenseBanner/LicenseBanner"; import { type FC, type HTMLAttributes, Suspense } from "react"; @@ -74,7 +74,8 @@ export const DashboardLayout: FC = () => { }} message={
- ({ fontSize: 16, height: 20, // 20 is the height of the text line so we can align them diff --git a/site/src/modules/dashboard/DeploymentBanner/DeploymentBannerView.tsx b/site/src/modules/dashboard/DeploymentBanner/DeploymentBannerView.tsx index 13bdaafef4a70..2fb5fdd819a03 100644 --- a/site/src/modules/dashboard/DeploymentBanner/DeploymentBannerView.tsx +++ b/site/src/modules/dashboard/DeploymentBanner/DeploymentBannerView.tsx @@ -1,10 +1,5 @@ import type { CSSInterpolation } from "@emotion/css/dist/declarations/src/create-instance"; import { type Interpolation, type Theme, css, useTheme } from "@emotion/react"; -import DownloadIcon from "@mui/icons-material/CloudDownload"; -import UploadIcon from "@mui/icons-material/CloudUpload"; -import CollectedIcon from "@mui/icons-material/Compare"; -import LatencyIcon from "@mui/icons-material/SettingsEthernet"; -import WebTerminalIcon from "@mui/icons-material/WebAsset"; import Button from "@mui/material/Button"; import Link from "@mui/material/Link"; import Tooltip from "@mui/material/Tooltip"; @@ -21,6 +16,11 @@ import { VSCodeIcon } from "components/Icons/VSCodeIcon"; import { Stack } from "components/Stack/Stack"; import dayjs from "dayjs"; import { type ClassName, useClassName } from "hooks/useClassName"; +import { CloudDownloadIcon } from "lucide-react"; +import { CloudUploadIcon } from "lucide-react"; +import { GitCompareArrowsIcon } from "lucide-react"; +import { GaugeIcon } from "lucide-react"; +import { AppWindowIcon } from "lucide-react"; import { RotateCwIcon, WrenchIcon } from "lucide-react"; import { CircleAlertIcon } from "lucide-react"; import prettyBytes from "pretty-bytes"; @@ -197,14 +197,14 @@ export const DeploymentBannerView: FC = ({
- + {stats ? prettyBytes(stats.workspaces.rx_bytes) : "-"}
- + {stats ? prettyBytes(stats.workspaces.tx_bytes) : "-"}
@@ -217,7 +217,7 @@ export const DeploymentBannerView: FC = ({ } >
- + {displayLatency > 0 ? `${displayLatency?.toFixed(2)} ms` : "-"}
@@ -269,7 +269,7 @@ export const DeploymentBannerView: FC = ({
- + {typeof stats?.session_count.reconnecting_pty === "undefined" ? "-" : stats?.session_count.reconnecting_pty} @@ -289,7 +289,7 @@ export const DeploymentBannerView: FC = ({ >
- + {lastAggregated}
diff --git a/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.tsx b/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.tsx index 9117b7aade6e5..412df60d9203e 100644 --- a/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.tsx +++ b/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.tsx @@ -4,7 +4,6 @@ import AppsIcon from "@mui/icons-material/Apps"; import CheckCircle from "@mui/icons-material/CheckCircle"; import ErrorIcon from "@mui/icons-material/Error"; import InsertDriveFile from "@mui/icons-material/InsertDriveFile"; -import OpenInNew from "@mui/icons-material/OpenInNew"; import Warning from "@mui/icons-material/Warning"; import CircularProgress from "@mui/material/CircularProgress"; import type { @@ -13,6 +12,7 @@ import type { WorkspaceAgent, WorkspaceApp, } from "api/typesGenerated"; +import { ExternalLinkIcon } from "lucide-react"; import { useAppLink } from "modules/apps/useAppLink"; import type { FC } from "react"; @@ -186,13 +186,12 @@ export const WorkspaceAppStatus = ({ }, }} > - = ({ {stage.label} - + diff --git a/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx b/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx index ebd79c0ba9abf..87b303f6014ef 100644 --- a/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx +++ b/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx @@ -1,5 +1,4 @@ import type { CSSObject, Interpolation, Theme } from "@emotion/react"; -import InfoOutlined from "@mui/icons-material/InfoOutlined"; import Collapse from "@mui/material/Collapse"; import Link from "@mui/material/Link"; import TableCell from "@mui/material/TableCell"; @@ -10,6 +9,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 { InfoIcon } from "lucide-react"; import { NetworkIcon } from "lucide-react"; import { type FC, useState } from "react"; import { Link as RouterLink } from "react-router-dom"; @@ -191,9 +191,8 @@ export const AuditLogRow: FC = ({
} > - ({ - fontSize: 20, color: theme.palette.info.light, })} /> From 257500c12f492058e15a3a878f2d05fee6698ac8 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 15 May 2025 12:08:35 -0300 Subject: [PATCH 61/88] chore: replace MUI icons with Lucide icons - 14 (#17832) HourglassEmpty -> HourglassIcon Star -> StarIcon CloudQueue -> CloudIcon InstallDesktop -> MonitorDownIcon WarningRounded -> TriangleAlertIcon ArrowBackOutlined -> ChevronLeftIcon MonetizationOnOutlined -> CircleDollarSign --- .../Navbar/UserDropdown/UserDropdownContent.tsx | 4 ++-- site/src/modules/resources/AgentStatus.tsx | 14 +++++++------- site/src/pages/HealthPage/DERPRegionPage.tsx | 7 ++++--- .../TemplateVersionEditor.tsx | 4 ++-- .../TemplateVersionStatusBadge.tsx | 4 ++-- site/src/pages/WorkspacePage/AppStatuses.tsx | 4 ++-- .../WorkspaceNotifications.tsx | 4 ++-- site/src/pages/WorkspacePage/WorkspaceTopbar.tsx | 11 +++++++---- .../WorkspacesPage/BatchUpdateConfirmation.tsx | 4 ++-- .../pages/WorkspacesPage/WorkspacesPageView.tsx | 4 ++-- site/src/pages/WorkspacesPage/WorkspacesTable.tsx | 6 ++++-- site/src/utils/workspace.tsx | 4 ++-- 12 files changed, 38 insertions(+), 32 deletions(-) diff --git a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx index 13ee16076dc5b..f0f9e257a0838 100644 --- a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx +++ b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx @@ -8,7 +8,6 @@ import AccountIcon from "@mui/icons-material/AccountCircleOutlined"; import BugIcon from "@mui/icons-material/BugReportOutlined"; import ChatIcon from "@mui/icons-material/ChatOutlined"; import LogoutIcon from "@mui/icons-material/ExitToAppOutlined"; -import InstallDesktopIcon from "@mui/icons-material/InstallDesktop"; import LaunchIcon from "@mui/icons-material/LaunchOutlined"; import DocsIcon from "@mui/icons-material/MenuBook"; import Divider from "@mui/material/Divider"; @@ -20,6 +19,7 @@ import { CopyButton } from "components/CopyButton/CopyButton"; import { ExternalImage } from "components/ExternalImage/ExternalImage"; import { Stack } from "components/Stack/Stack"; import { usePopover } from "components/deprecated/Popover/Popover"; +import { MonitorDownIcon } from "lucide-react"; import type { FC } from "react"; import { Link } from "react-router-dom"; @@ -79,7 +79,7 @@ export const UserDropdownContent: FC = ({ - + Install CLI diff --git a/site/src/modules/resources/AgentStatus.tsx b/site/src/modules/resources/AgentStatus.tsx index 88c127c58b14d..2f80ab5dab16a 100644 --- a/site/src/modules/resources/AgentStatus.tsx +++ b/site/src/modules/resources/AgentStatus.tsx @@ -1,5 +1,4 @@ import type { Interpolation, Theme } from "@emotion/react"; -import WarningRounded from "@mui/icons-material/WarningRounded"; import Link from "@mui/material/Link"; import Tooltip from "@mui/material/Tooltip"; import type { WorkspaceAgent } from "api/typesGenerated"; @@ -11,9 +10,10 @@ import { HelpTooltipTitle, } from "components/HelpTooltip/HelpTooltip"; import { PopoverTrigger } from "components/deprecated/Popover/Popover"; +import { TriangleAlertIcon } from "lucide-react"; import type { FC } from "react"; -// If we think in the agent status and lifecycle into a single enum/state I’d +// If we think in the agent status and lifecycle into a single enum/state I'd // say we would have: connecting, timeout, disconnected, connected:created, // connected:starting, connected:start_timeout, connected:start_error, // connected:ready, connected:shutting_down, connected:shutdown_timeout, @@ -50,7 +50,7 @@ const StartTimeoutLifecycle: FC = ({ agent }) => { return ( - + @@ -75,7 +75,7 @@ const StartErrorLifecycle: FC = ({ agent }) => { return ( - + Error starting the agent @@ -111,7 +111,7 @@ const ShutdownTimeoutLifecycle: FC = ({ agent }) => { return ( - + Agent is taking too long to stop @@ -135,7 +135,7 @@ const ShutdownErrorLifecycle: FC = ({ agent }) => { return ( - + Error stopping the agent @@ -231,7 +231,7 @@ const TimeoutStatus: FC = ({ agent }) => { return ( - + Agent is taking too long to connect diff --git a/site/src/pages/HealthPage/DERPRegionPage.tsx b/site/src/pages/HealthPage/DERPRegionPage.tsx index a32350e7afe2b..5bb190fd1e4b1 100644 --- a/site/src/pages/HealthPage/DERPRegionPage.tsx +++ b/site/src/pages/HealthPage/DERPRegionPage.tsx @@ -1,5 +1,4 @@ import { type Interpolation, type Theme, useTheme } from "@emotion/react"; -import ArrowBackOutlined from "@mui/icons-material/ArrowBackOutlined"; import TagOutlined from "@mui/icons-material/TagOutlined"; import Tooltip from "@mui/material/Tooltip"; import type { @@ -10,6 +9,7 @@ import type { HealthcheckReport, } from "api/typesGenerated"; import { Alert } from "components/Alert/Alert"; +import { ChevronLeftIcon } from "lucide-react"; import { CodeIcon } from "lucide-react"; import type { FC } from "react"; import { Helmet } from "react-helmet-async"; @@ -63,8 +63,9 @@ const DERPRegionPage: FC = () => { }} to="/health/derp" > - Back to DERP diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx index 7d8a3c36517a8..03dd9d47231bd 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx @@ -1,5 +1,4 @@ import { type Interpolation, type Theme, useTheme } from "@emotion/react"; -import ArrowBackOutlined from "@mui/icons-material/ArrowBackOutlined"; import WarningOutlined from "@mui/icons-material/WarningOutlined"; import Button from "@mui/material/Button"; import IconButton from "@mui/material/IconButton"; @@ -25,6 +24,7 @@ import { } from "components/FullPageLayout/Topbar"; import { displayError } from "components/GlobalSnackbar/utils"; import { Loader } from "components/Loader/Loader"; +import { ChevronLeftIcon } from "lucide-react"; import { PlayIcon, PlusIcon, XIcon } from "lucide-react"; import { linkToTemplate, useLinks } from "modules/navigation"; import { ProvisionerAlert } from "modules/provisioners/ProvisionerAlert"; @@ -217,7 +217,7 @@ export const TemplateVersionEditor: FC = ({
- +
diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionStatusBadge.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionStatusBadge.tsx index ac91c470fd3e2..bdafbd115a557 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionStatusBadge.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionStatusBadge.tsx @@ -1,6 +1,6 @@ -import QueuedIcon from "@mui/icons-material/HourglassEmpty"; import type { TemplateVersion } from "api/typesGenerated"; import { Pill, PillSpinner } from "components/Pill/Pill"; +import { HourglassIcon } from "lucide-react"; import { CheckIcon, CircleAlertIcon } from "lucide-react"; import type { FC, ReactNode } from "react"; import type { ThemeRole } from "theme/roles"; @@ -44,7 +44,7 @@ const getStatus = ( return { type: "active", text: getPendingStatusLabel(version.job), - icon: , + icon: , }; case "canceling": return { diff --git a/site/src/pages/WorkspacePage/AppStatuses.tsx b/site/src/pages/WorkspacePage/AppStatuses.tsx index 9d8b8ed4752c3..fe1df6863b26b 100644 --- a/site/src/pages/WorkspacePage/AppStatuses.tsx +++ b/site/src/pages/WorkspacePage/AppStatuses.tsx @@ -3,7 +3,6 @@ import { useTheme } from "@emotion/react"; import AppsIcon from "@mui/icons-material/Apps"; import CheckCircle from "@mui/icons-material/CheckCircle"; import ErrorIcon from "@mui/icons-material/Error"; -import HourglassEmpty from "@mui/icons-material/HourglassEmpty"; import InsertDriveFile from "@mui/icons-material/InsertDriveFile"; import OpenInNew from "@mui/icons-material/OpenInNew"; import Warning from "@mui/icons-material/Warning"; @@ -17,6 +16,7 @@ import type { WorkspaceApp, } from "api/typesGenerated"; import { formatDistance, formatDistanceToNow } from "date-fns"; +import { HourglassIcon } from "lucide-react"; import { CircleHelpIcon } from "lucide-react"; import { useAppLink } from "modules/apps/useAppLink"; import type { FC } from "react"; @@ -57,7 +57,7 @@ const getStatusIcon = ( return isLatest ? ( ) : ( - + ); default: return ; diff --git a/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.tsx b/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.tsx index 976e7bac4fcbb..53764f7afecd1 100644 --- a/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.tsx @@ -1,5 +1,4 @@ import type { Interpolation, Theme } from "@emotion/react"; -import WarningRounded from "@mui/icons-material/WarningRounded"; import { workspaceResolveAutostart } from "api/queries/workspaceQuota"; import type { Template, @@ -10,6 +9,7 @@ import type { import { MemoizedInlineMarkdown } from "components/Markdown/Markdown"; import formatDistanceToNow from "date-fns/formatDistanceToNow"; import dayjs from "dayjs"; +import { TriangleAlertIcon } from "lucide-react"; import { InfoIcon } from "lucide-react"; import { useDashboard } from "modules/dashboard/useDashboard"; import { TemplateUpdateMessage } from "modules/templates/TemplateUpdateMessage"; @@ -259,7 +259,7 @@ export const WorkspaceNotifications: FC = ({ } + icon={} /> )}
diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index 8f75e615895f6..5c4f3f99b7fb2 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -1,6 +1,4 @@ import { type Interpolation, type Theme, useTheme } from "@emotion/react"; -import ArrowBackOutlined from "@mui/icons-material/ArrowBackOutlined"; -import QuotaIcon from "@mui/icons-material/MonetizationOnOutlined"; import Link from "@mui/material/Link"; import Tooltip from "@mui/material/Tooltip"; import { workspaceQuota } from "api/queries/workspaceQuota"; @@ -17,6 +15,8 @@ import { } from "components/FullPageLayout/Topbar"; import { HelpTooltipContent } from "components/HelpTooltip/HelpTooltip"; import { Popover, PopoverTrigger } from "components/deprecated/Popover/Popover"; +import { ChevronLeftIcon } from "lucide-react"; +import { CircleDollarSign } from "lucide-react"; import { TrashIcon } from "lucide-react"; import { useDashboard } from "modules/dashboard/useDashboard"; import { linkToTemplate, useLinks } from "modules/navigation"; @@ -108,7 +108,7 @@ export const WorkspaceTopbar: FC = ({ - + @@ -163,7 +163,10 @@ export const WorkspaceTopbar: FC = ({ > - + diff --git a/site/src/pages/WorkspacesPage/BatchUpdateConfirmation.tsx b/site/src/pages/WorkspacesPage/BatchUpdateConfirmation.tsx index ac36009dacb70..a6b0a27b374f4 100644 --- a/site/src/pages/WorkspacesPage/BatchUpdateConfirmation.tsx +++ b/site/src/pages/WorkspacesPage/BatchUpdateConfirmation.tsx @@ -1,5 +1,4 @@ import type { Interpolation, Theme } from "@emotion/react"; -import InstallDesktopIcon from "@mui/icons-material/InstallDesktop"; import { API } from "api/api"; import type { TemplateVersion, Workspace } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; @@ -9,6 +8,7 @@ import { MemoizedInlineMarkdown } from "components/Markdown/Markdown"; import { Stack } from "components/Stack/Stack"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; +import { MonitorDownIcon } from "lucide-react"; import { ClockIcon, SettingsIcon, UserIcon } from "lucide-react"; import { type FC, type ReactNode, useEffect, useMemo, useState } from "react"; import { useQueries } from "react-query"; @@ -351,7 +351,7 @@ const Updates: FC = ({ workspaces, updates, error }) => { css={styles.summary} > - + {workspaceCount} {updateCount && ( diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index f4fd998f87410..55dd59db6a512 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -1,4 +1,3 @@ -import CloudQueue from "@mui/icons-material/CloudQueue"; import { hasError, isApiValidationError } from "api/errors"; import type { Template, Workspace } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; @@ -18,6 +17,7 @@ import { PaginationWidgetBase } from "components/PaginationWidget/PaginationWidg import { Spinner } from "components/Spinner/Spinner"; import { Stack } from "components/Stack/Stack"; import { TableToolbar } from "components/TableToolbar/TableToolbar"; +import { CloudIcon } from "lucide-react"; import { ChevronDownIcon, PlayIcon, SquareIcon, TrashIcon } from "lucide-react"; import { WorkspacesTable } from "pages/WorkspacesPage/WorkspacesTable"; import type { FC } from "react"; @@ -172,7 +172,7 @@ export const WorkspacesPageView: FC = ({ - Update… + Update… = ({ title={ {workspace.name} - {workspace.favorite && } + {workspace.favorite && ( + + )} {workspace.outdated && ( )} diff --git a/site/src/utils/workspace.tsx b/site/src/utils/workspace.tsx index 08bca308b20db..135b965589054 100644 --- a/site/src/utils/workspace.tsx +++ b/site/src/utils/workspace.tsx @@ -1,11 +1,11 @@ import type { Theme } from "@emotion/react"; -import QueuedIcon from "@mui/icons-material/HourglassEmpty"; import type * as TypesGen from "api/typesGenerated"; import { PillSpinner } from "components/Pill/Pill"; import dayjs from "dayjs"; import duration from "dayjs/plugin/duration"; import minMax from "dayjs/plugin/minMax"; import utc from "dayjs/plugin/utc"; +import { HourglassIcon } from "lucide-react"; import { CircleAlertIcon, PlayIcon, SquareIcon } from "lucide-react"; import semver from "semver"; import { getPendingStatusLabel } from "./provisionerJob"; @@ -249,7 +249,7 @@ export const getDisplayWorkspaceStatus = ( return { type: "active", text: getPendingStatusLabel(provisionerJob), - icon: , + icon: , } as const; } }; From 9beaca89fd06bcd31cdfd4d63aadbbf6a190a639 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 15 May 2025 12:08:48 -0300 Subject: [PATCH 62/88] chore: replace MUI LoadingButton - 3 (#17833) - RequestOTPPage - SetupPageView - TemplatePermissionsPageView - AccountForm - ExternalAuthPageView --- .../ResetPasswordPage/RequestOTPPage.tsx | 25 ++++++++++--------- site/src/pages/SetupPage/SetupPageView.tsx | 20 ++++++++------- .../TemplatePermissionsPageView.tsx | 14 +++++------ .../AccountPage/AccountForm.tsx | 8 +++--- .../ExternalAuthPage/ExternalAuthPageView.tsx | 11 ++++---- 5 files changed, 41 insertions(+), 37 deletions(-) diff --git a/site/src/pages/ResetPasswordPage/RequestOTPPage.tsx b/site/src/pages/ResetPasswordPage/RequestOTPPage.tsx index 6579eb1a0a265..2767dff4736ae 100644 --- a/site/src/pages/ResetPasswordPage/RequestOTPPage.tsx +++ b/site/src/pages/ResetPasswordPage/RequestOTPPage.tsx @@ -1,10 +1,11 @@ import { type Interpolation, type Theme, useTheme } from "@emotion/react"; -import LoadingButton from "@mui/lab/LoadingButton"; -import Button from "@mui/material/Button"; +import MuiButton from "@mui/material/Button"; import TextField from "@mui/material/TextField"; import { requestOneTimePassword } from "api/queries/users"; import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { Button } from "components/Button/Button"; import { CustomLogo } from "components/CustomLogo/CustomLogo"; +import { Spinner } from "components/Spinner/Spinner"; import { Stack } from "components/Stack/Stack"; import type { FC } from "react"; import { Helmet } from "react-helmet-async"; @@ -88,16 +89,16 @@ const RequestOTP: FC = ({ /> - + Reset password - - + @@ -150,9 +151,9 @@ const RequestOTPSuccess: FC<{ email: string }> = ({ email }) => { Contact your deployment administrator if you encounter issues.

- +
); diff --git a/site/src/pages/SetupPage/SetupPageView.tsx b/site/src/pages/SetupPage/SetupPageView.tsx index 42c8faedea348..b8735cbf0dbfa 100644 --- a/site/src/pages/SetupPage/SetupPageView.tsx +++ b/site/src/pages/SetupPage/SetupPageView.tsx @@ -1,8 +1,7 @@ import GitHubIcon from "@mui/icons-material/GitHub"; -import LoadingButton from "@mui/lab/LoadingButton"; import AlertTitle from "@mui/material/AlertTitle"; import Autocomplete from "@mui/material/Autocomplete"; -import Button from "@mui/material/Button"; +import MuiButton from "@mui/material/Button"; import Checkbox from "@mui/material/Checkbox"; import Link from "@mui/material/Link"; import MenuItem from "@mui/material/MenuItem"; @@ -11,10 +10,12 @@ import { countries } from "api/countriesGenerated"; import type * as TypesGen from "api/typesGenerated"; import { isAxiosError } from "axios"; import { Alert, AlertDetail } from "components/Alert/Alert"; +import { Button } from "components/Button/Button"; import { FormFields, VerticalForm } from "components/Form/Form"; import { CoderIcon } from "components/Icons/CoderIcon"; import { PasswordField } from "components/PasswordField/PasswordField"; import { SignInLayout } from "components/SignInLayout/SignInLayout"; +import { Spinner } from "components/Spinner/Spinner"; import { Stack } from "components/Stack/Stack"; import { type FormikContextType, useFormik } from "formik"; import type { ChangeEvent, FC } from "react"; @@ -172,7 +173,7 @@ export const SetupPageView: FC = ({ {authMethods?.github.enabled && ( <> - +
@@ -376,15 +377,16 @@ export const SetupPageView: FC = ({ )} - + {Language.create} - + diff --git a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.tsx b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.tsx index ab4cd597b9c2b..e00708a8b37ff 100644 --- a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.tsx @@ -1,6 +1,5 @@ import type { Interpolation, Theme } from "@emotion/react"; import PersonAdd from "@mui/icons-material/PersonAdd"; -import LoadingButton from "@mui/lab/LoadingButton"; import MenuItem from "@mui/material/MenuItem"; import Select, { type SelectProps } from "@mui/material/Select"; import Table from "@mui/material/Table"; @@ -29,6 +28,7 @@ import { } from "components/DropdownMenu/DropdownMenu"; import { EmptyState } from "components/EmptyState/EmptyState"; import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; +import { Spinner } from "components/Spinner/Spinner"; import { Stack } from "components/Stack/Stack"; import { TableLoader } from "components/TableLoader/TableLoader"; import { EllipsisVertical } from "lucide-react"; @@ -116,15 +116,15 @@ const AddTemplateUserOrGroup: FC = ({ - } - loading={isLoading} > + + + Add member - + ); diff --git a/site/src/pages/UserSettingsPage/AccountPage/AccountForm.tsx b/site/src/pages/UserSettingsPage/AccountPage/AccountForm.tsx index ea3b150d9844e..b5948d2b75a4d 100644 --- a/site/src/pages/UserSettingsPage/AccountPage/AccountForm.tsx +++ b/site/src/pages/UserSettingsPage/AccountPage/AccountForm.tsx @@ -1,8 +1,9 @@ -import LoadingButton from "@mui/lab/LoadingButton"; import TextField from "@mui/material/TextField"; import type { UpdateUserProfileRequest } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { Button } from "components/Button/Button"; import { Form, FormFields } from "components/Form/Form"; +import { Spinner } from "components/Spinner/Spinner"; import { type FormikTouched, useFormik } from "formik"; import type { FC } from "react"; import { @@ -86,9 +87,10 @@ export const AccountForm: FC = ({ />
- +
diff --git a/site/src/pages/UserSettingsPage/ExternalAuthPage/ExternalAuthPageView.tsx b/site/src/pages/UserSettingsPage/ExternalAuthPage/ExternalAuthPageView.tsx index e44e26fa5aeeb..7c325a20c7474 100644 --- a/site/src/pages/UserSettingsPage/ExternalAuthPage/ExternalAuthPageView.tsx +++ b/site/src/pages/UserSettingsPage/ExternalAuthPage/ExternalAuthPageView.tsx @@ -1,6 +1,5 @@ import { useTheme } from "@emotion/react"; import AutorenewIcon from "@mui/icons-material/Autorenew"; -import LoadingButton from "@mui/lab/LoadingButton"; import Table from "@mui/material/Table"; import TableBody from "@mui/material/TableBody"; import TableCell from "@mui/material/TableCell"; @@ -25,6 +24,7 @@ import { DropdownMenuTrigger, } from "components/DropdownMenu/DropdownMenu"; import { Loader } from "components/Loader/Loader"; +import { Spinner } from "components/Spinner/Spinner"; import { Stack } from "components/Stack/Stack"; import { TableEmpty } from "components/TableEmpty/TableEmpty"; import { EllipsisVertical } from "lucide-react"; @@ -165,17 +165,16 @@ const ExternalAuthRow: FC = ({ - { window.open(authURL, "_blank", "width=900,height=600"); startPollingExternalAuth(); }} > + {authenticated ? "Authenticated" : "Click to Login"} - + From bb6b96f11c8896624df10cc23ec82f35227af88b Mon Sep 17 00:00:00 2001 From: Tom Beckett <10406453+TomBeckett@users.noreply.github.com> Date: Thu, 15 May 2025 16:34:32 +0100 Subject: [PATCH 63/88] feat: add elixir icon (#17848) --- site/src/theme/icons.json | 1 + site/static/icon/elixir.svg | 1 + 2 files changed, 2 insertions(+) create mode 100644 site/static/icon/elixir.svg diff --git a/site/src/theme/icons.json b/site/src/theme/icons.json index 7a0d604167864..96f3abb704ef9 100644 --- a/site/src/theme/icons.json +++ b/site/src/theme/icons.json @@ -39,6 +39,7 @@ "docker.svg", "dotfiles.svg", "dotnet.svg", + "elixir.svg", "fedora.svg", "filebrowser.svg", "fleet.svg", diff --git a/site/static/icon/elixir.svg b/site/static/icon/elixir.svg new file mode 100644 index 0000000000000..5778e69d2d685 --- /dev/null +++ b/site/static/icon/elixir.svg @@ -0,0 +1 @@ + From bbceebde97ed82d36d4db5f04c25f8281c6611d6 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 15 May 2025 13:21:53 -0300 Subject: [PATCH 64/88] chore: remove @mui/lab (#17857) --- site/package.json | 1 - site/pnpm-lock.yaml | 76 --------------------------------------------- 2 files changed, 77 deletions(-) diff --git a/site/package.json b/site/package.json index bc459ce79f7a1..5c74070e936b3 100644 --- a/site/package.json +++ b/site/package.json @@ -51,7 +51,6 @@ "@fontsource/source-code-pro": "5.2.5", "@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", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index 252d7809033ec..d7b57631e8a3a 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -64,9 +64,6 @@ importers: '@mui/icons-material': specifier: 5.16.14 version: 5.16.14(@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))(@types/react@18.3.12)(react@18.3.1) - '@mui/lab': - specifier: 5.0.0-alpha.175 - version: 5.0.0-alpha.175(@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))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mui/material': specifier: 5.16.14 version: 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) @@ -1284,18 +1281,6 @@ packages: resolution: {integrity: sha512-SSnyl/4ni/2ViHKkiZb8eajA/eN1DNFaHjhGiLUdZvDz6PKF4COSf/17xqSz64nOo2Ia29SA6B2KNCsyCbVmaQ==, tarball: https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.35.9.tgz} engines: {node: '>=18'} - '@mui/base@5.0.0-beta.40-0': - resolution: {integrity: sha512-hG3atoDUxlvEy+0mqdMpWd04wca8HKr2IHjW/fAjlkCHQolSLazhZM46vnHjOf15M4ESu25mV/3PgjczyjVM4w==, tarball: https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40-0.tgz} - engines: {node: '>=12.0.0'} - deprecated: This package has been replaced by @base-ui-components/react - peerDependencies: - '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 - react: ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - '@mui/core-downloads-tracker@5.16.14': resolution: {integrity: sha512-sbjXW+BBSvmzn61XyTMun899E7nGPTXwqD9drm1jBUAvWEhJpPFIRxwQQiATWZnd9rvdxtnhhdsDxEGWI0jxqA==, tarball: https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.14.tgz} @@ -1310,24 +1295,6 @@ packages: '@types/react': optional: true - '@mui/lab@5.0.0-alpha.175': - resolution: {integrity: sha512-AvM0Nvnnj7vHc9+pkkQkoE1i+dEbr6gsMdnSfy7X4w3Ljgcj1yrjZhIt3jGTCLzyKVLa6uve5eLluOcGkvMqUA==, tarball: https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.175.tgz} - engines: {node: '>=12.0.0'} - peerDependencies: - '@emotion/react': ^11.5.0 - '@emotion/styled': ^11.3.0 - '@mui/material': '>=5.15.0' - '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 - react: ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@emotion/react': - optional: true - '@emotion/styled': - optional: true - '@types/react': - optional: true - '@mui/material@5.16.14': resolution: {integrity: sha512-eSXQVCMKU2xc7EcTxe/X/rC9QsV2jUe8eLM3MUCPYbo6V52eCE436akRIvELq/AqZpxx2bwkq7HC0cRhLB+yaw==, tarball: https://registry.npmjs.org/@mui/material/-/material-5.16.14.tgz} engines: {node: '>=12.0.0'} @@ -1384,14 +1351,6 @@ packages: '@types/react': optional: true - '@mui/types@7.2.20': - resolution: {integrity: sha512-straFHD7L8v05l/N5vcWk+y7eL9JF0C2mtph/y4BPm3gn2Eh61dDwDB65pa8DLss3WJfDXYC7Kx5yjP0EmXpgw==, tarball: https://registry.npmjs.org/@mui/types/-/types-7.2.20.tgz} - peerDependencies: - '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - '@mui/types@7.2.21': resolution: {integrity: sha512-6HstngiUxNqLU+/DPqlUJDIPbzUBxIVHb1MmXP0eTWDIROiCR2viugXpEif0PPe2mLqqakPzzRClWAnK+8UJww==, tarball: https://registry.npmjs.org/@mui/types/-/types-7.2.21.tgz} peerDependencies: @@ -7434,20 +7393,6 @@ snapshots: outvariant: 1.4.3 strict-event-emitter: 0.5.1 - '@mui/base@5.0.0-beta.40-0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@babel/runtime': 7.26.10 - '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@mui/types': 7.2.20(@types/react@18.3.12) - '@mui/utils': 5.16.14(@types/react@18.3.12)(react@18.3.1) - '@popperjs/core': 2.11.8 - clsx: 2.1.1 - prop-types: 15.8.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - optionalDependencies: - '@types/react': 18.3.12 - '@mui/core-downloads-tracker@5.16.14': {} '@mui/icons-material@5.16.14(@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))(@types/react@18.3.12)(react@18.3.1)': @@ -7458,23 +7403,6 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 - '@mui/lab@5.0.0-alpha.175(@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))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@babel/runtime': 7.26.10 - '@mui/base': 5.0.0-beta.40-0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(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) - '@mui/types': 7.2.20(@types/react@18.3.12) - '@mui/utils': 5.16.14(@types/react@18.3.12)(react@18.3.1) - clsx: 2.1.1 - prop-types: 15.8.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - optionalDependencies: - '@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 - '@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)': dependencies: '@babel/runtime': 7.26.10 @@ -7532,10 +7460,6 @@ snapshots: '@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 - '@mui/types@7.2.20(@types/react@18.3.12)': - optionalDependencies: - '@types/react': 18.3.12 - '@mui/types@7.2.21(@types/react@18.3.12)': optionalDependencies: '@types/react': 18.3.12 From 2c49fd9e9618efd2b55fb749fec208abd6760299 Mon Sep 17 00:00:00 2001 From: M Atif Ali Date: Thu, 15 May 2025 10:41:01 -0700 Subject: [PATCH 65/88] feat: add copy button for workspace name in breadcrumb (#17822) Co-authored-by: BrunoQuaresma --- .../components/CodeExample/CodeExample.tsx | 34 +----- site/src/components/CopyButton/CopyButton.tsx | 103 ++++++------------ .../GitDeviceAuth/GitDeviceAuth.tsx | 6 +- site/src/components/Icons/FileCopyIcon.tsx | 10 -- .../UserDropdown/UserDropdownContent.tsx | 12 +- .../pages/WorkspacePage/WorkspaceTopbar.tsx | 90 ++++++++------- 6 files changed, 95 insertions(+), 160 deletions(-) delete mode 100644 site/src/components/Icons/FileCopyIcon.tsx diff --git a/site/src/components/CodeExample/CodeExample.tsx b/site/src/components/CodeExample/CodeExample.tsx index 71ef7f951471e..b2c8bd16cf0a1 100644 --- a/site/src/components/CodeExample/CodeExample.tsx +++ b/site/src/components/CodeExample/CodeExample.tsx @@ -1,6 +1,5 @@ import type { Interpolation, Theme } from "@emotion/react"; -import { visuallyHidden } from "@mui/utils"; -import { type FC, type KeyboardEvent, type MouseEvent, useRef } from "react"; +import type { FC } from "react"; import { MONOSPACE_FONT_FAMILY } from "theme/constants"; import { CopyButton } from "../CopyButton/CopyButton"; @@ -21,33 +20,8 @@ export const CodeExample: FC = ({ // the secure option, not remember to opt in secret = true, }) => { - const buttonRef = useRef(null); - const triggerButton = (event: KeyboardEvent | MouseEvent) => { - const clickTriggeredOutsideButton = - event.target instanceof HTMLElement && - !buttonRef.current?.contains(event.target); - - if (clickTriggeredOutsideButton) { - buttonRef.current?.click(); - } - }; - return ( -
{ - if (event.key === "Enter") { - triggerButton(event); - } - }} - onKeyUp={(event) => { - if (event.key === " ") { - triggerButton(event); - } - }} - > +
{secret ? ( <> @@ -60,7 +34,7 @@ export const CodeExample: FC = ({ * readily available in the HTML itself */} {obfuscateText(code)} - + Encrypted text. Please access via the copy button. @@ -69,7 +43,7 @@ export const CodeExample: FC = ({ )} - +
); }; diff --git a/site/src/components/CopyButton/CopyButton.tsx b/site/src/components/CopyButton/CopyButton.tsx index 86a14c2a2ff48..9110bb4cd68d0 100644 --- a/site/src/components/CopyButton/CopyButton.tsx +++ b/site/src/components/CopyButton/CopyButton.tsx @@ -1,77 +1,44 @@ -import { type Interpolation, type Theme, css } from "@emotion/react"; -import IconButton from "@mui/material/Button"; -import Tooltip from "@mui/material/Tooltip"; +import { Button, type ButtonProps } from "components/Button/Button"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "components/Tooltip/Tooltip"; import { useClipboard } from "hooks/useClipboard"; -import { CheckIcon } from "lucide-react"; -import { type ReactNode, forwardRef } from "react"; -import { FileCopyIcon } from "../Icons/FileCopyIcon"; +import { CheckIcon, CopyIcon } from "lucide-react"; +import type { FC } from "react"; -interface CopyButtonProps { - children?: ReactNode; +type CopyButtonProps = ButtonProps & { text: string; - ctaCopy?: string; - wrapperStyles?: Interpolation; - buttonStyles?: Interpolation; - tooltipTitle?: string; -} - -const Language = { - tooltipTitle: "Copy to clipboard", - ariaLabel: "Copy to clipboard", + label: string; }; -/** - * Copy button used inside the CodeBlock component internally - */ -export const CopyButton = forwardRef( - (props, ref) => { - const { - text, - ctaCopy, - wrapperStyles, - buttonStyles, - tooltipTitle = Language.tooltipTitle, - } = props; - const { showCopiedSuccess, copyToClipboard } = useClipboard({ - textToCopy: text, - }); +export const CopyButton: FC = ({ + text, + label, + ...buttonProps +}) => { + const { showCopiedSuccess, copyToClipboard } = useClipboard({ + textToCopy: text, + }); - return ( - -
- + + +
+ {showCopiedSuccess ? : } + {label} + + + {label}
- ); - }, -); - -const styles = { - button: (theme) => css` - border-radius: 8px; - padding: 8px; - min-width: 32px; - - &:hover { - background: ${theme.palette.background.paper}; - } - `, - copyIcon: css` - width: 20px; - height: 20px; - `, -} satisfies Record>; + + ); +}; diff --git a/site/src/components/GitDeviceAuth/GitDeviceAuth.tsx b/site/src/components/GitDeviceAuth/GitDeviceAuth.tsx index 5bbf036943773..bd35b305eea7f 100644 --- a/site/src/components/GitDeviceAuth/GitDeviceAuth.tsx +++ b/site/src/components/GitDeviceAuth/GitDeviceAuth.tsx @@ -134,7 +134,11 @@ export const GitDeviceAuth: FC = ({ Copy your one-time code: 
{externalAuthDevice.user_code} -   +  {" "} +

Then open the link below and paste it: diff --git a/site/src/components/Icons/FileCopyIcon.tsx b/site/src/components/Icons/FileCopyIcon.tsx deleted file mode 100644 index bd6fc359fe71f..0000000000000 --- a/site/src/components/Icons/FileCopyIcon.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import SvgIcon, { type SvgIconProps } from "@mui/material/SvgIcon"; - -export const FileCopyIcon = (props: SvgIconProps): JSX.Element => ( - - - -); diff --git a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx index f0f9e257a0838..fe4c7d0cebe84 100644 --- a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx +++ b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx @@ -151,15 +151,7 @@ export const UserDropdownContent: FC = ({
)} @@ -181,7 +173,7 @@ const GithubStar: FC = (props) => ( fill="currentColor" {...props} > - + ); diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index 5c4f3f99b7fb2..33aa5d29ea922 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -5,6 +5,7 @@ import { workspaceQuota } from "api/queries/workspaceQuota"; import type * as TypesGen from "api/typesGenerated"; import { Avatar } from "components/Avatar/Avatar"; import { AvatarData } from "components/Avatar/AvatarData"; +import { CopyButton } from "components/CopyButton/CopyButton"; import { Topbar, TopbarAvatar, @@ -346,50 +347,57 @@ const WorkspaceBreadcrumb: FC = ({ templateDisplayName, }) => { return ( - - - - - - {workspaceName} - - - - - - - {templateDisplayName} - - } - subtitle={ - - Version: {latestBuildVersionName} - - } - avatar={ - + + + + - } - imgFallbackText={templateDisplayName} - /> - - + + + {workspaceName} + + + + + + + {templateDisplayName} + + } + subtitle={ + + Version: {latestBuildVersionName} + + } + avatar={ + + } + imgFallbackText={templateDisplayName} + /> + + + +
); }; From 952c254046b2328e738db920606f97db35d61dda Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 15 May 2025 15:21:33 -0300 Subject: [PATCH 66/88] fix: fix duplicated agent logs (#17806) Fix https://github.com/coder/coder/issues/16355 --- site/src/api/api.ts | 30 +--- site/src/api/queries/workspaces.ts | 16 +- .../resources/AgentLogs/useAgentLogs.test.tsx | 142 ------------------ .../resources/AgentLogs/useAgentLogs.ts | 95 ------------ site/src/modules/resources/AgentRow.tsx | 9 +- .../DownloadAgentLogsButton.stories.tsx | 2 +- .../resources/DownloadAgentLogsButton.tsx | 2 +- .../modules/resources/useAgentLogs.test.ts | 54 +++++++ site/src/modules/resources/useAgentLogs.ts | 47 ++++++ .../DownloadLogsDialog.stories.tsx | 4 +- .../DownloadLogsDialog.tsx | 2 +- .../WorkspaceBuildPageView.tsx | 27 ++-- .../WorkspaceActions.stories.tsx | 2 +- .../WorkspacePage/WorkspacePage.test.tsx | 7 +- site/src/testHelpers/storybook.tsx | 4 + 15 files changed, 136 insertions(+), 307 deletions(-) delete mode 100644 site/src/modules/resources/AgentLogs/useAgentLogs.test.tsx delete mode 100644 site/src/modules/resources/AgentLogs/useAgentLogs.ts create mode 100644 site/src/modules/resources/useAgentLogs.test.ts create mode 100644 site/src/modules/resources/useAgentLogs.ts diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 206e6e5f466f2..9e579c3706de6 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -221,11 +221,11 @@ export const watchBuildLogsByTemplateVersionId = ( export const watchWorkspaceAgentLogs = ( agentId: string, - { after, onMessage, onDone, onError }: WatchWorkspaceAgentLogsOptions, + params?: WatchWorkspaceAgentLogsParams, ) => { const searchParams = new URLSearchParams({ follow: "true", - after: after.toString(), + after: params?.after?.toString() ?? "", }); /** @@ -237,32 +237,14 @@ export const watchWorkspaceAgentLogs = ( searchParams.set("no_compression", ""); } - const socket = createWebSocket( - `/api/v2/workspaceagents/${agentId}/logs`, + return new OneWayWebSocket({ + apiRoute: `/api/v2/workspaceagents/${agentId}/logs`, searchParams, - ); - - socket.addEventListener("message", (event) => { - const logs = JSON.parse(event.data) as TypesGen.WorkspaceAgentLog[]; - onMessage(logs); - }); - - socket.addEventListener("error", () => { - onError(new Error("socket errored")); }); - - socket.addEventListener("close", () => { - onDone?.(); - }); - - return socket; }; -type WatchWorkspaceAgentLogsOptions = { - after: number; - onMessage: (logs: TypesGen.WorkspaceAgentLog[]) => void; - onDone?: () => void; - onError: (error: Error) => void; +type WatchWorkspaceAgentLogsParams = { + after?: number; }; type WatchBuildLogsByBuildIdOptions = { diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index 4f4b9b80cc8f9..86417e4f13655 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -5,6 +5,7 @@ import type { ProvisionerLogLevel, UsageAppName, Workspace, + WorkspaceAgentLog, WorkspaceBuild, WorkspaceBuildParameter, WorkspacesRequest, @@ -20,6 +21,7 @@ import type { QueryClient, QueryOptions, UseMutationOptions, + UseQueryOptions, } from "react-query"; import { checkAuthorization } from "./authCheck"; import { disabledRefetchOptions } from "./util"; @@ -342,20 +344,14 @@ export const buildLogs = (workspace: Workspace) => { }; }; -export const agentLogsKey = (workspaceId: string, agentId: string) => [ - "workspaces", - workspaceId, - "agents", - agentId, - "logs", -]; +export const agentLogsKey = (agentId: string) => ["agents", agentId, "logs"]; -export const agentLogs = (workspaceId: string, agentId: string) => { +export const agentLogs = (agentId: string) => { return { - queryKey: agentLogsKey(workspaceId, agentId), + queryKey: agentLogsKey(agentId), queryFn: () => API.getWorkspaceAgentLogs(agentId), ...disabledRefetchOptions, - }; + } satisfies UseQueryOptions; }; // workspace usage options diff --git a/site/src/modules/resources/AgentLogs/useAgentLogs.test.tsx b/site/src/modules/resources/AgentLogs/useAgentLogs.test.tsx deleted file mode 100644 index e1aaccc40d6f7..0000000000000 --- a/site/src/modules/resources/AgentLogs/useAgentLogs.test.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { act, renderHook, waitFor } from "@testing-library/react"; -import { API } from "api/api"; -import * as APIModule from "api/api"; -import { agentLogsKey } from "api/queries/workspaces"; -import type { WorkspaceAgentLog } from "api/typesGenerated"; -import WS from "jest-websocket-mock"; -import { type QueryClient, QueryClientProvider } from "react-query"; -import { MockWorkspace, MockWorkspaceAgent } from "testHelpers/entities"; -import { createTestQueryClient } from "testHelpers/renderHelpers"; -import { type UseAgentLogsOptions, useAgentLogs } from "./useAgentLogs"; - -afterEach(() => { - WS.clean(); -}); - -describe("useAgentLogs", () => { - it("should not fetch logs if disabled", async () => { - const queryClient = createTestQueryClient(); - const fetchSpy = jest.spyOn(API, "getWorkspaceAgentLogs"); - const wsSpy = jest.spyOn(APIModule, "watchWorkspaceAgentLogs"); - renderUseAgentLogs(queryClient, { - workspaceId: MockWorkspace.id, - agentId: MockWorkspaceAgent.id, - agentLifeCycleState: "ready", - enabled: false, - }); - expect(fetchSpy).not.toHaveBeenCalled(); - expect(wsSpy).not.toHaveBeenCalled(); - }); - - it("should return existing logs without network calls if state is off", async () => { - const queryClient = createTestQueryClient(); - queryClient.setQueryData( - agentLogsKey(MockWorkspace.id, MockWorkspaceAgent.id), - generateLogs(5), - ); - const fetchSpy = jest.spyOn(API, "getWorkspaceAgentLogs"); - const wsSpy = jest.spyOn(APIModule, "watchWorkspaceAgentLogs"); - const { result } = renderUseAgentLogs(queryClient, { - workspaceId: MockWorkspace.id, - agentId: MockWorkspaceAgent.id, - agentLifeCycleState: "off", - }); - await waitFor(() => { - expect(result.current).toHaveLength(5); - }); - expect(fetchSpy).not.toHaveBeenCalled(); - expect(wsSpy).not.toHaveBeenCalled(); - }); - - it("should fetch logs when empty", async () => { - const queryClient = createTestQueryClient(); - const fetchSpy = jest - .spyOn(API, "getWorkspaceAgentLogs") - .mockResolvedValueOnce(generateLogs(5)); - jest.spyOn(APIModule, "watchWorkspaceAgentLogs"); - const { result } = renderUseAgentLogs(queryClient, { - workspaceId: MockWorkspace.id, - agentId: MockWorkspaceAgent.id, - agentLifeCycleState: "ready", - }); - await waitFor(() => { - expect(result.current).toHaveLength(5); - }); - expect(fetchSpy).toHaveBeenCalledWith(MockWorkspaceAgent.id); - }); - - it("should fetch logs and connect to websocket", async () => { - const queryClient = createTestQueryClient(); - const logs = generateLogs(5); - const fetchSpy = jest - .spyOn(API, "getWorkspaceAgentLogs") - .mockResolvedValueOnce(logs); - const wsSpy = jest.spyOn(APIModule, "watchWorkspaceAgentLogs"); - new WS( - `ws://localhost/api/v2/workspaceagents/${ - MockWorkspaceAgent.id - }/logs?follow&after=${logs[logs.length - 1].id}`, - ); - const { result } = renderUseAgentLogs(queryClient, { - workspaceId: MockWorkspace.id, - agentId: MockWorkspaceAgent.id, - agentLifeCycleState: "starting", - }); - await waitFor(() => { - expect(result.current).toHaveLength(5); - }); - expect(fetchSpy).toHaveBeenCalledWith(MockWorkspaceAgent.id); - expect(wsSpy).toHaveBeenCalledWith(MockWorkspaceAgent.id, { - after: logs[logs.length - 1].id, - onMessage: expect.any(Function), - onError: expect.any(Function), - }); - }); - - it("update logs from websocket messages", async () => { - const queryClient = createTestQueryClient(); - const logs = generateLogs(5); - jest.spyOn(API, "getWorkspaceAgentLogs").mockResolvedValueOnce(logs); - const server = new WS( - `ws://localhost/api/v2/workspaceagents/${ - MockWorkspaceAgent.id - }/logs?follow&after=${logs[logs.length - 1].id}`, - ); - const { result } = renderUseAgentLogs(queryClient, { - workspaceId: MockWorkspace.id, - agentId: MockWorkspaceAgent.id, - agentLifeCycleState: "starting", - }); - await waitFor(() => { - expect(result.current).toHaveLength(5); - }); - await server.connected; - act(() => { - server.send(JSON.stringify(generateLogs(3))); - }); - await waitFor(() => { - expect(result.current).toHaveLength(8); - }); - }); -}); - -function renderUseAgentLogs( - queryClient: QueryClient, - options: UseAgentLogsOptions, -) { - return renderHook(() => useAgentLogs(options), { - wrapper: ({ children }) => ( - {children} - ), - }); -} - -function generateLogs(count: number): WorkspaceAgentLog[] { - return Array.from({ length: count }, (_, i) => ({ - id: i, - created_at: new Date().toISOString(), - level: "info", - output: `Log ${i}`, - source_id: "", - })); -} diff --git a/site/src/modules/resources/AgentLogs/useAgentLogs.ts b/site/src/modules/resources/AgentLogs/useAgentLogs.ts deleted file mode 100644 index a53f1d882dc60..0000000000000 --- a/site/src/modules/resources/AgentLogs/useAgentLogs.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { watchWorkspaceAgentLogs } from "api/api"; -import { agentLogs } from "api/queries/workspaces"; -import type { - WorkspaceAgentLifecycle, - WorkspaceAgentLog, -} from "api/typesGenerated"; -import { useEffectEvent } from "hooks/hookPolyfills"; -import { useEffect, useRef } from "react"; -import { useQuery, useQueryClient } from "react-query"; - -export type UseAgentLogsOptions = Readonly<{ - workspaceId: string; - agentId: string; - agentLifeCycleState: WorkspaceAgentLifecycle; - enabled?: boolean; -}>; - -/** - * Defines a custom hook that gives you all workspace agent logs for a given - * workspace.Depending on the status of the workspace, all logs may or may not - * be available. - */ -export function useAgentLogs( - options: UseAgentLogsOptions, -): readonly WorkspaceAgentLog[] | undefined { - const { workspaceId, agentId, agentLifeCycleState, enabled = true } = options; - const queryClient = useQueryClient(); - const queryOptions = agentLogs(workspaceId, agentId); - const { data: logs, isFetched } = useQuery({ ...queryOptions, enabled }); - - // Track the ID of the last log received when the initial logs response comes - // back. If the logs are not complete, the ID will mark the start point of the - // Web sockets response so that the remaining logs can be received over time - const lastQueriedLogId = useRef(0); - useEffect(() => { - const isAlreadyTracking = lastQueriedLogId.current !== 0; - if (isAlreadyTracking) { - return; - } - - const lastLog = logs?.at(-1); - if (lastLog !== undefined) { - lastQueriedLogId.current = lastLog.id; - } - }, [logs]); - - const addLogs = useEffectEvent((newLogs: WorkspaceAgentLog[]) => { - queryClient.setQueryData( - queryOptions.queryKey, - (oldData: WorkspaceAgentLog[] = []) => [...oldData, ...newLogs], - ); - }); - - useEffect(() => { - // Stream data only for new logs. Old logs should be loaded beforehand - // using a regular fetch to avoid overloading the websocket with all - // logs at once. - if (!isFetched) { - return; - } - - // If the agent is off, we don't need to stream logs. This is the only state - // where the Coder API can't receive logs for the agent from third-party - // apps like envbuilder. - if (agentLifeCycleState === "off") { - return; - } - - const socket = watchWorkspaceAgentLogs(agentId, { - after: lastQueriedLogId.current, - onMessage: (newLogs) => { - // Prevent new logs getting added when a connection is not open - if (socket.readyState !== WebSocket.OPEN) { - return; - } - addLogs(newLogs); - }, - onError: (error) => { - // For some reason Firefox and Safari throw an error when a websocket - // connection is close in the middle of a message and because of that we - // can't safely show to the users an error message since most of the - // time they are just internal stuff. This does not happen to Chrome at - // all and I tried to find better way to "soft close" a WS connection on - // those browsers without success. - console.error(error); - }, - }); - - return () => { - socket.close(); - }; - }, [addLogs, agentId, agentLifeCycleState, isFetched]); - - return logs; -} diff --git a/site/src/modules/resources/AgentRow.tsx b/site/src/modules/resources/AgentRow.tsx index bc0751c332942..4e53c2cf2ba2c 100644 --- a/site/src/modules/resources/AgentRow.tsx +++ b/site/src/modules/resources/AgentRow.tsx @@ -31,7 +31,6 @@ import { AgentDevcontainerCard } from "./AgentDevcontainerCard"; import { AgentLatency } from "./AgentLatency"; import { AGENT_LOG_LINE_HEIGHT } from "./AgentLogs/AgentLogLine"; import { AgentLogs } from "./AgentLogs/AgentLogs"; -import { useAgentLogs } from "./AgentLogs/useAgentLogs"; import { AgentMetadata } from "./AgentMetadata"; import { AgentStatus } from "./AgentStatus"; import { AgentVersion } from "./AgentVersion"; @@ -41,6 +40,7 @@ import { PortForwardButton } from "./PortForwardButton"; import { AgentSSHButton } from "./SSHButton/SSHButton"; import { TerminalLink } from "./TerminalLink/TerminalLink"; import { VSCodeDesktopButton } from "./VSCodeDesktopButton/VSCodeDesktopButton"; +import { useAgentLogs } from "./useAgentLogs"; export interface AgentRowProps { agent: WorkspaceAgent; @@ -89,12 +89,7 @@ export const AgentRow: FC = ({ ["starting", "start_timeout"].includes(agent.lifecycle_state) && hasStartupFeatures, ); - const agentLogs = useAgentLogs({ - workspaceId: workspace.id, - agentId: agent.id, - agentLifeCycleState: agent.lifecycle_state, - enabled: showLogs, - }); + const agentLogs = useAgentLogs(agent, showLogs); const logListRef = useRef(null); const logListDivRef = useRef(null); const startupLogs = useMemo(() => { diff --git a/site/src/modules/resources/DownloadAgentLogsButton.stories.tsx b/site/src/modules/resources/DownloadAgentLogsButton.stories.tsx index 5d39ab7d74412..74b1df7258059 100644 --- a/site/src/modules/resources/DownloadAgentLogsButton.stories.tsx +++ b/site/src/modules/resources/DownloadAgentLogsButton.stories.tsx @@ -15,7 +15,7 @@ const meta: Meta = { parameters: { queries: [ { - key: agentLogsKey(MockWorkspace.id, MockWorkspaceAgent.id), + key: agentLogsKey(MockWorkspaceAgent.id), data: generateLogs(5), }, ], diff --git a/site/src/modules/resources/DownloadAgentLogsButton.tsx b/site/src/modules/resources/DownloadAgentLogsButton.tsx index b884a250c7fcb..dfc00433a8063 100644 --- a/site/src/modules/resources/DownloadAgentLogsButton.tsx +++ b/site/src/modules/resources/DownloadAgentLogsButton.tsx @@ -23,7 +23,7 @@ export const DownloadAgentLogsButton: FC = ({ const [isDownloading, setIsDownloading] = useState(false); const fetchLogs = async () => { - const queryOpts = agentLogs(workspaceId, agent.id); + const queryOpts = agentLogs(agent.id); let logs = queryClient.getQueryData( queryOpts.queryKey, ); diff --git a/site/src/modules/resources/useAgentLogs.test.ts b/site/src/modules/resources/useAgentLogs.test.ts new file mode 100644 index 0000000000000..8480f756611d2 --- /dev/null +++ b/site/src/modules/resources/useAgentLogs.test.ts @@ -0,0 +1,54 @@ +import { renderHook } from "@testing-library/react"; +import type { WorkspaceAgentLog } from "api/typesGenerated"; +import WS from "jest-websocket-mock"; +import { MockWorkspaceAgent } from "testHelpers/entities"; +import { useAgentLogs } from "./useAgentLogs"; + +/** + * TODO: WS does not support multiple tests running at once in isolation so we + * have one single test that test the most common scenario. + * Issue: https://github.com/romgain/jest-websocket-mock/issues/172 + */ + +describe("useAgentLogs", () => { + afterEach(() => { + WS.clean(); + }); + + it("clear logs when disabled to avoid duplicates", async () => { + const server = new WS( + `ws://localhost/api/v2/workspaceagents/${ + MockWorkspaceAgent.id + }/logs?follow&after=0`, + ); + const { result, rerender } = renderHook( + ({ enabled }) => useAgentLogs(MockWorkspaceAgent, enabled), + { initialProps: { enabled: true } }, + ); + await server.connected; + + // Send 3 logs + server.send(JSON.stringify(generateLogs(3))); + expect(result.current).toHaveLength(3); + + // Disable the hook + rerender({ enabled: false }); + expect(result.current).toHaveLength(0); + + // Enable the hook again + rerender({ enabled: true }); + await server.connected; + server.send(JSON.stringify(generateLogs(3))); + expect(result.current).toHaveLength(3); + }); +}); + +function generateLogs(count: number): WorkspaceAgentLog[] { + return Array.from({ length: count }, (_, i) => ({ + id: i, + created_at: new Date().toISOString(), + level: "info", + output: `Log ${i}`, + source_id: "", + })); +} diff --git a/site/src/modules/resources/useAgentLogs.ts b/site/src/modules/resources/useAgentLogs.ts new file mode 100644 index 0000000000000..d7f810483a693 --- /dev/null +++ b/site/src/modules/resources/useAgentLogs.ts @@ -0,0 +1,47 @@ +import { watchWorkspaceAgentLogs } from "api/api"; +import type { WorkspaceAgent, WorkspaceAgentLog } from "api/typesGenerated"; +import { displayError } from "components/GlobalSnackbar/utils"; +import { useEffect, useState } from "react"; + +export function useAgentLogs( + agent: WorkspaceAgent, + enabled: boolean, +): readonly WorkspaceAgentLog[] { + const [logs, setLogs] = useState([]); + + useEffect(() => { + if (!enabled) { + // Clean up the logs when the agent is not enabled. So it can receive logs + // from the beginning without duplicating the logs. + setLogs([]); + return; + } + + // Always fetch the logs from the beginning. We may want to optimize this in + // the future, but it would add some complexity in the code that maybe does + // not worth it. + const socket = watchWorkspaceAgentLogs(agent.id, { after: 0 }); + socket.addEventListener("message", (e) => { + if (e.parseError) { + console.warn("Error parsing agent log: ", e.parseError); + return; + } + setLogs((logs) => [...logs, ...e.parsedMessage]); + }); + + socket.addEventListener("error", (e) => { + console.error("Error in agent log socket: ", e); + displayError( + "Unable to watch the agent logs", + "Please try refreshing the browser", + ); + socket.close(); + }); + + return () => { + socket.close(); + }; + }, [agent.id, enabled]); + + return logs; +} diff --git a/site/src/modules/workspaces/WorkspaceMoreActions/DownloadLogsDialog.stories.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/DownloadLogsDialog.stories.tsx index ad925798ac8d3..c8eab563c58ef 100644 --- a/site/src/modules/workspaces/WorkspaceMoreActions/DownloadLogsDialog.stories.tsx +++ b/site/src/modules/workspaces/WorkspaceMoreActions/DownloadLogsDialog.stories.tsx @@ -20,7 +20,7 @@ const meta: Meta = { data: generateLogs(200), }, { - key: agentLogsKey(MockWorkspace.id, MockWorkspaceAgent.id), + key: agentLogsKey(MockWorkspaceAgent.id), data: generateLogs(400), }, ], @@ -41,7 +41,7 @@ export const Loading: Story = { data: undefined, }, { - key: agentLogsKey(MockWorkspace.id, MockWorkspaceAgent.id), + key: agentLogsKey(MockWorkspaceAgent.id), data: undefined, }, ], diff --git a/site/src/modules/workspaces/WorkspaceMoreActions/DownloadLogsDialog.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/DownloadLogsDialog.tsx index 863bcb07061da..4a825ee6c3b68 100644 --- a/site/src/modules/workspaces/WorkspaceMoreActions/DownloadLogsDialog.tsx +++ b/site/src/modules/workspaces/WorkspaceMoreActions/DownloadLogsDialog.tsx @@ -53,7 +53,7 @@ export const DownloadLogsDialog: FC = ({ const agentLogQueries = useQueries({ queries: allUniqueAgents.map((agent) => ({ - ...agentLogs(workspace.id, agent.id), + ...agentLogs(agent.id), enabled: open, })), }); diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx index e291497a58fe0..f35b5b8465425 100644 --- a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx +++ b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx @@ -21,7 +21,7 @@ import { useSearchParamsKey } from "hooks/useSearchParamsKey"; import { BuildAvatar } from "modules/builds/BuildAvatar/BuildAvatar"; import { DashboardFullPage } from "modules/dashboard/DashboardLayout"; import { AgentLogs } from "modules/resources/AgentLogs/AgentLogs"; -import { useAgentLogs } from "modules/resources/AgentLogs/useAgentLogs"; +import { useAgentLogs } from "modules/resources/useAgentLogs"; import { WorkspaceBuildData, WorkspaceBuildDataSkeleton, @@ -212,13 +212,9 @@ export const WorkspaceBuildPageView: FC = ({ )} - {tabState.value === "build" ? ( - - ) : ( - + {tabState.value === "build" && } + {tabState.value !== "build" && selectedAgent && ( + )}
@@ -286,15 +282,12 @@ const BuildLogsContent: FC<{ logs?: ProvisionerJobLog[] }> = ({ logs }) => { ); }; -const AgentLogsContent: FC<{ workspaceId: string; agent: WorkspaceAgent }> = ({ - agent, - workspaceId, -}) => { - const logs = useAgentLogs({ - workspaceId, - agentId: agent.id, - agentLifeCycleState: agent.lifecycle_state, - }); +type AgentLogsContentProps = { + agent: WorkspaceAgent; +}; + +const AgentLogsContent: FC = ({ agent }) => { + const logs = useAgentLogs(agent, true); if (!logs) { return ; diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx index b94889b6709e7..19dde8871045f 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx @@ -222,7 +222,7 @@ export const OpenDownloadLogs: Story = { data: generateLogs(200), }, { - key: agentLogsKey(Mocks.MockWorkspace.id, Mocks.MockWorkspaceAgent.id), + key: agentLogsKey(Mocks.MockWorkspaceAgent.id), data: generateLogs(400), }, ], diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index 7489be8772ee4..3f217a86a3aad 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -50,12 +50,7 @@ const renderWorkspacePage = async ( jest .spyOn(API, "getDeploymentConfig") .mockResolvedValueOnce(MockDeploymentConfig); - jest - .spyOn(apiModule, "watchWorkspaceAgentLogs") - .mockImplementation((_, options) => { - options.onDone?.(); - return new WebSocket(""); - }); + jest.spyOn(apiModule, "watchWorkspaceAgentLogs"); renderWithAuth(, { ...options, diff --git a/site/src/testHelpers/storybook.tsx b/site/src/testHelpers/storybook.tsx index 84b2f3dd1a4b8..939ff91cb6c6c 100644 --- a/site/src/testHelpers/storybook.tsx +++ b/site/src/testHelpers/storybook.tsx @@ -75,6 +75,8 @@ export const withWebSocket = (Story: FC, { parameters }: StoryContext) => { let callEventsDelay: number; window.WebSocket = class WebSocket { + public readyState = 1; + addEventListener(type: string, callback: CallbackFn) { listeners.set(type, callback); @@ -93,6 +95,8 @@ export const withWebSocket = (Story: FC, { parameters }: StoryContext) => { }, 0); } + removeEventListener(type: string, callback: CallbackFn) {} + close() {} } as unknown as typeof window.WebSocket; From 3011eca0c541666f12177e1610f9c31eb638dfe4 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 15 May 2025 15:42:09 -0300 Subject: [PATCH 67/88] chore: replace MUI icons with Lucide icons - 16 (#17862) Close -> XIcon WarningOutlined -> TriangleAlertIcon FileCopyOutlined -> CopyIcon KeyboardArrowRight -> ChevronRightIcon Add -> PlusIcon Send -> SendIcon ChevronRight -> ChevronRightIcon MoreHorizOutlined -> EllipsisIcon --- site/src/modules/provisioners/ProvisionerTag.tsx | 7 ++----- .../templates/TemplateFiles/TemplateFileTree.tsx | 2 +- .../workspaces/WorkspaceTiming/Chart/Blocks.tsx | 4 ++-- .../workspaces/WorkspaceTiming/Chart/Chart.tsx | 4 ++-- site/src/pages/ChatPage/ChatLanding.tsx | 2 +- site/src/pages/ChatPage/ChatLayout.tsx | 4 ++-- site/src/pages/ChatPage/ChatMessages.tsx | 2 +- site/src/pages/CreateTemplatePage/BuildLogsDrawer.tsx | 11 +++-------- .../OAuth2AppsSettingsPage/EditOAuth2AppPageView.tsx | 9 ++++----- .../OAuth2AppsSettingsPageView.tsx | 11 ++--------- site/src/pages/GroupsPage/GroupPage.tsx | 4 ++-- site/src/pages/GroupsPage/GroupsPage.tsx | 4 ++-- site/src/pages/GroupsPage/GroupsPageView.tsx | 5 ++--- .../OrganizationMembersPageView.tsx | 4 ++-- .../TemplateEmbedPage/TemplateEmbedPage.tsx | 5 ++--- site/src/pages/TemplatePage/TemplatePageHeader.tsx | 4 ++-- .../TemplatePermissionsPageView.tsx | 4 ++-- .../TemplateVersionEditor.tsx | 6 +++--- .../TemplateVersionPage/TemplateVersionPageView.tsx | 4 ++-- 19 files changed, 39 insertions(+), 57 deletions(-) diff --git a/site/src/modules/provisioners/ProvisionerTag.tsx b/site/src/modules/provisioners/ProvisionerTag.tsx index 94467497cfa1b..62806edc4c15e 100644 --- a/site/src/modules/provisioners/ProvisionerTag.tsx +++ b/site/src/modules/provisioners/ProvisionerTag.tsx @@ -1,10 +1,7 @@ import type { Interpolation, Theme } from "@emotion/react"; -import CloseIcon from "@mui/icons-material/Close"; import IconButton from "@mui/material/IconButton"; import { Pill } from "components/Pill/Pill"; -import { CircleCheck as CircleCheckIcon } from "lucide-react"; -import { CircleMinus as CircleMinusIcon } from "lucide-react"; -import { Tag as TagIcon } from "lucide-react"; +import { CircleCheckIcon, CircleMinusIcon, TagIcon, XIcon } from "lucide-react"; import type { ComponentProps, FC } from "react"; const parseBool = (s: string): { valid: boolean; value: boolean } => { @@ -51,7 +48,7 @@ export const ProvisionerTag: FC = ({ onDelete(tagName); }} > - + Delete {tagName} diff --git a/site/src/modules/templates/TemplateFiles/TemplateFileTree.tsx b/site/src/modules/templates/TemplateFiles/TemplateFileTree.tsx index cfebbd81eee11..7c61519574254 100644 --- a/site/src/modules/templates/TemplateFiles/TemplateFileTree.tsx +++ b/site/src/modules/templates/TemplateFiles/TemplateFileTree.tsx @@ -1,11 +1,11 @@ import { css } from "@emotion/react"; -import ChevronRightIcon from "@mui/icons-material/ChevronRight"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import FormatAlignLeftOutlined from "@mui/icons-material/FormatAlignLeftOutlined"; import Menu from "@mui/material/Menu"; import MenuItem from "@mui/material/MenuItem"; import { SimpleTreeView, TreeItem } from "@mui/x-tree-view"; import { DockerIcon } from "components/Icons/DockerIcon"; +import { ChevronRightIcon } from "lucide-react"; import { type CSSProperties, type ElementType, type FC, useState } from "react"; import type { FileTree } from "utils/filetree"; diff --git a/site/src/modules/workspaces/WorkspaceTiming/Chart/Blocks.tsx b/site/src/modules/workspaces/WorkspaceTiming/Chart/Blocks.tsx index 00660c39f495c..c6e829e31f86f 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/Chart/Blocks.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/Chart/Blocks.tsx @@ -1,5 +1,5 @@ import type { Interpolation, Theme } from "@emotion/react"; -import MoreHorizOutlined from "@mui/icons-material/MoreHorizOutlined"; +import { EllipsisIcon } from "lucide-react"; import { type FC, useEffect, useRef, useState } from "react"; const spaceBetweenBlocks = 4; @@ -37,7 +37,7 @@ export const Blocks: FC = ({ count }) => { ))} {!hasSpacing && (
- +
)}
diff --git a/site/src/modules/workspaces/WorkspaceTiming/Chart/Chart.tsx b/site/src/modules/workspaces/WorkspaceTiming/Chart/Chart.tsx index cdef0fc68bdc2..08c365e957a78 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/Chart/Chart.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/Chart/Chart.tsx @@ -1,9 +1,9 @@ import type { Interpolation, Theme } from "@emotion/react"; -import ChevronRight from "@mui/icons-material/ChevronRight"; import { SearchField, type SearchFieldProps, } from "components/SearchField/SearchField"; +import { ChevronRightIcon } from "lucide-react"; import type { FC, HTMLProps } from "react"; import React, { useEffect, useRef } from "react"; import type { BarColors } from "./Bar"; @@ -81,7 +81,7 @@ export const ChartBreadcrumbs: FC = ({ {!isLast && (
  • - +
  • )} diff --git a/site/src/pages/ChatPage/ChatLanding.tsx b/site/src/pages/ChatPage/ChatLanding.tsx index 060752f895313..9ce232f6b3105 100644 --- a/site/src/pages/ChatPage/ChatLanding.tsx +++ b/site/src/pages/ChatPage/ChatLanding.tsx @@ -1,5 +1,4 @@ import { useTheme } from "@emotion/react"; -import SendIcon from "@mui/icons-material/Send"; import Button from "@mui/material/Button"; import IconButton from "@mui/material/IconButton"; import Paper from "@mui/material/Paper"; @@ -9,6 +8,7 @@ import { createChat } from "api/queries/chats"; import type { Chat } from "api/typesGenerated"; import { Margins } from "components/Margins/Margins"; import { useAuthenticated } from "hooks"; +import { SendIcon } from "lucide-react"; import { type FC, type FormEvent, useState } from "react"; import { useMutation, useQueryClient } from "react-query"; import { useNavigate } from "react-router-dom"; diff --git a/site/src/pages/ChatPage/ChatLayout.tsx b/site/src/pages/ChatPage/ChatLayout.tsx index 77de96af01595..ce69379bc9b27 100644 --- a/site/src/pages/ChatPage/ChatLayout.tsx +++ b/site/src/pages/ChatPage/ChatLayout.tsx @@ -1,5 +1,4 @@ import { useTheme } from "@emotion/react"; -import AddIcon from "@mui/icons-material/Add"; import Button from "@mui/material/Button"; import List from "@mui/material/List"; import ListItem from "@mui/material/ListItem"; @@ -13,6 +12,7 @@ import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Loader } from "components/Loader/Loader"; import { Margins } from "components/Margins/Margins"; import { useAgenticChat } from "contexts/useAgenticChat"; +import { PlusIcon } from "lucide-react"; import { type FC, type PropsWithChildren, @@ -169,7 +169,7 @@ export const ChatLayout: FC = () => { diff --git a/site/src/pages/GroupsPage/GroupsPage.tsx b/site/src/pages/GroupsPage/GroupsPage.tsx index 46903157734e7..4c1e6f9f1633d 100644 --- a/site/src/pages/GroupsPage/GroupsPage.tsx +++ b/site/src/pages/GroupsPage/GroupsPage.tsx @@ -1,4 +1,3 @@ -import GroupAdd from "@mui/icons-material/GroupAddOutlined"; import { getErrorMessage } from "api/errors"; import { groupsByOrganization } from "api/queries/groups"; import { organizationsPermissions } from "api/queries/organizations"; @@ -12,6 +11,7 @@ import { SettingsHeaderTitle, } from "components/SettingsHeader/SettingsHeader"; import { Stack } from "components/Stack/Stack"; +import { PlusIcon } from "lucide-react"; import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; import { RequirePermission } from "modules/permissions/RequirePermission"; import { type FC, useEffect } from "react"; @@ -95,7 +95,7 @@ const GroupsPage: FC = () => { {groupsEnabled && permissions.createGroup && ( diff --git a/site/src/pages/GroupsPage/GroupsPageView.tsx b/site/src/pages/GroupsPage/GroupsPageView.tsx index c1cc60ec83aa6..296425d2ebad5 100644 --- a/site/src/pages/GroupsPage/GroupsPageView.tsx +++ b/site/src/pages/GroupsPage/GroupsPageView.tsx @@ -1,5 +1,4 @@ import type { Interpolation, Theme } from "@emotion/react"; -import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight"; import Skeleton from "@mui/material/Skeleton"; import type { Group } from "api/typesGenerated"; import { Avatar } from "components/Avatar/Avatar"; @@ -23,7 +22,7 @@ import { TableRowSkeleton, } from "components/TableLoader/TableLoader"; import { useClickableTableRow } from "hooks"; -import { PlusIcon } from "lucide-react"; +import { ChevronRightIcon, PlusIcon } from "lucide-react"; import type { FC } from "react"; import { Link as RouterLink, useNavigate } from "react-router-dom"; import { docs } from "utils/docs"; @@ -158,7 +157,7 @@ const GroupRow: FC = ({ group }) => {
    - +
    diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx index dc507d567b3c0..99e80cb6de397 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx @@ -1,4 +1,3 @@ -import PersonAdd from "@mui/icons-material/PersonAdd"; import { getErrorMessage } from "api/errors"; import type { Group, @@ -35,6 +34,7 @@ import { } from "components/Table/Table"; import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete"; import type { PaginationResultInfo } from "hooks/usePaginatedQuery"; +import { UserPlusIcon } from "lucide-react"; import { EllipsisVertical, TriangleAlert } from "lucide-react"; import { UserGroupsCell } from "pages/UsersPage/UsersTable/UserGroupsCell"; import { type FC, useState } from "react"; @@ -243,7 +243,7 @@ const AddOrganizationMember: FC = ({ variant="outline" > - + Add user diff --git a/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.tsx b/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.tsx index bcbab3fe49ad2..74295ed63cf72 100644 --- a/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.tsx +++ b/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.tsx @@ -1,4 +1,3 @@ -import FileCopyOutlined from "@mui/icons-material/FileCopyOutlined"; import Button from "@mui/material/Button"; import FormControlLabel from "@mui/material/FormControlLabel"; import Radio from "@mui/material/Radio"; @@ -9,7 +8,7 @@ import { FormSection, VerticalForm } from "components/Form/Form"; import { Loader } from "components/Loader/Loader"; import { RichParameterInput } from "components/RichParameterInput/RichParameterInput"; import { useClipboard } from "hooks/useClipboard"; -import { CheckIcon } from "lucide-react"; +import { CheckIcon, CopyIcon } from "lucide-react"; import { useTemplateLayoutContext } from "pages/TemplatePage/TemplateLayout"; import { type FC, useEffect, useState } from "react"; import { Helmet } from "react-helmet-async"; @@ -189,7 +188,7 @@ export const TemplateEmbedPageView: FC = ({ clipboard.showCopiedSuccess ? ( ) : ( - + ) } variant="contained" diff --git a/site/src/pages/TemplatePage/TemplatePageHeader.tsx b/site/src/pages/TemplatePage/TemplatePageHeader.tsx index 7379ac0da0a96..94883e7b6c134 100644 --- a/site/src/pages/TemplatePage/TemplatePageHeader.tsx +++ b/site/src/pages/TemplatePage/TemplatePageHeader.tsx @@ -1,5 +1,4 @@ import EditIcon from "@mui/icons-material/EditOutlined"; -import CopyIcon from "@mui/icons-material/FileCopyOutlined"; import Button from "@mui/material/Button"; import { workspaces } from "api/queries/workspaces"; import type { @@ -27,6 +26,7 @@ import { } from "components/PageHeader/PageHeader"; import { Pill } from "components/Pill/Pill"; import { Stack } from "components/Stack/Stack"; +import { CopyIcon } from "lucide-react"; import { EllipsisVertical, PlusIcon, @@ -99,7 +99,7 @@ const TemplateMenu: FC = ({ navigate(`/templates/new?fromTemplate=${templateId}`) } > - + Duplicate…
    diff --git a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.tsx b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.tsx index e00708a8b37ff..62de4d7d5271b 100644 --- a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.tsx @@ -1,5 +1,4 @@ import type { Interpolation, Theme } from "@emotion/react"; -import PersonAdd from "@mui/icons-material/PersonAdd"; import MenuItem from "@mui/material/MenuItem"; import Select, { type SelectProps } from "@mui/material/Select"; import Table from "@mui/material/Table"; @@ -31,6 +30,7 @@ import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; import { Spinner } from "components/Spinner/Spinner"; import { Stack } from "components/Stack/Stack"; import { TableLoader } from "components/TableLoader/TableLoader"; +import { UserPlusIcon } from "lucide-react"; import { EllipsisVertical } from "lucide-react"; import { type FC, useState } from "react"; import { getGroupSubtitle } from "utils/groups"; @@ -121,7 +121,7 @@ const AddTemplateUserOrGroup: FC = ({ type="submit" > - + Add member diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx index 03dd9d47231bd..c2e729d994569 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx @@ -1,5 +1,4 @@ import { type Interpolation, type Theme, useTheme } from "@emotion/react"; -import WarningOutlined from "@mui/icons-material/WarningOutlined"; import Button from "@mui/material/Button"; import IconButton from "@mui/material/IconButton"; import Tooltip from "@mui/material/Tooltip"; @@ -24,6 +23,7 @@ import { } from "components/FullPageLayout/Topbar"; import { displayError } from "components/GlobalSnackbar/utils"; import { Loader } from "components/Loader/Loader"; +import { TriangleAlertIcon } from "lucide-react"; import { ChevronLeftIcon } from "lucide-react"; import { PlayIcon, PlusIcon, XIcon } from "lucide-react"; import { linkToTemplate, useLinks } from "modules/navigation"; @@ -461,11 +461,11 @@ export const TemplateVersionEditor: FC = ({ textAlign: "center", }} > -

    = ({ {createWorkspaceUrl && ( ); diff --git a/site/src/modules/resources/PortForwardButton.tsx b/site/src/modules/resources/PortForwardButton.tsx index a4b8ee8277ebc..026db8601c800 100644 --- a/site/src/modules/resources/PortForwardButton.tsx +++ b/site/src/modules/resources/PortForwardButton.tsx @@ -2,15 +2,13 @@ import { type Interpolation, type Theme, useTheme } from "@emotion/react"; import LockIcon from "@mui/icons-material/Lock"; import LockOpenIcon from "@mui/icons-material/LockOpen"; import SensorsIcon from "@mui/icons-material/Sensors"; -import MUIButton from "@mui/material/Button"; -import CircularProgress from "@mui/material/CircularProgress"; import FormControl from "@mui/material/FormControl"; import Link from "@mui/material/Link"; import MenuItem from "@mui/material/MenuItem"; import Select from "@mui/material/Select"; import Stack from "@mui/material/Stack"; import TextField from "@mui/material/TextField"; -import Tooltip from "@mui/material/Tooltip"; +import MUITooltip from "@mui/material/Tooltip"; import { API } from "api/api"; import { deleteWorkspacePortShare, @@ -33,6 +31,12 @@ import { HelpTooltipTitle, } from "components/HelpTooltip/HelpTooltip"; import { Spinner } from "components/Spinner/Spinner"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "components/Tooltip/Tooltip"; import { Popover, PopoverContent, @@ -40,7 +44,12 @@ import { } from "components/deprecated/Popover/Popover"; import { type FormikContextType, useFormik } from "formik"; import { type ClassName, useClassName } from "hooks/useClassName"; -import { ChevronDownIcon, ExternalLinkIcon, X as XIcon } from "lucide-react"; +import { + ChevronDownIcon, + ExternalLinkIcon, + ShareIcon, + X as XIcon, +} from "lucide-react"; import { useDashboard } from "modules/dashboard/useDashboard"; import { type FC, useState } from "react"; import { useMutation, useQuery } from "react-query"; @@ -77,26 +86,13 @@ export const PortForwardButton: FC = (props) => { return ( - } - css={{ fontSize: 13, padding: "8px 12px" }} - startIcon={ - portsQuery.data ? ( -

    - - {portsQuery.data.ports.length} - -
    - ) : ( - - ) - } - > + = ({ canSharePorts && template.max_port_share_level === "public"; const disabledPublicMenuItem = ( - + {/* Tooltips don't work directly on disabled MenuItem components so you must wrap in div. */}
    Public
    -
    + ); return ( @@ -297,24 +293,17 @@ export const PortForwardPopoverView: FC = ({ required css={styles.newPortInput} /> - - - + + + + + + Connect to port + +
    @@ -369,21 +358,29 @@ export const PortForwardPopoverView: FC = ({ alignItems="center" > {canSharePorts && ( - { - await upsertSharedPortMutation.mutateAsync({ - agent_name: agent.name, - port: port.port, - protocol: listeningPortProtocol, - share_level: "authenticated", - }); - await sharedPortsQuery.refetch(); - }} - > - Share - + + + + + + Share this port + + )} @@ -483,10 +480,9 @@ export const PortForwardPopoverView: FC = ({ )} - { await deleteSharedPortMutation.mutateAsync({ agent_name: agent.name, @@ -502,7 +498,7 @@ export const PortForwardPopoverView: FC = ({ color: theme.palette.text.primary, }} /> - + ); @@ -617,11 +613,6 @@ const styles = { }, }), - deleteButton: () => ({ - minWidth: 30, - padding: 0, - }), - newPortForm: (theme) => ({ border: `1px solid ${theme.palette.divider}`, borderRadius: "4px", diff --git a/site/src/modules/resources/SSHButton/SSHButton.tsx b/site/src/modules/resources/SSHButton/SSHButton.tsx index 2673d8a8e2241..42e2b3828f3ae 100644 --- a/site/src/modules/resources/SSHButton/SSHButton.tsx +++ b/site/src/modules/resources/SSHButton/SSHButton.tsx @@ -1,5 +1,5 @@ import type { Interpolation, Theme } from "@emotion/react"; -import Button from "@mui/material/Button"; +import { Button } from "components/Button/Button"; import { CodeExample } from "components/CodeExample/CodeExample"; import { HelpTooltipLink, @@ -34,12 +34,12 @@ export const AgentSSHButton: FC = ({ @@ -96,12 +96,12 @@ export const AgentDevcontainerSSHButton: FC< diff --git a/site/src/modules/templates/TemplateExampleCard/TemplateExampleCard.tsx b/site/src/modules/templates/TemplateExampleCard/TemplateExampleCard.tsx index f003a886552e1..bf5c03f96bd2d 100644 --- a/site/src/modules/templates/TemplateExampleCard/TemplateExampleCard.tsx +++ b/site/src/modules/templates/TemplateExampleCard/TemplateExampleCard.tsx @@ -1,7 +1,7 @@ import type { Interpolation, Theme } from "@emotion/react"; -import Button from "@mui/material/Button"; import Link from "@mui/material/Link"; import type { TemplateExample } from "api/typesGenerated"; +import { Button } from "components/Button/Button"; import { ExternalImage } from "components/ExternalImage/ExternalImage"; import { Pill } from "components/Pill/Pill"; import type { FC, HTMLAttributes } from "react"; @@ -55,12 +55,10 @@ export const TemplateExampleCard: FC = ({
    -
    diff --git a/site/src/pages/ChatPage/ChatLanding.tsx b/site/src/pages/ChatPage/ChatLanding.tsx index 9ce232f6b3105..fb49c609e6639 100644 --- a/site/src/pages/ChatPage/ChatLanding.tsx +++ b/site/src/pages/ChatPage/ChatLanding.tsx @@ -1,11 +1,11 @@ import { useTheme } from "@emotion/react"; -import Button from "@mui/material/Button"; import IconButton from "@mui/material/IconButton"; import Paper from "@mui/material/Paper"; import Stack from "@mui/material/Stack"; import TextField from "@mui/material/TextField"; import { createChat } from "api/queries/chats"; import type { Chat } from "api/typesGenerated"; +import { Button } from "components/Button/Button"; import { Margins } from "components/Margins/Margins"; import { useAuthenticated } from "hooks"; import { SendIcon } from "lucide-react"; @@ -89,19 +89,19 @@ const ChatLanding: FC = () => { sx={{ mb: 2 }} > - - Cancel - + @@ -151,9 +144,9 @@ const RequestOTPSuccess: FC<{ email: string }> = ({ email }) => { Contact your deployment administrator if you encounter issues.

    - - Back to login - + ); diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/IntervalMenu.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/IntervalMenu.tsx index 6f14cb0e38e75..c7da8332a29ab 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/IntervalMenu.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/IntervalMenu.tsx @@ -1,7 +1,7 @@ import ExpandMoreOutlined from "@mui/icons-material/ExpandMoreOutlined"; -import Button from "@mui/material/Button"; import Menu from "@mui/material/Menu"; import MenuItem from "@mui/material/MenuItem"; +import { Button } from "components/Button/Button"; import { CheckIcon } from "lucide-react"; import { type FC, useRef, useState } from "react"; @@ -38,9 +38,10 @@ export const IntervalMenu: FC = ({ value, onChange }) => { aria-haspopup="true" aria-expanded={open ? "true" : undefined} onClick={() => setOpen(true)} - endIcon={} + variant="outline" > {insightsIntervals[value].label} + = ({ ))} - } diff --git a/site/src/pages/TemplatesPage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/TemplatesPageView.tsx index a2b32fed58e7e..d81dc4e654994 100644 --- a/site/src/pages/TemplatesPage/TemplatesPageView.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPageView.tsx @@ -1,6 +1,5 @@ import type { Interpolation, Theme } from "@emotion/react"; import ArrowForwardOutlined from "@mui/icons-material/ArrowForwardOutlined"; -import MuiButton from "@mui/material/Button"; import Skeleton from "@mui/material/Skeleton"; import { hasError, isApiValidationError } from "api/errors"; import type { Template, TemplateExample } from "api/typesGenerated"; @@ -44,7 +43,7 @@ import { PlusIcon } from "lucide-react"; import { linkToTemplate, useLinks } from "modules/navigation"; import type { WorkspacePermissions } from "modules/permissions/workspaces"; import type { FC } from "react"; -import { Link, useNavigate } from "react-router-dom"; +import { Link as RouterLink, useNavigate } from "react-router-dom"; import { createDayString } from "utils/createDayString"; import { docs } from "utils/docs"; import { @@ -163,19 +162,20 @@ const TemplateRow: FC = ({ ) : workspacePermissions?.[template.organization_id] ?.createWorkspaceForUserID ? ( - } + ) : null} @@ -204,18 +204,20 @@ export const TemplatesPageView: FC = ({ const isLoading = !templates; const isEmpty = templates && templates.length === 0; - const createTemplateAction = ( - - ); - return ( - + + + + New template + + + ) + } + > Templates From d6cb9b49b71db3bb93dc730f6316c8ed17529e5a Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Fri, 16 May 2025 23:05:33 +0100 Subject: [PATCH 85/88] feat: setup url autofill for dynamic parameters (#17739) resolves coder/preview#80 Parameter autofill allows setting parameters from the url using the format param.[param name]=["purple","green"] Example: http://localhost:8080/templates/coder/scratch/workspace?param.list=%5b%22purple%22%2c%22green%22%5d%0a The goal is to maintain feature parity of for autofill with dynamic parameters. Note: user history autofill is no longer being used and is being removed. --- site/src/components/Select/Select.tsx | 7 +- .../DynamicParameter/DynamicParameter.tsx | 314 ++++++++++++------ .../CreateWorkspacePageExperimental.tsx | 101 ++++-- .../CreateWorkspacePageViewExperimental.tsx | 44 +-- 4 files changed, 307 insertions(+), 159 deletions(-) diff --git a/site/src/components/Select/Select.tsx b/site/src/components/Select/Select.tsx index 43839d9a008c3..3d2f8ffc3b706 100644 --- a/site/src/components/Select/Select.tsx +++ b/site/src/components/Select/Select.tsx @@ -15,10 +15,13 @@ export const SelectValue = SelectPrimitive.Value; export const SelectTrigger = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( + React.ComponentPropsWithoutRef & { + id?: string; + } +>(({ className, children, id, ...props }, ref) => ( void; disabled?: boolean; isPreset?: boolean; + autofill: boolean; } export const DynamicParameter: FC = ({ parameter, + value, onChange, disabled, isPreset, + autofill = false, }) => { const id = useId(); @@ -57,14 +63,31 @@ export const DynamicParameter: FC = ({ className="flex flex-col gap-2" data-testid={`parameter-field-${parameter.name}`} > - +
    - + {parameter.form_type === "input" || + parameter.form_type === "textarea" ? ( + + ) : ( + + )}
    {parameter.diagnostics.length > 0 && ( @@ -76,10 +99,16 @@ export const DynamicParameter: FC = ({ interface ParameterLabelProps { parameter: PreviewParameter; isPreset?: boolean; + autofill: boolean; + id: string; } -const ParameterLabel: FC = ({ parameter, isPreset }) => { - const hasDescription = parameter.description && parameter.description !== ""; +const ParameterLabel: FC = ({ + parameter, + isPreset, + autofill, + id, +}) => { const displayName = parameter.display_name ? parameter.display_name : parameter.name; @@ -95,7 +124,10 @@ const ParameterLabel: FC = ({ parameter, isPreset }) => { )}
    -